Some refactoring

This commit is contained in:
Shurkys 2025-01-17 02:23:11 +02:00 committed by GitHub
parent 86fa672d1f
commit b5dd7ea6e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -10,16 +10,15 @@ import (
"io" "io"
"net/http" "net/http"
"strconv" "strconv"
"sync" "time"
packages_model "code.gitea.io/gitea/models/packages" packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages" packages_module "code.gitea.io/gitea/modules/packages"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages" "code.gitea.io/gitea/services/packages"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -46,157 +45,105 @@ type InstanceState struct {
Attributes map[string]any `json:"attributes"` Attributes map[string]any `json:"attributes"`
} }
var ( type LockInfo struct {
stateStorage = make(map[string]*TFState) ID string `json:"id"`
stateLocks = make(map[string]string) Created string `json:"created"`
storeMutex sync.Mutex }
)
func apiError(ctx *context.Context, status int, obj any) { var stateLocks = make(map[string]LockInfo)
helper.LogAndProcessError(ctx, status, obj, func(message string) {
type Error struct { func apiError(ctx *context.Context, status int, message string) {
Status int `json:"status"` log.Error("Terraform API Error: %d - %s", status, message)
Message string `json:"message"` ctx.JSON(status, map[string]string{"error": message})
}
func getLockID(ctx *context.Context) (string, error) {
var lock struct {
ID string `json:"ID"`
}
// Read the body of the request and try to parse the JSON
body, err := io.ReadAll(ctx.Req.Body)
if err == nil && len(body) > 0 {
if err := json.Unmarshal(body, &lock); err != nil {
log.Error("Failed to unmarshal request body: %v", err)
return "", err
} }
ctx.JSON(status, struct { }
Errors []Error `json:"errors"`
}{ // We check the presence of lock ID in the request body or request parameters
Errors: []Error{ if lock.ID == "" {
{Status: status, Message: message}, lock.ID = ctx.Req.URL.Query().Get("ID")
}, }
})
}) if lock.ID == "" {
apiError(ctx, http.StatusBadRequest, "Missing lock ID")
return "", fmt.Errorf("missing lock ID")
}
log.Info("Extracted lockID: %s", lock.ID)
return lock.ID, nil
} }
func GetState(ctx *context.Context) { func GetState(ctx *context.Context) {
stateName := ctx.PathParam("statename") stateName := ctx.PathParam("statename")
log.Info("Function GetState called with parameters: stateName=%s", stateName) log.Info("GetState called for: %s", stateName)
// Find the package version pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID,
OwnerID: ctx.Package.Owner.ID, Type: packages_model.TypeTerraform,
Type: packages_model.TypeTerraform, Name: packages_model.SearchValue{ExactMatch: true, Value: stateName},
Name: packages_model.SearchValue{
ExactMatch: true,
Value: stateName,
},
HasFileWithName: stateName, HasFileWithName: stateName,
IsInternal: optional.Some(false), IsInternal: optional.Some(false),
Sort: packages_model.SortCreatedDesc,
}) })
if err != nil { if err != nil {
log.Error("Failed to search package versions for state %s: %v", stateName, err) apiError(ctx, http.StatusInternalServerError, "Failed to fetch latest versions")
apiError(ctx, http.StatusInternalServerError, err)
return return
} }
// If no version is found, return 204
if len(pvs) == 0 { if len(pvs) == 0 {
log.Info("No existing state found for %s, returning 204 No Content", stateName) apiError(ctx, http.StatusNoContent, "No content available")
ctx.Resp.WriteHeader(http.StatusNoContent)
return return
} }
// Get the latest package version stream, _, _, err := packages.GetFileStreamByPackageNameAndVersion(ctx, &packages.PackageInfo{
stateVersion := pvs[0] Owner: ctx.Package.Owner,
if stateVersion == nil { PackageType: packages_model.TypeTerraform,
log.Error("State version is nil for state %s", stateName) Name: stateName,
apiError(ctx, http.StatusInternalServerError, "Invalid state version") Version: pvs[0].Version,
return }, &packages.PackageFileInfo{Filename: stateName})
}
log.Info("Fetching file stream for state %s with version %s", stateName, stateVersion.Version)
// Log the parameters of GetFileStreamByPackageNameAndVersion call
log.Info("Fetching file stream with params: Owner=%v, PackageType=%v, Name=%v, Version=%v, Filename=%v",
ctx.Package.Owner,
packages_model.TypeTerraform,
stateName,
stateVersion.Version,
stateName,
)
// Fetch the file stream
s, _, _, err := packages_service.GetFileStreamByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeTerraform,
Name: stateName,
Version: stateVersion.Version,
},
&packages_service.PackageFileInfo{
Filename: stateName,
},
)
if err != nil { if err != nil {
log.Error("Error fetching file stream for state %s: %v", stateName, err) switch {
if errors.Is(err, packages_model.ErrPackageNotExist) { case errors.Is(err, packages_model.ErrPackageNotExist):
log.Error("Package does not exist: %v", err)
apiError(ctx, http.StatusNotFound, "Package not found") apiError(ctx, http.StatusNotFound, "Package not found")
return case errors.Is(err, packages_model.ErrPackageFileNotExist):
}
if errors.Is(err, packages_model.ErrPackageFileNotExist) {
log.Error("Package file does not exist: %v", err)
apiError(ctx, http.StatusNotFound, "File not found") apiError(ctx, http.StatusNotFound, "File not found")
return default:
apiError(ctx, http.StatusInternalServerError, err.Error())
} }
apiError(ctx, http.StatusInternalServerError, "Failed to fetch file stream")
return return
} }
defer s.Close() defer stream.Close()
// Read the file contents
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, s); err != nil {
log.Error("Failed to read state file for %s: %v", stateName, err)
apiError(ctx, http.StatusInternalServerError, "Failed to read state file")
return
}
// Deserialize the state
var state TFState var state TFState
if err := json.Unmarshal(buf.Bytes(), &state); err != nil { if err := json.NewDecoder(stream).Decode(&state); err != nil {
log.Error("Failed to unmarshal state file for %s: %v", stateName, err) apiError(ctx, http.StatusInternalServerError, "Failed to parse state file")
apiError(ctx, http.StatusInternalServerError, "Invalid state file format")
return return
} }
// Ensure lineage is set
if state.Lineage == "" { if state.Lineage == "" {
state.Lineage = uuid.NewString() state.Lineage = uuid.NewString()
log.Info("Generated new lineage for state %s: %s", stateName, state.Lineage) log.Info("Generated new lineage for state: %s", state.Lineage)
} }
// Send the state in the response
ctx.Resp.Header().Set("Content-Type", "application/json") ctx.Resp.Header().Set("Content-Type", "application/json")
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", stateName)) ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", stateName))
ctx.Resp.WriteHeader(http.StatusOK) ctx.JSON(http.StatusOK, state)
if _, writeErr := ctx.Resp.Write(buf.Bytes()); writeErr != nil {
log.Error("Failed to write response for state %s: %v", stateName, writeErr)
}
} }
// UpdateState updates or creates a new Terraform state and interacts with Gitea packages.
func UpdateState(ctx *context.Context) { func UpdateState(ctx *context.Context) {
stateName := ctx.PathParam("statename") stateName := ctx.PathParam("statename")
log.Info("UpdateState called for stateName: %s", stateName)
storeMutex.Lock()
defer storeMutex.Unlock()
// Check for the presence of a lock ID
requestLockID := ctx.Req.URL.Query().Get("ID")
if requestLockID == "" {
apiError(ctx, http.StatusBadRequest, "Missing ID query parameter")
return
}
// Check for blocking state
if lockID, locked := stateLocks[stateName]; locked && lockID != requestLockID {
apiError(ctx, http.StatusConflict, fmt.Sprintf("State %s is locked", stateName))
return
}
// Read the request body
body, err := io.ReadAll(ctx.Req.Body) body, err := io.ReadAll(ctx.Req.Body)
if err != nil { if err != nil {
apiError(ctx, http.StatusInternalServerError, "Failed to read request body") apiError(ctx, http.StatusInternalServerError, "Failed to read request body")
@ -209,37 +156,36 @@ func UpdateState(ctx *context.Context) {
return return
} }
// Getting the current serial pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID,
OwnerID: ctx.Package.Owner.ID, Type: packages_model.TypeTerraform,
Type: packages_model.TypeTerraform, Name: packages_model.SearchValue{ExactMatch: true, Value: stateName},
Name: packages_model.SearchValue{ExactMatch: true, Value: stateName}, HasFileWithName: stateName,
IsInternal: optional.Some(false), IsInternal: optional.Some(false),
Sort: packages_model.SortCreatedDesc,
}) })
if err != nil { if err != nil {
apiError(ctx, http.StatusInternalServerError, "Failed to search package versions") apiError(ctx, http.StatusInternalServerError, err.Error())
return return
} }
serial := uint64(0)
serial := uint64(1) // Start from 1
if len(pvs) > 0 { if len(pvs) > 0 {
lastSerial, _ := strconv.ParseUint(pvs[0].Version, 10, 64) if lastSerial, err := strconv.ParseUint(pvs[0].Version, 10, 64); err == nil {
serial = lastSerial + 1 serial = lastSerial + 1
}
} }
log.Info("State %s updated to serial %d", stateName, serial)
// Create package information
packageVersion := fmt.Sprintf("%d", serial) packageVersion := fmt.Sprintf("%d", serial)
packageInfo := &packages_service.PackageCreationInfo{
PackageInfo: packages_service.PackageInfo{ packageInfo := &packages.PackageCreationInfo{
PackageInfo: packages.PackageInfo{
Owner: ctx.Package.Owner, Owner: ctx.Package.Owner,
PackageType: packages_model.TypeTerraform, PackageType: packages_model.TypeTerraform,
Name: stateName, Name: stateName,
Version: packageVersion, Version: packageVersion,
}, },
SemverCompatible: true, Creator: ctx.Doer,
Creator: ctx.Doer, Metadata: newState,
Metadata: newState,
} }
buffer, err := packages_module.CreateHashedBufferFromReader(bytes.NewReader(body)) buffer, err := packages_module.CreateHashedBufferFromReader(bytes.NewReader(body))
@ -247,146 +193,86 @@ func UpdateState(ctx *context.Context) {
apiError(ctx, http.StatusInternalServerError, "Failed to create buffer") apiError(ctx, http.StatusInternalServerError, "Failed to create buffer")
return return
} }
_, _, err = packages.CreatePackageOrAddFileToExisting(ctx, packageInfo, &packages.PackageFileCreationInfo{
// Create/update package PackageFileInfo: packages.PackageFileInfo{Filename: stateName},
if _, _, err = packages_service.CreatePackageOrAddFileToExisting( Creator: ctx.Doer,
ctx, Data: buffer,
packageInfo, IsLead: true,
&packages_service.PackageFileCreationInfo{ })
PackageFileInfo: packages_service.PackageFileInfo{ if err != nil {
Filename: stateName,
},
Creator: ctx.Doer,
Data: buffer,
IsLead: true,
},
); err != nil {
apiError(ctx, http.StatusInternalServerError, "Failed to update package") apiError(ctx, http.StatusInternalServerError, "Failed to update package")
return return
} }
log.Info("State %s updated successfully with version %s", stateName, packageVersion) ctx.JSON(http.StatusOK, map[string]string{"message": "State updated successfully", "statename": stateName})
ctx.JSON(http.StatusOK, map[string]string{
"message": "State updated successfully",
"statename": stateName,
})
} }
// LockState locks a Terraform state to prevent updates.
func LockState(ctx *context.Context) { func LockState(ctx *context.Context) {
stateName := ctx.PathParam("statename") stateName := ctx.PathParam("statename")
log.Info("LockState called for state: %s", stateName) lockID, err := getLockID(ctx)
// Read the request body
body, err := io.ReadAll(ctx.Req.Body)
if err != nil { if err != nil {
log.Error("Failed to read request body: %v", err) apiError(ctx, http.StatusBadRequest, err.Error())
apiError(ctx, http.StatusInternalServerError, "Failed to read request body")
return return
} }
// Decode JSON and check lockID
var lockRequest struct {
ID string `json:"ID"`
}
if err := json.Unmarshal(body, &lockRequest); err != nil || lockRequest.ID == "" {
log.Error("Invalid lock request body: %v", err)
apiError(ctx, http.StatusBadRequest, "Invalid or missing lock ID")
return
}
storeMutex.Lock()
defer storeMutex.Unlock()
// Check if the state is locked // Check if the state is locked
if _, locked := stateLocks[stateName]; locked { if lockInfo, locked := stateLocks[stateName]; locked {
log.Warn("State %s is already locked", stateName) log.Warn("State %s is already locked", stateName)
apiError(ctx, http.StatusConflict, fmt.Sprintf("State %s is already locked", stateName))
// Generate a response for the conflict with information about the current lock
response := lockInfo // Return full information about the lock
ctx.JSON(http.StatusConflict, response)
return return
} }
// Set the lock // Set the lock
stateLocks[stateName] = lockRequest.ID stateLocks[stateName] = LockInfo{
log.Info("State %s locked with ID %s", stateName, lockRequest.ID) ID: lockID,
Created: time.Now().UTC().Format(time.RFC3339),
}
ctx.JSON(http.StatusOK, map[string]string{ log.Info("Locked state: %s with ID: %s", stateName, lockID)
"message": "State locked successfully", ctx.JSON(http.StatusOK, map[string]string{"message": "State locked successfully", "statename": stateName})
"statename": stateName,
})
} }
// UnlockState unlocks a Terraform state.
func UnlockState(ctx *context.Context) { func UnlockState(ctx *context.Context) {
stateName := ctx.PathParam("statename") stateName := ctx.PathParam("statename")
log.Info("UnlockState called for state: %s", stateName) lockID, err := getLockID(ctx)
if err != nil {
// Extract lockID from request body or parameters apiError(ctx, http.StatusBadRequest, err.Error())
var unlockRequest struct {
ID string `json:"ID"`
}
// Trying to read the request body
body, _ := io.ReadAll(ctx.Req.Body)
if len(body) > 0 {
_ = json.Unmarshal(body, &unlockRequest) // The error can be ignored, since the ID can also be in the query
}
// Check for ID presence
if unlockRequest.ID == "" {
log.Error("Missing lock ID in both query and request body")
apiError(ctx, http.StatusBadRequest, "Missing lock ID")
return return
} }
log.Info("Extracted lockID: %s", unlockRequest.ID)
storeMutex.Lock()
defer storeMutex.Unlock()
// Check the lock status // Check the lock status
currentLockID, locked := stateLocks[stateName] currentLockInfo, locked := stateLocks[stateName]
if !locked || currentLockID != unlockRequest.ID { if !locked || currentLockInfo.ID != lockID {
log.Warn("Unlock attempt failed for state %s with lock ID %s", stateName, unlockRequest.ID) log.Warn("Unlock attempt failed for state %s with lock ID %s", stateName, lockID)
apiError(ctx, http.StatusConflict, fmt.Sprintf("State %s is not locked or lock ID mismatch", stateName)) apiError(ctx, http.StatusConflict, fmt.Sprintf("State %s is not locked or lock ID mismatch", stateName))
return return
} }
// Remove the lock // Remove the lock
delete(stateLocks, stateName) delete(stateLocks, stateName)
log.Info("State %s unlocked successfully", stateName) log.Info("Unlocked state: %s with ID: %s", stateName, lockID)
ctx.JSON(http.StatusOK, map[string]string{"message": "State unlocked successfully"})
ctx.JSON(http.StatusOK, map[string]string{
"message": "State unlocked successfully",
"statename": stateName,
})
} }
// DeleteState deletes the Terraform state for a given name.
func DeleteState(ctx *context.Context) { func DeleteState(ctx *context.Context) {
stateName := ctx.PathParam("statename") stateName := ctx.PathParam("statename")
log.Info("Attempting to delete state: %s", stateName) pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraform, stateName)
if err != nil {
storeMutex.Lock() apiError(ctx, http.StatusInternalServerError, "Failed to fetch package versions")
defer storeMutex.Unlock()
// Check if a state or lock exists
_, stateExists := stateStorage[stateName]
_, lockExists := stateLocks[stateName]
if !stateExists && !lockExists {
log.Warn("State %s does not exist or is not locked", stateName)
apiError(ctx, http.StatusNotFound, "State not found")
return return
} }
if len(pvs) == 0 {
// Delete the state and lock ctx.Status(http.StatusNoContent)
delete(stateStorage, stateName) return
delete(stateLocks, stateName) }
for _, pv := range pvs {
log.Info("State %s deleted successfully", stateName) if err := packages.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
ctx.JSON(http.StatusOK, map[string]string{ apiError(ctx, http.StatusInternalServerError, "Failed to delete package version")
"message": "State deleted successfully", return
"statename": stateName, }
}) }
ctx.JSON(http.StatusOK, map[string]string{"message": "State deleted successfully"})
} }