Add terraform state registry (#36710)
Adds terraform/opentofu state registry with locking. Implements: https://github.com/go-gitea/gitea/issues/33644. I also checked [encrypted state](https://opentofu.org/docs/language/state/encryption), it works out of the box. Docs PR: https://gitea.com/gitea/docs/pulls/357 --------- Co-authored-by: Andras Elso <elso.andras@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -32,6 +32,12 @@ var (
|
||||
ErrQuotaTotalCount = errors.New("maximum allowed package count exceeded")
|
||||
)
|
||||
|
||||
type Specialization interface {
|
||||
OnBeforeRemovePackageAll(ctx context.Context, doer *user_model.User, pkg *packages_model.Package, pds []*packages_model.PackageDescriptor) error
|
||||
OnBeforeRemovePackageVersion(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) error
|
||||
GetViewPackageVersionData(ctx context.Context, pd *packages_model.PackageDescriptor) (any, error)
|
||||
}
|
||||
|
||||
// PackageInfo describes a package
|
||||
type PackageInfo struct {
|
||||
Owner *user_model.User
|
||||
@@ -394,6 +400,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p
|
||||
typeSpecificSize = setting.Packages.LimitSizeRubyGems
|
||||
case packages_model.TypeSwift:
|
||||
typeSpecificSize = setting.Packages.LimitSizeSwift
|
||||
case packages_model.TypeTerraformState:
|
||||
typeSpecificSize = setting.Packages.LimitSizeTerraformState
|
||||
case packages_model.TypeVagrant:
|
||||
typeSpecificSize = setting.Packages.LimitSizeVagrant
|
||||
}
|
||||
@@ -473,6 +481,9 @@ func RemovePackageVersion(ctx context.Context, doer *user_model.User, pv *packag
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := GetSpecManager().Get(pd.Package.Type).OnBeforeRemovePackageVersion(ctx, doer, pd); err != nil {
|
||||
return err
|
||||
}
|
||||
// HINT: PACKAGE-DEFER-STORAGE-DELETE: Blobs are not deleted immediately, instead they are deleted by the cleanup_packages cron task.
|
||||
// If there are no more versions for the package, the same task removes that as well.
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
@@ -631,6 +642,10 @@ func RemovePackage(ctx context.Context, doer *user_model.User, p *packages_model
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := GetSpecManager().Get(p.Type).OnBeforeRemovePackageAll(ctx, doer, p, pds); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// HINT: PACKAGE-DEFER-STORAGE-DELETE: Blobs are not deleted immediately, instead they are deleted by cleanup_packages cron task.
|
||||
err = db.WithTx(ctx, func(ctx context.Context) error {
|
||||
err := packages_model.DeletePropertiesByPackageID(ctx, packages_model.PropertyTypePackage, p.ID)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pkgspec
|
||||
|
||||
import (
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
"code.gitea.io/gitea/services/packages/terraform"
|
||||
)
|
||||
|
||||
func InitManager() error {
|
||||
mgr := packages_service.GetSpecManager()
|
||||
mgr.Add(packages_model.TypeTerraformState, &terraform.Specialization{})
|
||||
// TODO: add more in the future, refactor the existing code to use this approach
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package packages
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
)
|
||||
|
||||
type nop struct{}
|
||||
|
||||
func (n *nop) GetViewPackageVersionData(ctx context.Context, pd *packages_model.PackageDescriptor) (any, error) {
|
||||
return nil, nil //nolint:nilnil // no data, no error
|
||||
}
|
||||
|
||||
func (n *nop) OnBeforeRemovePackageAll(ctx context.Context, doer *user_model.User, pkg *packages_model.Package, pds []*packages_model.PackageDescriptor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *nop) OnBeforeRemovePackageVersion(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ Specialization = (*nop)(nil)
|
||||
|
||||
type SpecManagerType struct {
|
||||
specMap map[packages_model.Type]Specialization
|
||||
}
|
||||
|
||||
func (m *SpecManagerType) Add(t packages_model.Type, spec Specialization) {
|
||||
m.specMap[t] = spec
|
||||
}
|
||||
|
||||
func (m *SpecManagerType) Get(t packages_model.Type) Specialization {
|
||||
if len(m.specMap) == 0 {
|
||||
panic("specialization not initialized")
|
||||
}
|
||||
spec := m.specMap[t]
|
||||
if spec == nil {
|
||||
return &nop{}
|
||||
}
|
||||
return spec
|
||||
}
|
||||
|
||||
var GetSpecManager = sync.OnceValue(func() *SpecManagerType {
|
||||
return &SpecManagerType{specMap: make(map[packages_model.Type]Specialization)}
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
terraform_module "code.gitea.io/gitea/modules/packages/terraform"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
type Specialization struct{}
|
||||
|
||||
var _ packages_service.Specialization = (*Specialization)(nil)
|
||||
|
||||
func (s Specialization) GetViewPackageVersionData(ctx context.Context, pd *packages_model.PackageDescriptor) (any, error) {
|
||||
var ret struct {
|
||||
IsLatestVersion bool
|
||||
TerraformLock *terraform_module.LockInfo
|
||||
}
|
||||
latestPvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
PackageID: pd.Package.ID,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
isLatest := len(latestPvs) > 0 && latestPvs[0].ID == pd.Version.ID
|
||||
ret.IsLatestVersion = isLatest
|
||||
|
||||
if isLatest {
|
||||
lockInfo, err := terraform_module.GetLock(ctx, pd.Package.ID)
|
||||
if err != nil {
|
||||
return ret, nil
|
||||
}
|
||||
if lockInfo.IsLocked() {
|
||||
ret.TerraformLock = &lockInfo
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (s Specialization) OnBeforeRemovePackageAll(ctx context.Context, doer *user_model.User, pkg *packages_model.Package, pds []*packages_model.PackageDescriptor) error {
|
||||
locked, err := IsLocked(ctx, pkg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if locked {
|
||||
return util.ErrorWrapTranslatable(
|
||||
util.ErrorWrap(util.ErrUnprocessableContent, "terraform state is locked and cannot be deleted"),
|
||||
"packages.terraform.delete.locked",
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Specialization) OnBeforeRemovePackageVersion(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) error {
|
||||
locked, err := IsLocked(ctx, pd.Package)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if locked {
|
||||
return util.ErrorWrapTranslatable(
|
||||
util.ErrorWrap(util.ErrUnprocessableContent, "terraform state is locked and cannot be deleted"),
|
||||
"packages.terraform.delete.locked",
|
||||
)
|
||||
}
|
||||
|
||||
latest, err := IsLatest(ctx, pd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if latest {
|
||||
return util.ErrorWrapTranslatable(
|
||||
util.ErrorWrap(util.ErrUnprocessableContent, "the latest version of a Terraform state cannot be deleted"),
|
||||
"packages.terraform.delete.latest",
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
terraform_module "code.gitea.io/gitea/modules/packages/terraform"
|
||||
)
|
||||
|
||||
// IsLocked is a helper function to check if the terraform state is locked
|
||||
func IsLocked(ctx context.Context, pkg *packages_model.Package) (bool, error) {
|
||||
// Non terraform state packages aren't handled here
|
||||
if pkg.Type == packages_model.TypeTerraformState {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
lock, err := terraform_module.GetLock(ctx, pkg.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return lock.IsLocked(), nil
|
||||
}
|
||||
|
||||
// IsLatest is a helper function to check if the terraform state is the latest version
|
||||
func IsLatest(ctx context.Context, pd *packages_model.PackageDescriptor) (bool, error) {
|
||||
if pd.Package.Type == packages_model.TypeTerraformState {
|
||||
return false, nil
|
||||
}
|
||||
latestPvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
PackageID: pd.Package.ID,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(latestPvs) > 0 && latestPvs[0].ID == pd.Version.ID {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
Reference in New Issue
Block a user