diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index 1ea181c72320b..edd8ff2a072ba 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -203,6 +203,8 @@ func getPackageDescriptor(ctx context.Context, pv *PackageVersion, c *cache.Ephe metadata = &rubygems.Metadata{} case TypeSwift: metadata = &swift.Metadata{} + case TypeTerraform: + // terraform packages have no metadata case TypeVagrant: metadata = &vagrant.Metadata{} default: diff --git a/models/packages/package.go b/models/packages/package.go index 38d1cdcf66030..ae182d715ec48 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -51,6 +51,7 @@ const ( TypeRpm Type = "rpm" TypeRubyGems Type = "rubygems" TypeSwift Type = "swift" + TypeTerraform Type = "terraform" TypeVagrant Type = "vagrant" ) @@ -76,6 +77,7 @@ var TypeList = []Type{ TypeRpm, TypeRubyGems, TypeSwift, + TypeTerraform, TypeVagrant, } @@ -124,6 +126,8 @@ func (pt Type) Name() string { return "RubyGems" case TypeSwift: return "Swift" + case TypeTerraform: + return "Terraform" case TypeVagrant: return "Vagrant" } @@ -175,6 +179,8 @@ func (pt Type) SVGName() string { return "gitea-rubygems" case TypeSwift: return "gitea-swift" + case TypeTerraform: + return "gitea-terraform" case TypeVagrant: return "gitea-vagrant" } diff --git a/modules/globallock/globallock.go b/modules/globallock/globallock.go index 24e91881bb338..470d5f88f0342 100644 --- a/modules/globallock/globallock.go +++ b/modules/globallock/globallock.go @@ -45,6 +45,10 @@ func TryLock(ctx context.Context, key string) (bool, ReleaseFunc, error) { return DefaultLocker().TryLock(ctx, key) } +func Unlock(ctx context.Context, key string) error { + return DefaultLocker().Unlock(ctx, key) +} + // LockAndDo tries to acquire a lock for the given key and then calls the given function. // It uses the default locker, and it will return an error if failed to acquire the lock. func LockAndDo(ctx context.Context, key string, f func(context.Context) error) error { diff --git a/modules/globallock/locker.go b/modules/globallock/locker.go index 682e24d052aba..2d399eec886ef 100644 --- a/modules/globallock/locker.go +++ b/modules/globallock/locker.go @@ -32,6 +32,8 @@ type Locker interface { // And if it fails to acquire the lock because it's already locked, not other reasons like redis is down, // it will return false without any error. TryLock(ctx context.Context, key string) (bool, ReleaseFunc, error) + + Unlock(ctx context.Context, key string) error } // ReleaseFunc is a function that releases a lock. diff --git a/modules/globallock/memory_locker.go b/modules/globallock/memory_locker.go index 3f818d8d43929..086986be6e00b 100644 --- a/modules/globallock/memory_locker.go +++ b/modules/globallock/memory_locker.go @@ -61,6 +61,11 @@ func (l *memoryLocker) TryLock(_ context.Context, key string) (bool, ReleaseFunc return false, func() {}, nil } +func (l *memoryLocker) Unlock(_ context.Context, key string) error { + l.locks.Delete(key) + return nil +} + func (l *memoryLocker) tryLock(key string) bool { _, loaded := l.locks.LoadOrStore(key, struct{}{}) return !loaded diff --git a/modules/globallock/redis_locker.go b/modules/globallock/redis_locker.go index 45dc769fd499b..37b7409a78ed6 100644 --- a/modules/globallock/redis_locker.go +++ b/modules/globallock/redis_locker.go @@ -64,6 +64,19 @@ func (l *redisLocker) TryLock(ctx context.Context, key string) (bool, ReleaseFun return err == nil, f, err } +func (l *redisLocker) Unlock(ctx context.Context, key string) error { + mutex, ok := l.mutexM.Load(key) + if ok { + l.mutexM.Delete(key) + + // It's safe to ignore the error here, + // if it failed to unlock, it will be released automatically after the lock expires. + // Do not call mutex.UnlockContext(ctx) here, or it will fail to release when ctx has timed out. + _, _ = mutex.(*redsync.Mutex).Unlock() + } + return nil +} + // Close closes the locker. // It will stop extending the locks and refuse to acquire new locks. // In actual use, it is not necessary to call this function. diff --git a/modules/setting/packages.go b/modules/setting/packages.go index b598424064832..e873aa3001caa 100644 --- a/modules/setting/packages.go +++ b/modules/setting/packages.go @@ -39,6 +39,7 @@ var ( LimitSizeRpm int64 LimitSizeRubyGems int64 LimitSizeSwift int64 + LimitSizeTerraform int64 LimitSizeVagrant int64 DefaultRPMSignEnabled bool @@ -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 diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index a8fabc9ca1014..2acac795e11d8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3673,6 +3673,8 @@ rubygems.required.rubygems = Requires RubyGem version swift.registry = Setup this registry from the command line: swift.install = Add the package in your Package.swift file: swift.install2 = and run the following command: +terraform.install = Setup this registry to your backend config +terraform.install2 = and run the following command: vagrant.install = To add a Vagrant box, run the following command: settings.link = Link this package to a repository settings.link.description = If you link a package with a repository, the package is listed in the repository's package list. diff --git a/public/assets/img/svg/gitea-terraform.svg b/public/assets/img/svg/gitea-terraform.svg new file mode 100644 index 0000000000000..82db40da8671e --- /dev/null +++ b/public/assets/img/svg/gitea-terraform.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index ae4ea7ea87afc..f18794f26687d 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -34,6 +34,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" @@ -662,6 +663,22 @@ func CommonRoutes() *web.Router { r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers) }, reqPackageAccess(perm.AccessModeRead)) }) + r.Group("/terraform", func() { + r.Group("/{packagename}", func() { + r.Delete("", reqPackageAccess(perm.AccessModeWrite), terraform.DeletePackage) + r.Group("/state/{filename}", func() { + r.Get("", terraform.DownloadPackageFile) + r.Group("", func() { + r.Put("", terraform.UploadPackage) + r.Delete("", terraform.DeletePackageFile) + }, reqPackageAccess(perm.AccessModeWrite)) + r.Group("/lock", func() { + r.Post("", terraform.LockPackage) + r.Delete("", terraform.UnlockPackage) + }, reqPackageAccess(perm.AccessModeWrite)) + }) + }) + }, reqPackageAccess(perm.AccessModeRead)) r.Group("/vagrant", func() { r.Group("/authenticate", func() { r.Get("", vagrant.CheckAuthenticate) diff --git a/routers/api/packages/terraform/terraform.go b/routers/api/packages/terraform/terraform.go new file mode 100644 index 0000000000000..4d2d441ec069e --- /dev/null +++ b/routers/api/packages/terraform/terraform.go @@ -0,0 +1,253 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package terraform + +import ( + "errors" + "fmt" + "net/http" + "regexp" + "strings" + "unicode" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/globallock" + "code.gitea.io/gitea/modules/log" + packages_module "code.gitea.io/gitea/modules/packages" + "code.gitea.io/gitea/routers/api/packages/helper" + "code.gitea.io/gitea/services/context" + packages_service "code.gitea.io/gitea/services/packages" +) + +var ( + packageNameRegex = regexp.MustCompile(`\A[-_+.\w]+\z`) + filenameRegex = regexp.MustCompile(`\A[-_+=:;.()\[\]{}~!@#$%^& \w]+\z`) +) + +func apiError(ctx *context.Context, status int, obj any) { + helper.LogAndProcessError(ctx, status, obj, func(message string) { + ctx.PlainText(status, message) + }) +} + +// DownloadPackageFile serves the specific terraform package. +func DownloadPackageFile(ctx *context.Context) { + s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + ctx, + &packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeTerraform, + Name: ctx.PathParam("packagename"), + Version: ctx.PathParam("filename"), + }, + &packages_service.PackageFileInfo{ + Filename: "tfstate", + // CompositeKey: "state", + }, + ) + 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 != ".." +} + +func isValidFileName(filename string) bool { + return filenameRegex.MatchString(filename) && + strings.TrimSpace(filename) == filename && + filename != "." && filename != ".." +} + +// UploadPackage uploads the specific terraform package. +func UploadPackage(ctx *context.Context) { + packageName := ctx.PathParam("packagename") + filename := ctx.PathParam("filename") + + if !isValidPackageName(packageName) { + apiError(ctx, http.StatusBadRequest, errors.New("invalid package name")) + return + } + + if !isValidFileName(filename) { + apiError(ctx, http.StatusBadRequest, errors.New("invalid filename")) + 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() + + _, _, err = packages_service.CreatePackageOrAddFileToExisting( + ctx, + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeTerraform, + Name: packageName, + Version: filename, + }, + Creator: ctx.Doer, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: "tfstate", + }, + Creator: ctx.Doer, + Data: buf, + IsLead: true, + OverwriteExisting: true, + }, + ) + if err != nil { + switch err { + case packages_model.ErrDuplicatePackageFile: + apiError(ctx, http.StatusConflict, err) + case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: + apiError(ctx, http.StatusForbidden, err) + default: + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + ctx.Status(http.StatusCreated) +} + +// DeletePackage deletes the specific terraform package. +func DeletePackage(ctx *context.Context) { + err := packages_service.RemovePackageVersionByNameAndVersion( + ctx, + ctx.Doer, + &packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeTerraform, + Name: ctx.PathParam("packagename"), + // Version: ctx.PathParam("filename"), + }, + ) + if err != nil { + if errors.Is(err, packages_model.ErrPackageNotExist) { + apiError(ctx, http.StatusNotFound, err) + return + } + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusNoContent) +} + +// DeletePackageFile deletes the specific file of a terraform package. +func DeletePackageFile(ctx *context.Context) { + pv, pf, err := func() (*packages_model.PackageVersion, *packages_model.PackageFile, error) { + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraform, ctx.PathParam("packagename"), ctx.PathParam("filename")) + if err != nil { + return nil, nil, err + } + + pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, "tfstate", packages_model.EmptyFileKey) + if err != nil { + return nil, nil, err + } + + return pv, pf, nil + }() + 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 + } + + pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + if len(pfs) == 1 { + if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + } else { + if err := packages_service.DeletePackageFile(ctx, pf); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + } + + ctx.Status(http.StatusNoContent) +} + +// LockPackage locks the specific terraform state. +func LockPackage(ctx *context.Context) { + packageName := ctx.PathParam("packagename") + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraform, packageName, ctx.PathParam("filename")) + 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 + } + + ok, _, err := globallock.TryLock(ctx, fmt.Sprintf("%s/%s", packageName, pv.LowerVersion)) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if !ok { + apiError(ctx, http.StatusLocked, err) + return + } + + ctx.Status(http.StatusOK) +} + +// UnlockPackage unlock the specific terraform state. +func UnlockPackage(ctx *context.Context) { + packageName := ctx.PathParam("packagename") + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraform, packageName, ctx.PathParam("filename")) + 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 + } + + _ = globallock.Unlock(ctx, fmt.Sprintf("%s/%s", packageName, pv.LowerVersion)) + + ctx.Status(http.StatusOK) +} diff --git a/routers/api/packages/terraform/terraform_test.go b/routers/api/packages/terraform/terraform_test.go new file mode 100644 index 0000000000000..6e69100a28c7e --- /dev/null +++ b/routers/api/packages/terraform/terraform_test.go @@ -0,0 +1,65 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package terraform + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidatePackageName(t *testing.T) { + bad := []string{ + "", + ".", + "..", + "-", + "a?b", + "a b", + "a/b", + } + for _, name := range bad { + assert.False(t, isValidPackageName(name), "bad=%q", name) + } + + good := []string{ + "a", + "1", + "a-", + "a_b", + "c.d+", + } + for _, name := range good { + assert.True(t, isValidPackageName(name), "good=%q", name) + } +} + +func TestValidateFileName(t *testing.T) { + bad := []string{ + "", + ".", + "..", + "a?b", + "a/b", + " a", + "a ", + } + for _, name := range bad { + assert.False(t, isValidFileName(name), "bad=%q", name) + } + + good := []string{ + "-", + "a", + "1", + "a-", + "a_b", + "a b", + "c.d+", + `-_+=:;.()[]{}~!@#$%^& aA1`, + } + for _, name := range good { + assert.True(t, isValidFileName(name), "good=%q", name) + } +} diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index 41b7f2a43f67b..5928ad6c2e502 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -43,7 +43,7 @@ func ListPackages(ctx *context.APIContext) { // in: query // description: package type filter // type: string - // enum: [alpine, cargo, chef, composer, conan, conda, container, cran, debian, generic, go, helm, maven, npm, nuget, pub, pypi, rpm, rubygems, swift, vagrant] + // enum: [alpine, cargo, chef, composer, conan, conda, container, cran, debian, generic, go, helm, maven, npm, nuget, pub, pypi, rpm, rubygems, swift, terraform, vagrant] // - name: q // in: query // description: name filter diff --git a/services/forms/package_form.go b/services/forms/package_form.go index 9b6f9071647bc..d1a2b8587ccf5 100644 --- a/services/forms/package_form.go +++ b/services/forms/package_form.go @@ -15,7 +15,7 @@ import ( type PackageCleanupRuleForm struct { ID int64 Enabled bool - 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)"` + 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)"` KeepCount int `binding:"In(0,1,5,10,25,50,100)"` KeepPattern string `binding:"RegexPattern"` RemoveDays int `binding:"In(0,7,14,30,60,90,180)"` diff --git a/services/packages/packages.go b/services/packages/packages.go index bd1d460fd3ba8..0736ef4b56e2d 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -393,6 +393,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.TypeTerraform: + typeSpecificSize = setting.Packages.LimitSizeTerraform case packages_model.TypeVagrant: typeSpecificSize = setting.Packages.LimitSizeVagrant } diff --git a/templates/package/content/terraform.tmpl b/templates/package/content/terraform.tmpl new file mode 100644 index 0000000000000..afd46427a1411 --- /dev/null +++ b/templates/package/content/terraform.tmpl @@ -0,0 +1,29 @@ +{{if eq .PackageDescriptor.Package.Type "terraform"}} +

{{ctx.Locale.Tr "packages.installation"}}

+
+
+
+ +

+terraform {
+	backend "http" {
+		address = ""
+		update_method = "PUT"
+		lock_address = ""
+		unlock_address = ""
+		lock_method = "POST"
+		unlock_method = "DELETE"
+	}
+}
+				
+
+
+ +
terraform init -migrate-state
+
+
+ +
+
+
+{{end}} diff --git a/templates/package/metadata/terraform.tmpl b/templates/package/metadata/terraform.tmpl new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl index e621c04b438bd..3306d8aa945eb 100644 --- a/templates/package/shared/list.tmpl +++ b/templates/package/shared/list.tmpl @@ -20,7 +20,11 @@
+ {{if eq .Package.Type "terraform"}} + {{.Package.Name}} + {{else}} {{.Package.Name}} + {{end}} {{svg .Package.Type.SVGName 16}} {{.Package.Type.Name}}
diff --git a/templates/package/shared/view.tmpl b/templates/package/shared/view.tmpl index 713e1bbfc5520..6b8bd9f817cff 100644 --- a/templates/package/shared/view.tmpl +++ b/templates/package/shared/view.tmpl @@ -32,6 +32,7 @@ {{template "package/content/rpm" .}} {{template "package/content/rubygems" .}} {{template "package/content/swift" .}} + {{template "package/content/terraform" .}} {{template "package/content/vagrant" .}}
@@ -63,6 +64,7 @@ {{template "package/metadata/rpm" .}} {{template "package/metadata/rubygems" .}} {{template "package/metadata/swift" .}} + {{template "package/metadata/terraform" .}} {{template "package/metadata/vagrant" .}} {{if not (and (eq .PackageDescriptor.Package.Type "container") .PackageDescriptor.Metadata.Manifests)}}
{{svg "octicon-database"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 8758b5c0a1505..58e214a6300f5 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3549,6 +3549,7 @@ "rpm", "rubygems", "swift", + "terraform", "vagrant" ], "type": "string", diff --git a/tests/integration/api_packages_terraform_test.go b/tests/integration/api_packages_terraform_test.go new file mode 100644 index 0000000000000..2a44570a3bc2d --- /dev/null +++ b/tests/integration/api_packages_terraform_test.go @@ -0,0 +1,247 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "bytes" + "fmt" + "io" + "net/http" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestPackageTerraform(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + packageName := "te-st_pac.kage" + filename := "fi-le_na.me" + content := []byte{1, 2, 3} + + url := fmt.Sprintf("/api/packages/%s/terraform/%s/state", user.Name, packageName) + + t.Run("Upload", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithBody(t, "PUT", url+"/"+filename, bytes.NewReader(content)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeTerraform) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) + assert.NoError(t, err) + assert.Nil(t, pd.Metadata) + assert.Equal(t, packageName, pd.Package.Name) + assert.Equal(t, filename, pd.Version.Version) + + pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) + assert.NoError(t, err) + assert.Len(t, pfs, 1) + assert.Equal(t, "tfstate", pfs[0].Name) + assert.True(t, pfs[0].IsLead) + + pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID) + assert.NoError(t, err) + assert.Equal(t, int64(len(content)), pb.Size) + + t.Run("Exists", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithBody(t, "PUT", url+"/"+filename, bytes.NewReader(content)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + }) + + t.Run("Additional", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithBody(t, "PUT", url+"/dummy.bin", bytes.NewReader(content)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + + // Check deduplication + pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) + assert.NoError(t, err) + assert.Len(t, pfs, 1) + // assert.Equal(t, pfs[0].BlobID, pfs[1].BlobID) + }) + + t.Run("InvalidParameter", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/terraform/%s/state/%s", user.Name, "invalid package name", filename), bytes.NewReader(content)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusBadRequest) + + req = NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/terraform/%s/state/%s", user.Name, packageName, "inva|id.name"), bytes.NewReader(content)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusBadRequest) + }) + }) + + t.Run("Download", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + checkDownloadCount := func(count int64) { + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeTerraform) + assert.NoError(t, err) + assert.Len(t, pvs, 2) + assert.Equal(t, count, pvs[0].DownloadCount) + } + + checkDownloadCount(0) + + req := NewRequest(t, "GET", url+"/"+filename) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, content, resp.Body.Bytes()) + + checkDownloadCount(1) + + req = NewRequest(t, "GET", url+"/dummy.bin") + MakeRequest(t, req, http.StatusOK) + + checkDownloadCount(1) + + t.Run("NotExists", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", url+"/not.found") + MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("RequireSignInView", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)() + + req = NewRequest(t, "GET", url+"/dummy.bin") + MakeRequest(t, req, http.StatusUnauthorized) + }) + + t.Run("ServeDirect", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + if setting.Packages.Storage.Type != setting.MinioStorageType && setting.Packages.Storage.Type != setting.AzureBlobStorageType { + t.Skip("Test skipped for non-Minio-storage and non-AzureBlob-storage.") + return + } + + if setting.Packages.Storage.Type == setting.MinioStorageType { + if !setting.Packages.Storage.MinioConfig.ServeDirect { + old := setting.Packages.Storage.MinioConfig.ServeDirect + defer func() { + setting.Packages.Storage.MinioConfig.ServeDirect = old + }() + + setting.Packages.Storage.MinioConfig.ServeDirect = true + } + } else if setting.Packages.Storage.Type == setting.AzureBlobStorageType { + if !setting.Packages.Storage.AzureBlobConfig.ServeDirect { + old := setting.Packages.Storage.AzureBlobConfig.ServeDirect + defer func() { + setting.Packages.Storage.AzureBlobConfig.ServeDirect = old + }() + + setting.Packages.Storage.AzureBlobConfig.ServeDirect = true + } + } + + req := NewRequest(t, "GET", url+"/"+filename) + resp := MakeRequest(t, req, http.StatusSeeOther) + + checkDownloadCount(3) + + location := resp.Header().Get("Location") + assert.NotEmpty(t, location) + + resp2, err := (&http.Client{}).Get(location) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp2.StatusCode, location) + + body, err := io.ReadAll(resp2.Body) + assert.NoError(t, err) + assert.Equal(t, content, body) + + checkDownloadCount(3) + }) + }) + + t.Run("Delete", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + t.Run("File", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "DELETE", url+"/"+filename) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "DELETE", url+"/"+filename). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "GET", url+"/"+filename) + MakeRequest(t, req, http.StatusNotFound) + + req = NewRequest(t, "DELETE", url+"/"+filename). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNotFound) + + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeTerraform) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + + t.Run("RemovesVersion", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req = NewRequest(t, "DELETE", url+"/dummy.bin"). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeTerraform) + assert.NoError(t, err) + assert.Empty(t, pvs) + }) + }) + + t.Run("Version", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithBody(t, "PUT", url+"/"+filename, bytes.NewReader(content)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequest(t, "DELETE", url+"/"+filename) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "DELETE", url+"/"+filename). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeTerraform) + assert.NoError(t, err) + assert.Empty(t, pvs) + + req = NewRequest(t, "GET", url+"/"+filename) + MakeRequest(t, req, http.StatusNotFound) + + req = NewRequest(t, "DELETE", url). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNotFound) + }) + }) +} diff --git a/web_src/svg/gitea-terraform.svg b/web_src/svg/gitea-terraform.svg new file mode 100644 index 0000000000000..24d340f0f8c87 --- /dev/null +++ b/web_src/svg/gitea-terraform.svg @@ -0,0 +1 @@ + \ No newline at end of file