Skip to content

Commit 2f8577e

Browse files
committed
Add terraform state packages
1 parent 1f52304 commit 2f8577e

File tree

16 files changed

+575
-1
lines changed

16 files changed

+575
-1
lines changed

models/packages/descriptor.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ func getPackageDescriptor(ctx context.Context, pv *PackageVersion, c *cache.Ephe
203203
metadata = &rubygems.Metadata{}
204204
case TypeSwift:
205205
metadata = &swift.Metadata{}
206+
case TypeTerraform:
207+
// terraform packages have no metadata
206208
case TypeVagrant:
207209
metadata = &vagrant.Metadata{}
208210
default:

models/packages/package.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const (
5151
TypeRpm Type = "rpm"
5252
TypeRubyGems Type = "rubygems"
5353
TypeSwift Type = "swift"
54+
TypeTerraform Type = "terraform"
5455
TypeVagrant Type = "vagrant"
5556
)
5657

@@ -76,6 +77,7 @@ var TypeList = []Type{
7677
TypeRpm,
7778
TypeRubyGems,
7879
TypeSwift,
80+
TypeTerraform,
7981
TypeVagrant,
8082
}
8183

@@ -124,6 +126,8 @@ func (pt Type) Name() string {
124126
return "RubyGems"
125127
case TypeSwift:
126128
return "Swift"
129+
case TypeTerraform:
130+
return "Terraform"
127131
case TypeVagrant:
128132
return "Vagrant"
129133
}
@@ -175,6 +179,8 @@ func (pt Type) SVGName() string {
175179
return "gitea-rubygems"
176180
case TypeSwift:
177181
return "gitea-swift"
182+
case TypeTerraform:
183+
return "gitea-terraform"
178184
case TypeVagrant:
179185
return "gitea-vagrant"
180186
}

modules/setting/packages.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ var (
3939
LimitSizeRpm int64
4040
LimitSizeRubyGems int64
4141
LimitSizeSwift int64
42+
LimitSizeTerraform int64
4243
LimitSizeVagrant int64
4344

4445
DefaultRPMSignEnabled bool
@@ -86,6 +87,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
8687
Packages.LimitSizeRpm = mustBytes(sec, "LIMIT_SIZE_RPM")
8788
Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS")
8889
Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT")
90+
Packages.LimitSizeTerraform = mustBytes(sec, "LIMIT_SIZE_TERRAFORM")
8991
Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT")
9092
Packages.DefaultRPMSignEnabled = sec.Key("DEFAULT_RPM_SIGN_ENABLED").MustBool(false)
9193
return nil

public/assets/img/svg/gitea-terraform.svg

Lines changed: 1 addition & 0 deletions
Loading

routers/api/packages/api.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"code.gitea.io/gitea/routers/api/packages/rpm"
3535
"code.gitea.io/gitea/routers/api/packages/rubygems"
3636
"code.gitea.io/gitea/routers/api/packages/swift"
37+
"code.gitea.io/gitea/routers/api/packages/terraform"
3738
"code.gitea.io/gitea/routers/api/packages/vagrant"
3839
"code.gitea.io/gitea/services/auth"
3940
"code.gitea.io/gitea/services/context"
@@ -662,6 +663,18 @@ func CommonRoutes() *web.Router {
662663
r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers)
663664
}, reqPackageAccess(perm.AccessModeRead))
664665
})
666+
r.Group("/terraform", func() {
667+
r.Group("/{packagename}", func() {
668+
r.Delete("", reqPackageAccess(perm.AccessModeWrite), terraform.DeletePackage)
669+
r.Group("/state/{filename}", func() {
670+
r.Get("", terraform.DownloadPackageFile)
671+
r.Group("", func() {
672+
r.Put("", terraform.UploadPackage)
673+
r.Delete("", terraform.DeletePackageFile)
674+
}, reqPackageAccess(perm.AccessModeWrite))
675+
})
676+
})
677+
}, reqPackageAccess(perm.AccessModeRead))
665678
r.Group("/vagrant", func() {
666679
r.Group("/authenticate", func() {
667680
r.Get("", vagrant.CheckAuthenticate)
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package terraform
5+
6+
import (
7+
"code.gitea.io/gitea/modules/globallock"
8+
"errors"
9+
"fmt"
10+
"net/http"
11+
"regexp"
12+
"strings"
13+
"unicode"
14+
15+
packages_model "code.gitea.io/gitea/models/packages"
16+
"code.gitea.io/gitea/modules/log"
17+
packages_module "code.gitea.io/gitea/modules/packages"
18+
"code.gitea.io/gitea/routers/api/packages/helper"
19+
"code.gitea.io/gitea/services/context"
20+
packages_service "code.gitea.io/gitea/services/packages"
21+
)
22+
23+
var (
24+
packageNameRegex = regexp.MustCompile(`\A[-_+.\w]+\z`)
25+
filenameRegex = regexp.MustCompile(`\A[-_+=:;.()\[\]{}~!@#$%^& \w]+\z`)
26+
lockRelease globallock.ReleaseFunc = nil
27+
)
28+
29+
func apiError(ctx *context.Context, status int, obj any) {
30+
helper.LogAndProcessError(ctx, status, obj, func(message string) {
31+
ctx.PlainText(status, message)
32+
})
33+
}
34+
35+
// DownloadPackageFile serves the specific terraform package.
36+
func DownloadPackageFile(ctx *context.Context) {
37+
s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
38+
ctx,
39+
&packages_service.PackageInfo{
40+
Owner: ctx.Package.Owner,
41+
PackageType: packages_model.TypeTerraform,
42+
Name: ctx.PathParam("packagename"),
43+
Version: ctx.PathParam("filename"),
44+
},
45+
&packages_service.PackageFileInfo{
46+
Filename: "tfstate",
47+
// CompositeKey: "state",
48+
},
49+
)
50+
if err != nil {
51+
if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) {
52+
apiError(ctx, http.StatusNotFound, err)
53+
return
54+
}
55+
apiError(ctx, http.StatusInternalServerError, err)
56+
return
57+
}
58+
59+
helper.ServePackageFile(ctx, s, u, pf)
60+
}
61+
62+
func isValidPackageName(packageName string) bool {
63+
if len(packageName) == 1 && !unicode.IsLetter(rune(packageName[0])) && !unicode.IsNumber(rune(packageName[0])) {
64+
return false
65+
}
66+
return packageNameRegex.MatchString(packageName) && packageName != ".."
67+
}
68+
69+
func isValidFileName(filename string) bool {
70+
return filenameRegex.MatchString(filename) &&
71+
strings.TrimSpace(filename) == filename &&
72+
filename != "." && filename != ".."
73+
}
74+
75+
// UploadPackage uploads the specific terraform package.
76+
func UploadPackage(ctx *context.Context) {
77+
packageName := ctx.PathParam("packagename")
78+
filename := ctx.PathParam("filename")
79+
80+
if !isValidPackageName(packageName) {
81+
apiError(ctx, http.StatusBadRequest, errors.New("invalid package name"))
82+
return
83+
}
84+
85+
if !isValidFileName(filename) {
86+
apiError(ctx, http.StatusBadRequest, errors.New("invalid filename"))
87+
return
88+
}
89+
90+
upload, needToClose, err := ctx.UploadStream()
91+
if err != nil {
92+
apiError(ctx, http.StatusInternalServerError, err)
93+
return
94+
}
95+
if needToClose {
96+
defer upload.Close()
97+
}
98+
99+
buf, err := packages_module.CreateHashedBufferFromReader(upload)
100+
if err != nil {
101+
log.Error("Error creating hashed buffer: %v", err)
102+
apiError(ctx, http.StatusInternalServerError, err)
103+
return
104+
}
105+
defer buf.Close()
106+
107+
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
108+
ctx,
109+
&packages_service.PackageCreationInfo{
110+
PackageInfo: packages_service.PackageInfo{
111+
Owner: ctx.Package.Owner,
112+
PackageType: packages_model.TypeTerraform,
113+
Name: packageName,
114+
Version: filename,
115+
},
116+
Creator: ctx.Doer,
117+
},
118+
&packages_service.PackageFileCreationInfo{
119+
PackageFileInfo: packages_service.PackageFileInfo{
120+
Filename: "tfstate",
121+
},
122+
Creator: ctx.Doer,
123+
Data: buf,
124+
IsLead: true,
125+
OverwriteExisting: true,
126+
},
127+
)
128+
if err != nil {
129+
switch err {
130+
case packages_model.ErrDuplicatePackageFile:
131+
apiError(ctx, http.StatusConflict, err)
132+
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
133+
apiError(ctx, http.StatusForbidden, err)
134+
default:
135+
apiError(ctx, http.StatusInternalServerError, err)
136+
}
137+
return
138+
}
139+
140+
ctx.Status(http.StatusCreated)
141+
}
142+
143+
// DeletePackage deletes the specific terraform package.
144+
func DeletePackage(ctx *context.Context) {
145+
err := packages_service.RemovePackageVersionByNameAndVersion(
146+
ctx,
147+
ctx.Doer,
148+
&packages_service.PackageInfo{
149+
Owner: ctx.Package.Owner,
150+
PackageType: packages_model.TypeTerraform,
151+
Name: ctx.PathParam("packagename"),
152+
// Version: ctx.PathParam("filename"),
153+
},
154+
)
155+
if err != nil {
156+
if errors.Is(err, packages_model.ErrPackageNotExist) {
157+
apiError(ctx, http.StatusNotFound, err)
158+
return
159+
}
160+
apiError(ctx, http.StatusInternalServerError, err)
161+
return
162+
}
163+
164+
ctx.Status(http.StatusNoContent)
165+
}
166+
167+
// DeletePackageFile deletes the specific file of a terraform package.
168+
func DeletePackageFile(ctx *context.Context) {
169+
pv, pf, err := func() (*packages_model.PackageVersion, *packages_model.PackageFile, error) {
170+
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraform, ctx.PathParam("packagename"), ctx.PathParam("filename"))
171+
if err != nil {
172+
return nil, nil, err
173+
}
174+
175+
pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, "tfstate", packages_model.EmptyFileKey)
176+
if err != nil {
177+
return nil, nil, err
178+
}
179+
180+
return pv, pf, nil
181+
}()
182+
if err != nil {
183+
if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) {
184+
apiError(ctx, http.StatusNotFound, err)
185+
return
186+
}
187+
apiError(ctx, http.StatusInternalServerError, err)
188+
return
189+
}
190+
191+
pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
192+
if err != nil {
193+
apiError(ctx, http.StatusInternalServerError, err)
194+
return
195+
}
196+
197+
if len(pfs) == 1 {
198+
if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
199+
apiError(ctx, http.StatusInternalServerError, err)
200+
return
201+
}
202+
} else {
203+
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
204+
apiError(ctx, http.StatusInternalServerError, err)
205+
return
206+
}
207+
}
208+
209+
ctx.Status(http.StatusNoContent)
210+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package terraform
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestValidatePackageName(t *testing.T) {
13+
bad := []string{
14+
"",
15+
".",
16+
"..",
17+
"-",
18+
"a?b",
19+
"a b",
20+
"a/b",
21+
}
22+
for _, name := range bad {
23+
assert.False(t, isValidPackageName(name), "bad=%q", name)
24+
}
25+
26+
good := []string{
27+
"a",
28+
"1",
29+
"a-",
30+
"a_b",
31+
"c.d+",
32+
}
33+
for _, name := range good {
34+
assert.True(t, isValidPackageName(name), "good=%q", name)
35+
}
36+
}
37+
38+
func TestValidateFileName(t *testing.T) {
39+
bad := []string{
40+
"",
41+
".",
42+
"..",
43+
"a?b",
44+
"a/b",
45+
" a",
46+
"a ",
47+
}
48+
for _, name := range bad {
49+
assert.False(t, isValidFileName(name), "bad=%q", name)
50+
}
51+
52+
good := []string{
53+
"-",
54+
"a",
55+
"1",
56+
"a-",
57+
"a_b",
58+
"a b",
59+
"c.d+",
60+
`-_+=:;.()[]{}~!@#$%^& aA1`,
61+
}
62+
for _, name := range good {
63+
assert.True(t, isValidFileName(name), "good=%q", name)
64+
}
65+
}

services/forms/package_form.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
type PackageCleanupRuleForm struct {
1616
ID int64
1717
Enabled bool
18-
Type string `binding:"Required;In(alpine,arch,cargo,chef,composer,conan,conda,container,cran,debian,generic,go,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,vagrant)"`
18+
Type string `binding:"Required;In(alpine,arch,cargo,chef,composer,conan,conda,container,cran,debian,generic,go,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,terraform,vagrant)"`
1919
KeepCount int `binding:"In(0,1,5,10,25,50,100)"`
2020
KeepPattern string `binding:"RegexPattern"`
2121
RemoveDays int `binding:"In(0,7,14,30,60,90,180)"`

services/packages/packages.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p
393393
typeSpecificSize = setting.Packages.LimitSizeRubyGems
394394
case packages_model.TypeSwift:
395395
typeSpecificSize = setting.Packages.LimitSizeSwift
396+
case packages_model.TypeTerraform:
397+
typeSpecificSize = setting.Packages.LimitSizeTerraform
396398
case packages_model.TypeVagrant:
397399
typeSpecificSize = setting.Packages.LimitSizeVagrant
398400
}

0 commit comments

Comments
 (0)