Skip to content
Merged
Show file tree
Hide file tree
Changes from 61 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
2f8577e
Add terraform state packages
elbandi Apr 30, 2025
1d7eaef
Add terraform state lock
elbandi Apr 30, 2025
321d999
Fix lint
elbandi Apr 30, 2025
0926611
Regenerate gitea-terraform svg
elbandi May 1, 2025
c4e566b
Better package setup details
elbandi May 1, 2025
4272ae5
lintfix no2
elbandi May 1, 2025
e6f33be
fix indent
elbandi May 1, 2025
e2b1476
Merge branch 'main' into terraform-package
TheFox0x7 Feb 22, 2026
ea9b931
port locale addition
TheFox0x7 Feb 22, 2026
5fc205f
replace function calls
TheFox0x7 Feb 22, 2026
f2a4971
mirror gitlab routes, use serial as verison system
TheFox0x7 Feb 22, 2026
b7e51ee
update routes
TheFox0x7 Feb 22, 2026
d662d4b
fix test a bit
TheFox0x7 Feb 22, 2026
2d51d06
remove direct serve test
TheFox0x7 Feb 22, 2026
1273929
add database held locks
TheFox0x7 Feb 25, 2026
2ef423c
fix locking not returning the held lock
TheFox0x7 Feb 25, 2026
98eb8c0
Merge branch 'main' into terraform-package
TheFox0x7 Feb 26, 2026
86d0438
extract consts
TheFox0x7 Feb 27, 2026
f985133
remove unlocker since state is stored in database now
TheFox0x7 Feb 27, 2026
d67b55e
extract common helpers
TheFox0x7 Feb 27, 2026
cb80dde
fix svg
TheFox0x7 Feb 27, 2026
c0c6d5b
rename const
TheFox0x7 Feb 28, 2026
d29cc85
rephrase the setup section
TheFox0x7 Feb 28, 2026
1a6313b
Merge branch 'main' into terraform-package
TheFox0x7 Mar 3, 2026
1360e3d
Merge branch 'main' into terraform-package
TheFox0x7 Mar 8, 2026
98f0118
add lock display in UI
TheFox0x7 Mar 8, 2026
ce4bf24
fix icon
TheFox0x7 Mar 8, 2026
21da29a
prevent deletion of locked and latest packages in UI
TheFox0x7 Mar 8, 2026
c455ccd
fix nilnil
TheFox0x7 Mar 8, 2026
799d7c0
factor out common parts to a module
TheFox0x7 Mar 9, 2026
16dca47
fix lint
TheFox0x7 Mar 9, 2026
adb2cf2
add copyright header
TheFox0x7 Mar 9, 2026
21ec4c9
update copyright date
TheFox0x7 Mar 15, 2026
6504ed3
fix duplicated logic
TheFox0x7 Mar 15, 2026
b76bbc2
add lock on api upload
TheFox0x7 Mar 15, 2026
e93b1de
fix error comparison
TheFox0x7 Mar 15, 2026
c8318f5
update svg
TheFox0x7 Mar 15, 2026
f80116f
group routes and enforce read write on locks
TheFox0x7 Mar 15, 2026
fb3f13f
fix route
TheFox0x7 Mar 15, 2026
b4e98ea
remove if in template
TheFox0x7 Mar 18, 2026
3ab2acf
parse the state and lock file
TheFox0x7 Mar 22, 2026
9d3f2e5
address remaining points of review
TheFox0x7 Mar 22, 2026
00c2720
refactor set and remove state to ignore nonempty/empty state
TheFox0x7 Mar 22, 2026
e5b5a4e
fix formatting
TheFox0x7 Mar 22, 2026
a1c9ddc
Update options/locale/locale_en-US.json
techknowlogick Mar 24, 2026
aa509e4
Merge branch 'main' into terraform-package
TheFox0x7 Mar 28, 2026
cf3c6e6
Merge branch 'main' into terraform-package
TheFox0x7 Apr 5, 2026
e9f0c35
forbid from removing the state from UI when its locked
TheFox0x7 Apr 5, 2026
b84389a
drop origin-url
TheFox0x7 Apr 6, 2026
5972ab5
add service helpers for state
TheFox0x7 Apr 6, 2026
0d6c9a6
add Value to gitea's json module
TheFox0x7 Apr 6, 2026
4e8a48f
formatting
TheFox0x7 Apr 6, 2026
0e7394f
missed import
TheFox0x7 Apr 6, 2026
1412290
json value
wxiaoguang Apr 6, 2026
907dc68
fix lint
wxiaoguang Apr 6, 2026
2bbc654
refactor
wxiaoguang Apr 6, 2026
8e79712
refactor view package version
wxiaoguang Apr 6, 2026
f995892
fix err msg
wxiaoguang Apr 6, 2026
700781c
fix lint
wxiaoguang Apr 6, 2026
f8adfaf
fix UI regression where lock could not have been added on the package
TheFox0x7 Apr 6, 2026
67ab2a2
use flex-text-block and drop margins
TheFox0x7 Apr 6, 2026
f17d92c
update example ini with state limit
TheFox0x7 Apr 6, 2026
a073c72
remove unused global error var
wxiaoguang Apr 6, 2026
9797754
Merge branch 'main' into terraform-package
wxiaoguang Apr 6, 2026
7bd9134
drop full validation of state file as it was interrupting the encrypt…
TheFox0x7 Apr 6, 2026
e45dcbb
someday I'll setup a pre-push hook to stop me from pushing before for…
TheFox0x7 Apr 6, 2026
34d8676
expand comment
TheFox0x7 Apr 6, 2026
134637b
return
TheFox0x7 Apr 6, 2026
ef630e7
Merge branch 'main' into terraform-package
GiteaBot Apr 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions models/packages/descriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ func GetPackageDescriptorWithCache(ctx context.Context, pv *PackageVersion, c *c
metadata = &rubygems.Metadata{}
case TypeSwift:
metadata = &swift.Metadata{}
case TypeTerraformState:
// terraform packages have no metadata
case TypeVagrant:
metadata = &vagrant.Metadata{}
default:
Expand Down
50 changes: 28 additions & 22 deletions models/packages/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,29 @@ type Type string

// List of supported packages
const (
TypeAlpine Type = "alpine"
TypeArch Type = "arch"
TypeCargo Type = "cargo"
TypeChef Type = "chef"
TypeComposer Type = "composer"
TypeConan Type = "conan"
TypeConda Type = "conda"
TypeContainer Type = "container"
TypeCran Type = "cran"
TypeDebian Type = "debian"
TypeGeneric Type = "generic"
TypeGo Type = "go"
TypeHelm Type = "helm"
TypeMaven Type = "maven"
TypeNpm Type = "npm"
TypeNuGet Type = "nuget"
TypePub Type = "pub"
TypePyPI Type = "pypi"
TypeRpm Type = "rpm"
TypeRubyGems Type = "rubygems"
TypeSwift Type = "swift"
TypeVagrant Type = "vagrant"
TypeAlpine Type = "alpine"
TypeArch Type = "arch"
TypeCargo Type = "cargo"
TypeChef Type = "chef"
TypeComposer Type = "composer"
TypeConan Type = "conan"
TypeConda Type = "conda"
TypeContainer Type = "container"
TypeCran Type = "cran"
TypeDebian Type = "debian"
TypeGeneric Type = "generic"
TypeGo Type = "go"
TypeHelm Type = "helm"
TypeMaven Type = "maven"
TypeNpm Type = "npm"
TypeNuGet Type = "nuget"
TypePub Type = "pub"
TypePyPI Type = "pypi"
TypeRpm Type = "rpm"
TypeRubyGems Type = "rubygems"
TypeSwift Type = "swift"
TypeTerraformState Type = "terraform"
TypeVagrant Type = "vagrant"
)

var TypeList = []Type{
Expand All @@ -76,6 +77,7 @@ var TypeList = []Type{
TypeRpm,
TypeRubyGems,
TypeSwift,
TypeTerraformState,
TypeVagrant,
}

Expand Down Expand Up @@ -124,6 +126,8 @@ func (pt Type) Name() string {
return "RubyGems"
case TypeSwift:
return "Swift"
case TypeTerraformState:
return "Terraform State"
case TypeVagrant:
return "Vagrant"
}
Expand Down Expand Up @@ -175,6 +179,8 @@ func (pt Type) SVGName() string {
return "gitea-rubygems"
case TypeSwift:
return "gitea-swift"
case TypeTerraformState:
return "gitea-terraform"
case TypeVagrant:
return "gitea-vagrant"
}
Expand Down
3 changes: 3 additions & 0 deletions modules/json/jsonlegacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package json

import (
"encoding/json"
"io"
)

Expand All @@ -20,3 +21,5 @@ func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
func NewDecoderCaseInsensitive(reader io.Reader) Decoder {
return DefaultJSONHandler.NewDecoder(reader)
}

type Value = json.RawMessage
3 changes: 3 additions & 0 deletions modules/json/jsonv2.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package json
import (
"bytes"
jsonv1 "encoding/json" //nolint:depguard // this package wraps it
"encoding/json/jsontext" //nolint:depguard // this package wraps it
jsonv2 "encoding/json/v2" //nolint:depguard // this package wraps it
"io"
)
Expand Down Expand Up @@ -90,3 +91,5 @@ func (d *jsonV2Decoder) Decode(v any) error {
func NewDecoderCaseInsensitive(reader io.Reader) Decoder {
return &jsonV2Decoder{reader: reader, opts: jsonV2.unmarshalCaseInsensitiveOptions}
}

type Value = jsontext.Value
102 changes: 102 additions & 0 deletions modules/packages/terraform/lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package terraform

import (
"context"
"errors"
"io"
"time"

"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/util"

"xorm.io/builder"
)

const LockFile = "terraform.lock"

var ErrMissingLockID = util.NewInvalidArgumentErrorf("terraform lock is missing an ID")

// LockInfo is the metadata for a terraform lock.
type LockInfo struct {
ID string `json:"ID"`
Operation string `json:"Operation"`
Info string `json:"Info"`
Who string `json:"Who"`
Version string `json:"Version"`
Created time.Time `json:"Created"`
Path string `json:"Path"`
}

func (l *LockInfo) IsLocked() bool {
return l.ID != ""
}

func ParseLockInfo(r io.Reader) (*LockInfo, error) {
var lock LockInfo
err := json.NewDecoder(r).Decode(&lock)
if err != nil {
return nil, err
}
// ID is required. Rest is less important.
if lock.ID == "" {
return nil, ErrMissingLockID
}
return &lock, nil
}

// GetLock returns the terraform lock for the given package.
// Lock is empty if no lock exists.
func GetLock(ctx context.Context, packageID int64) (LockInfo, error) {
var lock LockInfo
locks, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypePackage, packageID, LockFile)
if err != nil {
return lock, err
}
if len(locks) == 0 || locks[0].Value == "" {
return lock, nil
}

err = json.Unmarshal([]byte(locks[0].Value), &lock)
return lock, err
}

// SetLock sets the terraform lock for the given package.
func SetLock(ctx context.Context, packageID int64, lock *LockInfo) error {
jsonBytes, err := json.Marshal(lock)
if err != nil {
return err
}

return updateLock(ctx, packageID, string(jsonBytes), builder.Eq{"value": ""})
}

// RemoveLock removes the terraform lock for the given package.
func RemoveLock(ctx context.Context, packageID int64) error {
return updateLock(ctx, packageID, "", builder.Neq{"value": ""})
}

func updateLock(ctx context.Context, refID int64, value string, cond builder.Cond) error {
pp := packages_model.PackageProperty{RefType: packages_model.PropertyTypePackage, RefID: refID, Name: LockFile}
ok, err := db.GetEngine(ctx).Get(&pp)
if err != nil {
return err
}
if ok {
n, err := db.GetEngine(ctx).Where("ref_type=? AND ref_id=? AND name=?", packages_model.PropertyTypePackage, refID, LockFile).And(cond).Cols("value").Update(&packages_model.PackageProperty{Value: value})
if err != nil {
return err
}
if n == 0 {
return errors.New("failed to update lock state")
}

return nil
}
_, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, refID, LockFile, value)
return err
}
59 changes: 59 additions & 0 deletions modules/packages/terraform/state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package terraform

import (
"io"

"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/util"
)

var (
ErrNoSerial = util.NewInvalidArgumentErrorf("state serial is missing")
ErrNoLineage = util.NewInvalidArgumentErrorf("state lineage is missing")
ErrNoTerraformVersion = util.NewInvalidArgumentErrorf("state terraform version is missing")
ErrNoResources = util.NewInvalidArgumentErrorf("state resources are missing")
ErrNoOutputs = util.NewInvalidArgumentErrorf("state outputs are missing")
)

type State struct {
Version int `json:"version"`
TerraformVersion string `json:"terraform_version"`
Serial uint64 `json:"serial"`
Lineage string `json:"lineage"`
Resources json.Value `json:"resources"`
Outputs json.Value `json:"outputs"`
}

// ParseState parses the Terraform state file
func ParseState(r io.Reader) (*State, error) {
var state State
err := json.NewDecoder(r).Decode(&state)
if err != nil {
return nil, err
}
// Serial starts at 1; 0 means it wasn't set in the state file
if state.Serial == 0 {
return nil, ErrNoSerial
}
if state.Version != 4 {
return nil, util.NewInvalidArgumentErrorf("state version %d is not supported", state.Version)
}
if state.TerraformVersion == "" {
return nil, ErrNoTerraformVersion
}
// Lineage should always be set
if state.Lineage == "" {
return nil, ErrNoLineage
}
if state.Resources == nil {
return nil, ErrNoResources
}
if state.Outputs == nil {
return nil, ErrNoOutputs
}

return &state, nil
}
2 changes: 2 additions & 0 deletions modules/setting/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var (
LimitSizeRpm int64
LimitSizeRubyGems int64
LimitSizeSwift int64
LimitSizeTerraform int64
LimitSizeVagrant int64

DefaultRPMSignEnabled bool
Expand Down Expand Up @@ -86,6 +87,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
Packages.LimitSizeRpm = mustBytes(sec, "LIMIT_SIZE_RPM")
Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS")
Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT")
Packages.LimitSizeTerraform = mustBytes(sec, "LIMIT_SIZE_TERRAFORM")
Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT")
Packages.DefaultRPMSignEnabled = sec.Key("DEFAULT_RPM_SIGN_ENABLED").MustBool(false)
return nil
Expand Down
12 changes: 12 additions & 0 deletions options/locale/locale_en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -3602,6 +3602,18 @@
"packages.swift.registry": "Set up this registry from the command line:",
"packages.swift.install": "Add the package in your <code>Package.swift</code> file:",
"packages.swift.install2": "and run the following command:",
"packages.terraform.install": "Set your state to use the HTTP backend",
"packages.terraform.install2": "and run the following command:",
"packages.terraform.lock_status": "Lock Status",
"packages.terraform.locked_by": "Locked by %s",
"packages.terraform.unlocked": "Unlocked",
"packages.terraform.lock": "Lock",
"packages.terraform.unlock": "Unlock",
"packages.terraform.lock.success": "Terraform state was successfully locked.",
"packages.terraform.unlock.success": "Terraform state was successfully unlocked.",
"packages.terraform.lock.error.already_locked": "Terraform state is already locked.",
"packages.terraform.delete.locked": "Terraform state is locked and cannot be deleted.",
"packages.terraform.delete.latest": "The latest version of a Terraform state cannot be deleted.",
"packages.vagrant.install": "To add a Vagrant box, run the following command:",
"packages.settings.link": "Link this package to a repository",
"packages.settings.link.description": "If you link a package with a repository, the package will appear in the repository's package list. Only repositories under the same owner can be linked. Leaving the field empty will remove the link.",
Expand Down
1 change: 1 addition & 0 deletions public/assets/img/svg/gitea-terraform.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions routers/api/packages/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"code.gitea.io/gitea/routers/api/packages/rpm"
"code.gitea.io/gitea/routers/api/packages/rubygems"
"code.gitea.io/gitea/routers/api/packages/swift"
"code.gitea.io/gitea/routers/api/packages/terraform"
"code.gitea.io/gitea/routers/api/packages/vagrant"
"code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context"
Expand Down Expand Up @@ -514,6 +515,21 @@ func CommonRoutes() *web.Router {
r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers)
}, reqPackageAccess(perm.AccessModeRead))
})
// See https://docs.gitlab.com/ci/jobs/fine_grained_permissions/#terraform-state-endpoints
// For endpoint and permission reference
r.Group("/terraform/state/{name}", func() {
r.Get("", terraform.GetTerraformState)
r.Get("/versions/{serial}", terraform.GetTerraformStateBySerial)
r.Group("", func() {
r.Post("", terraform.UploadState)
r.Delete("", terraform.DeleteState)
r.Delete("/versions/{serial}", terraform.DeleteStateBySerial)
}, reqPackageAccess(perm.AccessModeWrite))
r.Group("/lock", func() {
r.Post("", terraform.LockState)
r.Delete("", terraform.UnlockState)
}, reqPackageAccess(perm.AccessModeWrite))
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/vagrant", func() {
r.Group("/authenticate", func() {
r.Get("", vagrant.CheckAuthenticate)
Expand Down
Loading
Loading