Files
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

439 lines
12 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package terraform
import (
"errors"
"fmt"
"io"
"net/http"
"regexp"
"strconv"
"strings"
"unicode"
packages_model "github.com/hanzoai/git/models/packages"
"github.com/hanzoai/git/modules/globallock"
"github.com/hanzoai/git/modules/log"
"github.com/hanzoai/git/modules/optional"
packages_module "github.com/hanzoai/git/modules/packages"
terraform_module "github.com/hanzoai/git/modules/packages/terraform"
"github.com/hanzoai/git/routers/api/packages/helper"
"github.com/hanzoai/git/services/context"
packages_service "github.com/hanzoai/git/services/packages"
)
var packageNameRegex = regexp.MustCompile(`\A[-_+.\w]+\z`)
const (
stateFilename = "tfstate"
)
func apiError(ctx *context.Context, status int, obj any) {
message := helper.ProcessErrorForUser(ctx, status, obj)
ctx.PlainText(status, message)
}
// GetTerraformState serves the latest version of the state
func GetTerraformState(ctx *context.Context) {
stateName := ctx.PathParam("name")
pv, err := getLatestVersion(ctx, stateName)
if errors.Is(err, packages_model.ErrPackageNotExist) {
apiError(ctx, http.StatusNotFound, nil)
return
} else if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
streamState(ctx, stateName, pv.Version)
}
// GetTerraformStateBySerial serves a specific version of terraform state.
func GetTerraformStateBySerial(ctx *context.Context) {
streamState(ctx, ctx.PathParam("name"), ctx.PathParam("serial"))
}
// streamState serves the terraform state file
func streamState(ctx *context.Context, name, serial string) {
s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeTerraformState,
Name: name,
Version: serial,
},
&packages_service.PackageFileInfo{
Filename: stateFilename,
},
ctx.Req.Method,
)
if err != nil {
if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) {
apiError(ctx, http.StatusNotFound, err)
return
}
apiError(ctx, http.StatusInternalServerError, err)
return
}
helper.ServePackageFile(ctx, s, u, pf)
}
func isValidPackageName(packageName string) bool {
if len(packageName) == 1 && !unicode.IsLetter(rune(packageName[0])) && !unicode.IsNumber(rune(packageName[0])) {
return false
}
return packageNameRegex.MatchString(packageName) && packageName != ".."
}
// UploadState uploads the specific terraform package.
func UploadState(ctx *context.Context) {
packageName := ctx.PathParam("name")
if !isValidPackageName(packageName) {
apiError(ctx, http.StatusBadRequest, errors.New("invalid package name"))
return
}
lockKey := getLockKey(ctx)
release, err := globallock.Lock(ctx, lockKey)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer release()
p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, packageName)
if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if p != nil {
// Check lock
lock, err := terraform_module.GetLock(ctx, p.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
// If the state is locked, enforce the lock
if lock.IsLocked() && lock.ID != ctx.FormString("ID") {
ctx.JSON(http.StatusLocked, lock)
return
}
}
upload, needToClose, err := ctx.UploadStream()
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if needToClose {
defer upload.Close()
}
buf, err := packages_module.CreateHashedBufferFromReader(upload)
if err != nil {
log.Error("Error creating hashed buffer: %v", err)
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer buf.Close()
state, err := terraform_module.ParseState(buf)
if err != nil {
log.Error("Error decoding state: %v", err)
apiError(ctx, http.StatusBadRequest, err)
return
}
if _, err := buf.Seek(0, io.SeekStart); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
ctx,
&packages_service.PackageCreationInfo{
PackageInfo: packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeTerraformState,
Name: packageName,
Version: strconv.FormatUint(state.Serial, 10),
},
Creator: ctx.Doer,
},
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: stateFilename,
},
Creator: ctx.Doer,
Data: buf,
IsLead: true,
},
)
if err != nil {
switch {
case errors.Is(err, packages_model.ErrDuplicatePackageFile):
apiError(ctx, http.StatusConflict, err)
case errors.Is(err, packages_service.ErrQuotaTotalCount), errors.Is(err, packages_service.ErrQuotaTypeSize), errors.Is(err, packages_service.ErrQuotaTotalSize):
apiError(ctx, http.StatusForbidden, err)
default:
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
ctx.Status(http.StatusCreated)
}
// DeleteStateBySerial deletes the specific serial of a terraform package as long as it's not the latest one.
func DeleteStateBySerial(ctx *context.Context) {
lockKey := getLockKey(ctx)
release, err := globallock.Lock(ctx, lockKey)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer release()
serial := ctx.PathParam("serial")
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, ctx.PathParam("name"), serial)
if errors.Is(err, packages_model.ErrPackageNotExist) {
apiError(ctx, http.StatusNotFound, err)
return
} else if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
pvLatest, err := getLatestVersion(ctx, ctx.PathParam("name"))
if errors.Is(err, packages_model.ErrPackageNotExist) {
apiError(ctx, http.StatusNotFound, err)
return
} else if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if pvLatest.ID == pv.ID {
apiError(ctx, http.StatusForbidden, errors.New("cannot delete the latest version"))
return
}
err = packages_service.DeletePackageVersionAndReferences(ctx, pv)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.Status(http.StatusNoContent)
}
// DeleteState deletes the specific file of a terraform package.
// Fails if the state is locked
func DeleteState(ctx *context.Context) {
packageName := ctx.PathParam("name")
lockKey := getLockKey(ctx)
release, err := globallock.Lock(ctx, lockKey)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer release()
p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, packageName)
if err != nil {
if errors.Is(err, packages_model.ErrPackageNotExist) {
apiError(ctx, http.StatusNotFound, err)
return
}
apiError(ctx, http.StatusInternalServerError, err)
return
}
lock, err := terraform_module.GetLock(ctx, p.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if lock.IsLocked() {
apiError(ctx, http.StatusLocked, errors.New("terraform state is locked"))
return
}
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
PackageID: p.ID,
IsInternal: optional.None[bool](),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
err = packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypePackage, p.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
for _, pv := range pvs {
if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
}
if err := packages_model.DeletePackageByID(ctx, p.ID); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.Status(http.StatusOK)
}
// LockState locks the specific terraform state.
// Internally, it adds a property to the package with the lock information
// Caveat being that it allocates a package if one doesn't exist to attach the property
func LockState(ctx *context.Context) {
packageName := ctx.PathParam("name")
if !isValidPackageName(packageName) {
apiError(ctx, http.StatusBadRequest, errors.New("invalid package name"))
return
}
var reqLockInfo *terraform_module.LockInfo
reqLockInfo, err := terraform_module.ParseLockInfo(ctx.Req.Body)
if err != nil {
apiError(ctx, http.StatusBadRequest, err)
return
}
lockKey := getLockKey(ctx)
release, err := globallock.Lock(ctx, lockKey)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer release()
p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, packageName)
if err != nil {
// If the package doesn't exist, allocate it for the lock.
if errors.Is(err, packages_model.ErrPackageNotExist) {
p = &packages_model.Package{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeTerraformState,
Name: packageName,
LowerName: strings.ToLower(packageName),
}
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
} else {
apiError(ctx, http.StatusInternalServerError, err)
return
}
}
currentLock, err := terraform_module.GetLock(ctx, p.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if currentLock.IsLocked() {
ctx.JSON(http.StatusLocked, currentLock)
return
}
err = terraform_module.SetLock(ctx, p.ID, reqLockInfo)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.Status(http.StatusOK)
}
// UnlockState unlock the specific terraform state.
// Internally, it clears the package property
func UnlockState(ctx *context.Context) {
packageName := ctx.PathParam("name")
if !isValidPackageName(packageName) {
apiError(ctx, http.StatusBadRequest, errors.New("invalid package name"))
return
}
reqLockInfo, err := terraform_module.ParseLockInfo(ctx.Req.Body)
if err != nil {
apiError(ctx, http.StatusBadRequest, err)
return
}
lockKey := getLockKey(ctx)
release, err := globallock.Lock(ctx, lockKey)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer release()
p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, packageName)
if err != nil {
if errors.Is(err, packages_model.ErrPackageNotExist) {
ctx.Status(http.StatusOK)
return
}
apiError(ctx, http.StatusInternalServerError, err)
return
}
existingLock, err := terraform_module.GetLock(ctx, p.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
// we can bypass messing with the lock since it's empty
if !existingLock.IsLocked() {
ctx.Status(http.StatusOK)
return
}
// Unlocking ID must be the same as locker one.
if existingLock.ID != reqLockInfo.ID {
apiError(ctx, http.StatusLocked, errors.New("lock ID mismatch"))
return
}
// We can clear the state if lock id matches
err = terraform_module.RemoveLock(ctx, p.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.Status(http.StatusOK)
}
func getLatestVersion(ctx *context.Context, packageName string) (*packages_model.PackageVersion, error) {
pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeTerraformState,
Name: packages_model.SearchValue{ExactMatch: true, Value: packageName},
IsInternal: optional.Some(false),
Sort: packages_model.SortCreatedDesc,
})
if err != nil {
return nil, err
}
if len(pvs) == 0 {
return nil, packages_model.ErrPackageNotExist
}
return pvs[0], nil
}
func getLockKey(ctx *context.Context) string {
return fmt.Sprintf("terraform_lock_%d_%s", ctx.Package.Owner.ID, strings.ToLower(ctx.PathParam("name")))
}