mirror of
https://github.com/go-gitea/gitea.git
synced 2025-07-27 15:54:36 +02:00
add terraform state package
This commit is contained in:
parent
4d399e717d
commit
351b83df5b
@ -31,6 +31,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/packages/rpm"
|
"code.gitea.io/gitea/modules/packages/rpm"
|
||||||
"code.gitea.io/gitea/modules/packages/rubygems"
|
"code.gitea.io/gitea/modules/packages/rubygems"
|
||||||
"code.gitea.io/gitea/modules/packages/swift"
|
"code.gitea.io/gitea/modules/packages/swift"
|
||||||
|
"code.gitea.io/gitea/modules/packages/terraform"
|
||||||
"code.gitea.io/gitea/modules/packages/vagrant"
|
"code.gitea.io/gitea/modules/packages/vagrant"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
@ -191,6 +192,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
|
|||||||
metadata = &rubygems.Metadata{}
|
metadata = &rubygems.Metadata{}
|
||||||
case TypeSwift:
|
case TypeSwift:
|
||||||
metadata = &swift.Metadata{}
|
metadata = &swift.Metadata{}
|
||||||
|
case TypeTerraform:
|
||||||
|
metadata = &terraform.Metadata{}
|
||||||
case TypeVagrant:
|
case TypeVagrant:
|
||||||
metadata = &vagrant.Metadata{}
|
metadata = &vagrant.Metadata{}
|
||||||
default:
|
default:
|
||||||
|
@ -51,6 +51,7 @@ const (
|
|||||||
TypeRpm Type = "rpm"
|
TypeRpm Type = "rpm"
|
||||||
TypeRubyGems Type = "rubygems"
|
TypeRubyGems Type = "rubygems"
|
||||||
TypeSwift Type = "swift"
|
TypeSwift Type = "swift"
|
||||||
|
TypeTerraform Type = "terraform"
|
||||||
TypeVagrant Type = "vagrant"
|
TypeVagrant Type = "vagrant"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -76,6 +77,7 @@ var TypeList = []Type{
|
|||||||
TypeRpm,
|
TypeRpm,
|
||||||
TypeRubyGems,
|
TypeRubyGems,
|
||||||
TypeSwift,
|
TypeSwift,
|
||||||
|
TypeTerraform,
|
||||||
TypeVagrant,
|
TypeVagrant,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,6 +177,8 @@ func (pt Type) SVGName() string {
|
|||||||
return "gitea-rubygems"
|
return "gitea-rubygems"
|
||||||
case TypeSwift:
|
case TypeSwift:
|
||||||
return "gitea-swift"
|
return "gitea-swift"
|
||||||
|
case TypeTerraform:
|
||||||
|
return "gitea-terraform"
|
||||||
case TypeVagrant:
|
case TypeVagrant:
|
||||||
return "gitea-vagrant"
|
return "gitea-vagrant"
|
||||||
}
|
}
|
||||||
|
88
modules/packages/terraform/metadata.go
Normal file
88
modules/packages/terraform/metadata.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PropertyTerraformState = "terraform.state"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Metadata represents the Terraform backend metadata
|
||||||
|
// Updated to align with TerraformState structure
|
||||||
|
// Includes additional metadata fields like Description, Author, and URLs
|
||||||
|
type Metadata struct {
|
||||||
|
Version int `json:"version"`
|
||||||
|
TerraformVersion string `json:"terraform_version,omitempty"`
|
||||||
|
Serial uint64 `json:"serial"`
|
||||||
|
Lineage string `json:"lineage"`
|
||||||
|
Outputs map[string]any `json:"outputs,omitempty"`
|
||||||
|
Resources []ResourceState `json:"resources,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Author string `json:"author,omitempty"`
|
||||||
|
ProjectURL string `json:"project_url,omitempty"`
|
||||||
|
RepositoryURL string `json:"repository_url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceState represents the state of a resource
|
||||||
|
type ResourceState struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Instances []InstanceState `json:"instances"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstanceState represents the state of a resource instance
|
||||||
|
type InstanceState struct {
|
||||||
|
SchemaVersion int `json:"schema_version"`
|
||||||
|
Attributes map[string]any `json:"attributes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseMetadataFromState retrieves metadata from the archive with Terraform state
|
||||||
|
func ParseMetadataFromState(r io.Reader) (*Metadata, error) {
|
||||||
|
gzr, err := gzip.NewReader(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer gzr.Close()
|
||||||
|
|
||||||
|
tr := tar.NewReader(gzr)
|
||||||
|
for {
|
||||||
|
hd, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hd.Typeflag != tar.TypeReg {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looking for the state.json file
|
||||||
|
if hd.Name == "state.json" {
|
||||||
|
return ParseStateFile(tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("state.json not found in archive")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseStateFile parses the state.json file and returns Terraform metadata
|
||||||
|
func ParseStateFile(r io.Reader) (*Metadata, error) {
|
||||||
|
var stateData Metadata
|
||||||
|
if err := json.NewDecoder(r).Decode(&stateData); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &stateData, nil
|
||||||
|
}
|
161
modules/packages/terraform/metadata_test.go
Normal file
161
modules/packages/terraform/metadata_test.go
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestParseMetadataFromState tests the ParseMetadataFromState function
|
||||||
|
func TestParseMetadataFromState(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []byte
|
||||||
|
expectedError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid state file",
|
||||||
|
input: createValidStateArchive(),
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing state.json file",
|
||||||
|
input: createInvalidStateArchive(),
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "corrupt archive",
|
||||||
|
input: []byte("invalid archive data"),
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := bytes.NewReader(tt.input)
|
||||||
|
metadata, err := ParseMetadataFromState(r)
|
||||||
|
|
||||||
|
if tt.expectedError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, metadata)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, metadata)
|
||||||
|
// Optionally, check if certain fields are populated correctly
|
||||||
|
assert.NotEmpty(t, metadata.Lineage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createValidStateArchive creates a valid TAR.GZ archive with a sample state.json
|
||||||
|
func createValidStateArchive() []byte {
|
||||||
|
metadata := `{
|
||||||
|
"version": 4,
|
||||||
|
"terraform_version": "1.2.0",
|
||||||
|
"serial": 1,
|
||||||
|
"lineage": "abc123",
|
||||||
|
"resources": [],
|
||||||
|
"description": "Test project",
|
||||||
|
"author": "Test Author",
|
||||||
|
"project_url": "http://example.com",
|
||||||
|
"repository_url": "http://repo.com"
|
||||||
|
}`
|
||||||
|
|
||||||
|
// Create a gzip writer and tar writer
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
gz := gzip.NewWriter(buf)
|
||||||
|
tw := tar.NewWriter(gz)
|
||||||
|
|
||||||
|
// Add the state.json file to the tar
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: "state.json",
|
||||||
|
Size: int64(len(metadata)),
|
||||||
|
Mode: 0o600,
|
||||||
|
}
|
||||||
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if _, err := tw.Write([]byte(metadata)); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the writers
|
||||||
|
if err := tw.Close(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := gz.Close(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// createInvalidStateArchive creates an invalid TAR.GZ archive (missing state.json)
|
||||||
|
func createInvalidStateArchive() []byte {
|
||||||
|
// Create a tar archive without the state.json file
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
gz := gzip.NewWriter(buf)
|
||||||
|
tw := tar.NewWriter(gz)
|
||||||
|
|
||||||
|
// Add an empty file to the tar (but not state.json)
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: "other_file.txt",
|
||||||
|
Size: 0,
|
||||||
|
Mode: 0o600,
|
||||||
|
}
|
||||||
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the writers
|
||||||
|
if err := tw.Close(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := gz.Close(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParseStateFile tests the ParseStateFile function directly
|
||||||
|
func TestParseStateFile(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expectedError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid state.json",
|
||||||
|
input: `{"version":4,"terraform_version":"1.2.0","serial":1,"lineage":"abc123"}`,
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid JSON",
|
||||||
|
input: `{"version":4,"terraform_version"}`,
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := bytes.NewReader([]byte(tt.input))
|
||||||
|
metadata, err := ParseStateFile(r)
|
||||||
|
|
||||||
|
if tt.expectedError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, metadata)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, metadata)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -42,6 +42,7 @@ var (
|
|||||||
LimitSizeRpm int64
|
LimitSizeRpm int64
|
||||||
LimitSizeRubyGems int64
|
LimitSizeRubyGems int64
|
||||||
LimitSizeSwift int64
|
LimitSizeSwift int64
|
||||||
|
LimitSizeTerraform int64
|
||||||
LimitSizeVagrant int64
|
LimitSizeVagrant int64
|
||||||
|
|
||||||
DefaultRPMSignEnabled bool
|
DefaultRPMSignEnabled bool
|
||||||
@ -100,6 +101,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
|
|||||||
Packages.LimitSizeRpm = mustBytes(sec, "LIMIT_SIZE_RPM")
|
Packages.LimitSizeRpm = mustBytes(sec, "LIMIT_SIZE_RPM")
|
||||||
Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS")
|
Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS")
|
||||||
Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT")
|
Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT")
|
||||||
|
Packages.LimitSizeTerraform = mustBytes(sec, "LIMIT_SIZE_TERRAFORM")
|
||||||
Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT")
|
Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT")
|
||||||
Packages.DefaultRPMSignEnabled = sec.Key("DEFAULT_RPM_SIGN_ENABLED").MustBool(false)
|
Packages.DefaultRPMSignEnabled = sec.Key("DEFAULT_RPM_SIGN_ENABLED").MustBool(false)
|
||||||
return nil
|
return nil
|
||||||
|
@ -34,6 +34,7 @@ import (
|
|||||||
"code.gitea.io/gitea/routers/api/packages/rpm"
|
"code.gitea.io/gitea/routers/api/packages/rpm"
|
||||||
"code.gitea.io/gitea/routers/api/packages/rubygems"
|
"code.gitea.io/gitea/routers/api/packages/rubygems"
|
||||||
"code.gitea.io/gitea/routers/api/packages/swift"
|
"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/routers/api/packages/vagrant"
|
||||||
"code.gitea.io/gitea/services/auth"
|
"code.gitea.io/gitea/services/auth"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
@ -674,6 +675,26 @@ func CommonRoutes() *web.Router {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
|
// Define routes for Terraform HTTP backend API
|
||||||
|
r.Group("/terraform/state", func() {
|
||||||
|
// Routes for specific state identified by {statename}
|
||||||
|
r.Group("/{statename}", func() {
|
||||||
|
// Fetch the current state
|
||||||
|
r.Get("", reqPackageAccess(perm.AccessModeRead), terraform.GetState)
|
||||||
|
// Update the state (supports both POST and PUT methods)
|
||||||
|
r.Post("", reqPackageAccess(perm.AccessModeWrite), terraform.UpdateState)
|
||||||
|
r.Put("", reqPackageAccess(perm.AccessModeWrite), terraform.UpdateState)
|
||||||
|
// Delete the state
|
||||||
|
r.Delete("", reqPackageAccess(perm.AccessModeWrite), terraform.DeleteState)
|
||||||
|
// Lock and unlock operations for the state
|
||||||
|
r.Group("/lock", func() {
|
||||||
|
// Lock the state
|
||||||
|
r.Post("", reqPackageAccess(perm.AccessModeWrite), terraform.LockState)
|
||||||
|
// Unlock the state
|
||||||
|
r.Delete("", reqPackageAccess(perm.AccessModeWrite), terraform.UnlockState)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
}, context.UserAssignmentWeb(), context.PackageAssignment())
|
}, context.UserAssignmentWeb(), context.PackageAssignment())
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
396
routers/api/packages/terraform/terraform.go
Normal file
396
routers/api/packages/terraform/terraform.go
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
packages_model "code.gitea.io/gitea/models/packages"
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/optional"
|
||||||
|
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"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TFState struct {
|
||||||
|
Version int `json:"version"`
|
||||||
|
TerraformVersion string `json:"terraform_version"`
|
||||||
|
Serial uint64 `json:"serial"`
|
||||||
|
Lineage string `json:"lineage"`
|
||||||
|
Outputs map[string]any `json:"outputs"`
|
||||||
|
Resources []ResourceState `json:"resources"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceState struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Instances []InstanceState `json:"instances"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InstanceState struct {
|
||||||
|
SchemaVersion int `json:"schema_version"`
|
||||||
|
Attributes map[string]any `json:"attributes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
stateStorage = make(map[string]*TFState)
|
||||||
|
stateLocks = make(map[string]string)
|
||||||
|
storeMutex sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func apiError(ctx *context.Context, status int, obj any) {
|
||||||
|
helper.LogAndProcessError(ctx, status, obj, func(message string) {
|
||||||
|
type Error struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
ctx.JSON(status, struct {
|
||||||
|
Errors []Error `json:"errors"`
|
||||||
|
}{
|
||||||
|
Errors: []Error{
|
||||||
|
{Status: status, Message: message},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetState(ctx *context.Context) {
|
||||||
|
stateName := ctx.PathParam("statename")
|
||||||
|
log.Info("Function GetState called with parameters: stateName=%s", stateName)
|
||||||
|
|
||||||
|
// Find the package version
|
||||||
|
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||||
|
OwnerID: ctx.Package.Owner.ID,
|
||||||
|
Type: packages_model.TypeTerraform,
|
||||||
|
Name: packages_model.SearchValue{
|
||||||
|
ExactMatch: true,
|
||||||
|
Value: stateName,
|
||||||
|
},
|
||||||
|
HasFileWithName: stateName,
|
||||||
|
IsInternal: optional.Some(false),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to search package versions for state %s: %v", stateName, err)
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no version is found, return 204
|
||||||
|
if len(pvs) == 0 {
|
||||||
|
log.Info("No existing state found for %s, returning 204 No Content", stateName)
|
||||||
|
ctx.Resp.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the latest package version
|
||||||
|
stateVersion := pvs[0]
|
||||||
|
if stateVersion == nil {
|
||||||
|
log.Error("State version is nil for state %s", stateName)
|
||||||
|
apiError(ctx, http.StatusInternalServerError, "Invalid state version")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Info("Fetching file stream for state %s with version %s", stateName, stateVersion.Version)
|
||||||
|
|
||||||
|
// Log the parameters of GetFileStreamByPackageNameAndVersion call
|
||||||
|
log.Info("Fetching file stream with params: Owner=%v, PackageType=%v, Name=%v, Version=%v, Filename=%v",
|
||||||
|
ctx.Package.Owner,
|
||||||
|
packages_model.TypeTerraform,
|
||||||
|
stateName,
|
||||||
|
stateVersion.Version,
|
||||||
|
stateName,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fetch the file stream
|
||||||
|
s, _, _, err := packages_service.GetFileStreamByPackageNameAndVersion(
|
||||||
|
ctx,
|
||||||
|
&packages_service.PackageInfo{
|
||||||
|
Owner: ctx.Package.Owner,
|
||||||
|
PackageType: packages_model.TypeTerraform,
|
||||||
|
Name: stateName,
|
||||||
|
Version: stateVersion.Version,
|
||||||
|
},
|
||||||
|
&packages_service.PackageFileInfo{
|
||||||
|
Filename: stateName,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error fetching file stream for state %s: %v", stateName, err)
|
||||||
|
if errors.Is(err, packages_model.ErrPackageNotExist) {
|
||||||
|
log.Error("Package does not exist: %v", err)
|
||||||
|
apiError(ctx, http.StatusNotFound, "Package not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errors.Is(err, packages_model.ErrPackageFileNotExist) {
|
||||||
|
log.Error("Package file does not exist: %v", err)
|
||||||
|
apiError(ctx, http.StatusNotFound, "File not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
apiError(ctx, http.StatusInternalServerError, "Failed to fetch file stream")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// Read the file contents
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
if _, err := io.Copy(buf, s); err != nil {
|
||||||
|
log.Error("Failed to read state file for %s: %v", stateName, err)
|
||||||
|
apiError(ctx, http.StatusInternalServerError, "Failed to read state file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize the state
|
||||||
|
var state TFState
|
||||||
|
if err := json.Unmarshal(buf.Bytes(), &state); err != nil {
|
||||||
|
log.Error("Failed to unmarshal state file for %s: %v", stateName, err)
|
||||||
|
apiError(ctx, http.StatusInternalServerError, "Invalid state file format")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure lineage is set
|
||||||
|
if state.Lineage == "" {
|
||||||
|
state.Lineage = uuid.NewString()
|
||||||
|
log.Info("Generated new lineage for state %s: %s", stateName, state.Lineage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the state in the response
|
||||||
|
ctx.Resp.Header().Set("Content-Type", "application/json")
|
||||||
|
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", stateName))
|
||||||
|
ctx.Resp.WriteHeader(http.StatusOK)
|
||||||
|
if _, writeErr := ctx.Resp.Write(buf.Bytes()); writeErr != nil {
|
||||||
|
log.Error("Failed to write response for state %s: %v", stateName, writeErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateState updates or creates a new Terraform state and interacts with Gitea packages.
|
||||||
|
func UpdateState(ctx *context.Context) {
|
||||||
|
stateName := ctx.PathParam("statename")
|
||||||
|
log.Info("UpdateState called for stateName: %s", stateName)
|
||||||
|
|
||||||
|
storeMutex.Lock()
|
||||||
|
defer storeMutex.Unlock()
|
||||||
|
|
||||||
|
// Check for the presence of a lock ID
|
||||||
|
requestLockID := ctx.Req.URL.Query().Get("ID")
|
||||||
|
if requestLockID == "" {
|
||||||
|
apiError(ctx, http.StatusBadRequest, "Missing ID query parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for blocking state
|
||||||
|
if lockID, locked := stateLocks[stateName]; locked && lockID != requestLockID {
|
||||||
|
apiError(ctx, http.StatusConflict, fmt.Sprintf("State %s is locked", stateName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the request body
|
||||||
|
body, err := io.ReadAll(ctx.Req.Body)
|
||||||
|
if err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, "Failed to read request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newState TFState
|
||||||
|
if err := json.Unmarshal(body, &newState); err != nil {
|
||||||
|
apiError(ctx, http.StatusBadRequest, "Invalid JSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getting the current serial
|
||||||
|
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||||
|
OwnerID: ctx.Package.Owner.ID,
|
||||||
|
Type: packages_model.TypeTerraform,
|
||||||
|
Name: packages_model.SearchValue{ExactMatch: true, Value: stateName},
|
||||||
|
IsInternal: optional.Some(false),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, "Failed to search package versions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serial := uint64(1) // Start from 1
|
||||||
|
if len(pvs) > 0 {
|
||||||
|
lastSerial, _ := strconv.ParseUint(pvs[0].Version, 10, 64)
|
||||||
|
serial = lastSerial + 1
|
||||||
|
}
|
||||||
|
log.Info("State %s updated to serial %d", stateName, serial)
|
||||||
|
|
||||||
|
// Create package information
|
||||||
|
packageVersion := fmt.Sprintf("%d", serial)
|
||||||
|
packageInfo := &packages_service.PackageCreationInfo{
|
||||||
|
PackageInfo: packages_service.PackageInfo{
|
||||||
|
Owner: ctx.Package.Owner,
|
||||||
|
PackageType: packages_model.TypeTerraform,
|
||||||
|
Name: stateName,
|
||||||
|
Version: packageVersion,
|
||||||
|
},
|
||||||
|
SemverCompatible: true,
|
||||||
|
Creator: ctx.Doer,
|
||||||
|
Metadata: newState,
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer, err := packages_module.CreateHashedBufferFromReader(bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, "Failed to create buffer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create/update package
|
||||||
|
if _, _, err = packages_service.CreatePackageOrAddFileToExisting(
|
||||||
|
ctx,
|
||||||
|
packageInfo,
|
||||||
|
&packages_service.PackageFileCreationInfo{
|
||||||
|
PackageFileInfo: packages_service.PackageFileInfo{
|
||||||
|
Filename: stateName,
|
||||||
|
},
|
||||||
|
Creator: ctx.Doer,
|
||||||
|
Data: buffer,
|
||||||
|
IsLead: true,
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, "Failed to update package")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("State %s updated successfully with version %s", stateName, packageVersion)
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, map[string]string{
|
||||||
|
"message": "State updated successfully",
|
||||||
|
"statename": stateName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockState locks a Terraform state to prevent updates.
|
||||||
|
func LockState(ctx *context.Context) {
|
||||||
|
stateName := ctx.PathParam("statename")
|
||||||
|
log.Info("LockState called for state: %s", stateName)
|
||||||
|
|
||||||
|
// Read the request body
|
||||||
|
body, err := io.ReadAll(ctx.Req.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to read request body: %v", err)
|
||||||
|
apiError(ctx, http.StatusInternalServerError, "Failed to read request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode JSON and check lockID
|
||||||
|
var lockRequest struct {
|
||||||
|
ID string `json:"ID"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &lockRequest); err != nil || lockRequest.ID == "" {
|
||||||
|
log.Error("Invalid lock request body: %v", err)
|
||||||
|
apiError(ctx, http.StatusBadRequest, "Invalid or missing lock ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
storeMutex.Lock()
|
||||||
|
defer storeMutex.Unlock()
|
||||||
|
|
||||||
|
// Check if the state is locked
|
||||||
|
if _, locked := stateLocks[stateName]; locked {
|
||||||
|
log.Warn("State %s is already locked", stateName)
|
||||||
|
apiError(ctx, http.StatusConflict, fmt.Sprintf("State %s is already locked", stateName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the lock
|
||||||
|
stateLocks[stateName] = lockRequest.ID
|
||||||
|
log.Info("State %s locked with ID %s", stateName, lockRequest.ID)
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, map[string]string{
|
||||||
|
"message": "State locked successfully",
|
||||||
|
"statename": stateName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnlockState unlocks a Terraform state.
|
||||||
|
func UnlockState(ctx *context.Context) {
|
||||||
|
stateName := ctx.PathParam("statename")
|
||||||
|
log.Info("UnlockState called for state: %s", stateName)
|
||||||
|
|
||||||
|
// Extract lockID from request body or parameters
|
||||||
|
var unlockRequest struct {
|
||||||
|
ID string `json:"ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trying to read the request body
|
||||||
|
body, _ := io.ReadAll(ctx.Req.Body)
|
||||||
|
if len(body) > 0 {
|
||||||
|
_ = json.Unmarshal(body, &unlockRequest) // The error can be ignored, since the ID can also be in the query
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the ID is not found in the body, look in the query parameters
|
||||||
|
if unlockRequest.ID == "" {
|
||||||
|
unlockRequest.ID = ctx.Query("ID").(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for ID presence
|
||||||
|
if unlockRequest.ID == "" {
|
||||||
|
log.Error("Missing lock ID in both query and request body")
|
||||||
|
apiError(ctx, http.StatusBadRequest, "Missing lock ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Extracted lockID: %s", unlockRequest.ID)
|
||||||
|
|
||||||
|
storeMutex.Lock()
|
||||||
|
defer storeMutex.Unlock()
|
||||||
|
|
||||||
|
// Check the lock status
|
||||||
|
currentLockID, locked := stateLocks[stateName]
|
||||||
|
if !locked || currentLockID != unlockRequest.ID {
|
||||||
|
log.Warn("Unlock attempt failed for state %s with lock ID %s", stateName, unlockRequest.ID)
|
||||||
|
apiError(ctx, http.StatusConflict, fmt.Sprintf("State %s is not locked or lock ID mismatch", stateName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the lock
|
||||||
|
delete(stateLocks, stateName)
|
||||||
|
log.Info("State %s unlocked successfully", stateName)
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, map[string]string{
|
||||||
|
"message": "State unlocked successfully",
|
||||||
|
"statename": stateName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteState deletes the Terraform state for a given name.
|
||||||
|
func DeleteState(ctx *context.Context) {
|
||||||
|
stateName := ctx.PathParam("statename")
|
||||||
|
log.Info("Attempting to delete state: %s", stateName)
|
||||||
|
|
||||||
|
storeMutex.Lock()
|
||||||
|
defer storeMutex.Unlock()
|
||||||
|
|
||||||
|
// Check if a state or lock exists
|
||||||
|
_, stateExists := stateStorage[stateName]
|
||||||
|
_, lockExists := stateLocks[stateName]
|
||||||
|
|
||||||
|
if !stateExists && !lockExists {
|
||||||
|
log.Warn("State %s does not exist or is not locked", stateName)
|
||||||
|
apiError(ctx, http.StatusNotFound, "State not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the state and lock
|
||||||
|
delete(stateStorage, stateName)
|
||||||
|
delete(stateLocks, stateName)
|
||||||
|
|
||||||
|
log.Info("State %s deleted successfully", stateName)
|
||||||
|
ctx.JSON(http.StatusOK, map[string]string{
|
||||||
|
"message": "State deleted successfully",
|
||||||
|
"statename": stateName,
|
||||||
|
})
|
||||||
|
}
|
@ -40,7 +40,7 @@ func ListPackages(ctx *context.APIContext) {
|
|||||||
// in: query
|
// in: query
|
||||||
// description: package type filter
|
// description: package type filter
|
||||||
// type: string
|
// 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
|
// - name: q
|
||||||
// in: query
|
// in: query
|
||||||
// description: name filter
|
// description: name filter
|
||||||
|
@ -15,7 +15,7 @@ import (
|
|||||||
type PackageCleanupRuleForm struct {
|
type PackageCleanupRuleForm struct {
|
||||||
ID int64
|
ID int64
|
||||||
Enabled bool
|
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)"`
|
KeepCount int `binding:"In(0,1,5,10,25,50,100)"`
|
||||||
KeepPattern string `binding:"RegexPattern"`
|
KeepPattern string `binding:"RegexPattern"`
|
||||||
RemoveDays int `binding:"In(0,7,14,30,60,90,180)"`
|
RemoveDays int `binding:"In(0,7,14,30,60,90,180)"`
|
||||||
|
@ -393,6 +393,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p
|
|||||||
typeSpecificSize = setting.Packages.LimitSizeRubyGems
|
typeSpecificSize = setting.Packages.LimitSizeRubyGems
|
||||||
case packages_model.TypeSwift:
|
case packages_model.TypeSwift:
|
||||||
typeSpecificSize = setting.Packages.LimitSizeSwift
|
typeSpecificSize = setting.Packages.LimitSizeSwift
|
||||||
|
case packages_model.TypeTerraform:
|
||||||
|
typeSpecificSize = setting.Packages.LimitSizeTerraform
|
||||||
case packages_model.TypeVagrant:
|
case packages_model.TypeVagrant:
|
||||||
typeSpecificSize = setting.Packages.LimitSizeVagrant
|
typeSpecificSize = setting.Packages.LimitSizeVagrant
|
||||||
}
|
}
|
||||||
|
30
templates/package/content/terraform.tmpl
Normal file
30
templates/package/content/terraform.tmpl
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{{if eq .PackageDescriptor.Package.Type "terraform"}}
|
||||||
|
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.installation"}}</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<div class="ui form">
|
||||||
|
<div class="field">
|
||||||
|
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.terraform.install"}}</label>
|
||||||
|
<div class="markup"><pre class="code-block"><code>
|
||||||
|
export GITEA_USER_PASSWORD=<YOUR-USER-PASSWORD>
|
||||||
|
export TF_STATE_NAME=your-state.tfstate
|
||||||
|
terraform init \
|
||||||
|
-backend-config="address=<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/terraform/state/$TF_STATE_NAME"></origin-url> \
|
||||||
|
-backend-config="lock_address=<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/terraform/state/$TF_STATE_NAME/lock"></origin-url> \
|
||||||
|
-backend-config="unlock_address=<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/terraform/state/$TF_STATE_NAME/lock"></origin-url> \
|
||||||
|
-backend-config="username={{.PackageDescriptor.Owner.Name}}" \
|
||||||
|
-backend-config="password=$GITEA_USER_PASSWORD" \
|
||||||
|
-backend-config="lock_method=POST" \
|
||||||
|
-backend-config="unlock_method=DELETE" \
|
||||||
|
-backend-config="retry_wait_min=5"
|
||||||
|
</code></pre></div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ctx.Locale.Tr "packages.registry.documentation" "Terraform" "https://docs.gitea.com/usage/packages/terraform/"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{if .PackageDescriptor.Metadata.Description}}
|
||||||
|
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4>
|
||||||
|
<div class="ui attached segment">{{.PackageDescriptor.Metadata.Description}}</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
@ -37,6 +37,7 @@
|
|||||||
{{template "package/content/rpm" .}}
|
{{template "package/content/rpm" .}}
|
||||||
{{template "package/content/rubygems" .}}
|
{{template "package/content/rubygems" .}}
|
||||||
{{template "package/content/swift" .}}
|
{{template "package/content/swift" .}}
|
||||||
|
{{template "package/content/terraform" .}}
|
||||||
{{template "package/content/vagrant" .}}
|
{{template "package/content/vagrant" .}}
|
||||||
</div>
|
</div>
|
||||||
<div class="issue-content-right ui segment">
|
<div class="issue-content-right ui segment">
|
||||||
@ -68,6 +69,7 @@
|
|||||||
{{template "package/metadata/rpm" .}}
|
{{template "package/metadata/rpm" .}}
|
||||||
{{template "package/metadata/rubygems" .}}
|
{{template "package/metadata/rubygems" .}}
|
||||||
{{template "package/metadata/swift" .}}
|
{{template "package/metadata/swift" .}}
|
||||||
|
{{template "package/metadata/terraform" .}}
|
||||||
{{template "package/metadata/vagrant" .}}
|
{{template "package/metadata/vagrant" .}}
|
||||||
{{if not (and (eq .PackageDescriptor.Package.Type "container") .PackageDescriptor.Metadata.Manifests)}}
|
{{if not (and (eq .PackageDescriptor.Package.Type "container") .PackageDescriptor.Metadata.Manifests)}}
|
||||||
<div class="item">{{svg "octicon-database"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}</div>
|
<div class="item">{{svg "octicon-database"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}</div>
|
||||||
|
1
templates/swagger/v1_json.tmpl
generated
1
templates/swagger/v1_json.tmpl
generated
@ -3275,6 +3275,7 @@
|
|||||||
"rpm",
|
"rpm",
|
||||||
"rubygems",
|
"rubygems",
|
||||||
"swift",
|
"swift",
|
||||||
|
"terraform",
|
||||||
"vagrant"
|
"vagrant"
|
||||||
],
|
],
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
154
tests/integration/api_packages_terraform_test.go
Normal file
154
tests/integration/api_packages_terraform_test.go
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
"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/json"
|
||||||
|
terraform_module "code.gitea.io/gitea/modules/packages/terraform"
|
||||||
|
"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})
|
||||||
|
|
||||||
|
token := "Bearer " + getUserToken(t, user.Name, auth_model.AccessTokenScopeWritePackage)
|
||||||
|
|
||||||
|
packageName := "test_module"
|
||||||
|
packageVersion := "1.0.1"
|
||||||
|
packageDescription := "Test Terraform Module"
|
||||||
|
|
||||||
|
filename := "terraform_module.tar.gz"
|
||||||
|
|
||||||
|
infoContent, _ := json.Marshal(map[string]string{
|
||||||
|
"description": packageDescription,
|
||||||
|
})
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
zw := gzip.NewWriter(&buf)
|
||||||
|
archive := tar.NewWriter(zw)
|
||||||
|
archive.WriteHeader(&tar.Header{
|
||||||
|
Name: "info.json",
|
||||||
|
Mode: 0o600,
|
||||||
|
Size: int64(len(infoContent)),
|
||||||
|
})
|
||||||
|
archive.Write(infoContent)
|
||||||
|
archive.Close()
|
||||||
|
zw.Close()
|
||||||
|
content := buf.Bytes()
|
||||||
|
|
||||||
|
root := fmt.Sprintf("/api/packages/%s/terraform", user.Name)
|
||||||
|
|
||||||
|
t.Run("Authenticate", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
authenticateURL := fmt.Sprintf("%s/authenticate", root)
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", authenticateURL)
|
||||||
|
MakeRequest(t, req, http.StatusUnauthorized)
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", authenticateURL).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
moduleURL := fmt.Sprintf("%s/%s", root, packageName)
|
||||||
|
|
||||||
|
t.Run("Upload", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "HEAD", moduleURL)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
|
uploadURL := fmt.Sprintf("%s/%s/%s", moduleURL, packageVersion, filename)
|
||||||
|
|
||||||
|
req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content))
|
||||||
|
MakeRequest(t, req, http.StatusUnauthorized)
|
||||||
|
|
||||||
|
req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content)).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusCreated)
|
||||||
|
|
||||||
|
req = NewRequest(t, "HEAD", moduleURL)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.True(t, strings.HasPrefix(resp.Header().Get("Content-Type"), "application/json"))
|
||||||
|
|
||||||
|
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.NotNil(t, pd.SemVer)
|
||||||
|
assert.IsType(t, &terraform_module.Metadata{}, pd.Metadata)
|
||||||
|
assert.Equal(t, packageName, pd.Package.Name)
|
||||||
|
assert.Equal(t, packageVersion, pd.Version.Version)
|
||||||
|
|
||||||
|
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, pfs, 1)
|
||||||
|
assert.Equal(t, filename, 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)
|
||||||
|
|
||||||
|
req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content)).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusConflict)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Download", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", moduleURL, packageVersion, filename))
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
assert.Equal(t, content, resp.Body.Bytes())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("EnumeratePackageVersions", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", moduleURL)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
type versionMetadata struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type packageMetadata struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Versions []*versionMetadata `json:"versions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var result packageMetadata
|
||||||
|
DecodeJSON(t, resp, &result)
|
||||||
|
|
||||||
|
assert.Equal(t, packageName, result.Name)
|
||||||
|
assert.Equal(t, packageDescription, result.Description)
|
||||||
|
assert.Len(t, result.Versions, 1)
|
||||||
|
version := result.Versions[0]
|
||||||
|
assert.Equal(t, packageVersion, version.Version)
|
||||||
|
assert.Equal(t, "active", version.Status)
|
||||||
|
})
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user