mirror of
https://github.com/go-gitea/gitea.git
synced 2025-02-20 11:43:57 +08:00
Compare commits
14 Commits
2f8ec3d3fd
...
8f616977fc
Author | SHA1 | Date | |
---|---|---|---|
|
8f616977fc | ||
|
c2e23d3301 | ||
|
84d2159ef6 | ||
|
0dc7c5509c | ||
|
400fb382a0 | ||
|
98775acb60 | ||
|
b5dd7ea6e1 | ||
|
86fa672d1f | ||
|
0d3d5c7393 | ||
|
e58fa368c6 | ||
|
64f08862c0 | ||
|
5ad2c19e7b | ||
|
0457597f82 | ||
|
351b83df5b |
@ -31,6 +31,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/packages/rpm"
|
||||
"code.gitea.io/gitea/modules/packages/rubygems"
|
||||
"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/util"
|
||||
|
||||
@ -191,6 +192,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
|
||||
metadata = &rubygems.Metadata{}
|
||||
case TypeSwift:
|
||||
metadata = &swift.Metadata{}
|
||||
case TypeTerraform:
|
||||
metadata = &terraform.Metadata{}
|
||||
case TypeVagrant:
|
||||
metadata = &vagrant.Metadata{}
|
||||
default:
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -152,7 +152,7 @@ func (p *Permission) ReadableUnitTypes() []unit.Type {
|
||||
}
|
||||
|
||||
func (p *Permission) LogString() string {
|
||||
format := "<Permission AccessMode=%s, %d Units, %d UnitsMode(s): [ "
|
||||
format := "<Permission AccessMode=%s, %d Units, %d UnitsMode(s): ["
|
||||
args := []any{p.AccessMode.ToString(), len(p.units), len(p.unitsMode)}
|
||||
|
||||
for i, u := range p.units {
|
||||
@ -164,14 +164,16 @@ func (p *Permission) LogString() string {
|
||||
config = err.Error()
|
||||
}
|
||||
}
|
||||
format += "\nUnits[%d]: ID: %d RepoID: %d Type: %s Config: %s"
|
||||
format += "\n\tunits[%d]: ID=%d RepoID=%d Type=%s Config=%s"
|
||||
args = append(args, i, u.ID, u.RepoID, u.Type.LogString(), config)
|
||||
}
|
||||
for key, value := range p.unitsMode {
|
||||
format += "\nUnitMode[%-v]: %-v"
|
||||
format += "\n\tunitsMode[%-v]: %-v"
|
||||
args = append(args, key.LogString(), value.LogString())
|
||||
}
|
||||
format += " ]>"
|
||||
format += "\n\teveryoneAccessMode: %-v"
|
||||
args = append(args, p.everyoneAccessMode)
|
||||
format += "\n\t]>"
|
||||
return fmt.Sprintf(format, args...)
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -175,6 +176,20 @@ func (l *LoggerImpl) IsEnabled() bool {
|
||||
return l.level.Load() < int32(FATAL) && len(l.eventWriters) > 0
|
||||
}
|
||||
|
||||
func asLogStringer(v any) LogStringer {
|
||||
if s, ok := v.(LogStringer); ok {
|
||||
return s
|
||||
} else if a := reflect.ValueOf(v); a.Kind() == reflect.Struct {
|
||||
// in case the receiver is a pointer, but the value is a struct
|
||||
vp := reflect.New(a.Type())
|
||||
vp.Elem().Set(a)
|
||||
if s, ok := vp.Interface().(LogStringer); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Log prepares the log event, if the level matches, the event will be sent to the writers
|
||||
func (l *LoggerImpl) Log(skip int, level Level, format string, logArgs ...any) {
|
||||
if Level(l.level.Load()) > level {
|
||||
@ -207,11 +222,11 @@ func (l *LoggerImpl) Log(skip int, level Level, format string, logArgs ...any) {
|
||||
// handle LogStringer values
|
||||
for i, v := range msgArgs {
|
||||
if cv, ok := v.(*ColoredValue); ok {
|
||||
if s, ok := cv.v.(LogStringer); ok {
|
||||
cv.v = logStringFormatter{v: s}
|
||||
if ls := asLogStringer(cv.v); ls != nil {
|
||||
cv.v = logStringFormatter{v: ls}
|
||||
}
|
||||
} else if s, ok := v.(LogStringer); ok {
|
||||
msgArgs[i] = logStringFormatter{v: s}
|
||||
} else if ls := asLogStringer(v); ls != nil {
|
||||
msgArgs[i] = logStringFormatter{v: ls}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,6 +116,14 @@ func (t testLogString) LogString() string {
|
||||
return "log-string"
|
||||
}
|
||||
|
||||
type testLogStringPtrReceiver struct {
|
||||
Field string
|
||||
}
|
||||
|
||||
func (t *testLogStringPtrReceiver) LogString() string {
|
||||
return "log-string-ptr-receiver"
|
||||
}
|
||||
|
||||
func TestLoggerLogString(t *testing.T) {
|
||||
logger := NewLoggerWithWriters(context.Background(), "test")
|
||||
|
||||
@ -124,9 +132,13 @@ func TestLoggerLogString(t *testing.T) {
|
||||
logger.AddWriters(w1)
|
||||
|
||||
logger.Info("%s %s %#v %v", testLogString{}, &testLogString{}, testLogString{Field: "detail"}, NewColoredValue(testLogString{}, FgRed))
|
||||
logger.Info("%s %s %#v %v", testLogStringPtrReceiver{}, &testLogStringPtrReceiver{}, testLogStringPtrReceiver{Field: "detail"}, NewColoredValue(testLogStringPtrReceiver{}, FgRed))
|
||||
logger.Close()
|
||||
|
||||
assert.Equal(t, []string{"log-string log-string log.testLogString{Field:\"detail\"} \x1b[31mlog-string\x1b[0m\n"}, w1.GetLogs())
|
||||
assert.Equal(t, []string{
|
||||
"log-string log-string log.testLogString{Field:\"detail\"} \x1b[31mlog-string\x1b[0m\n",
|
||||
"log-string-ptr-receiver log-string-ptr-receiver &log.testLogStringPtrReceiver{Field:\"detail\"} \x1b[31mlog-string-ptr-receiver\x1b[0m\n",
|
||||
}, w1.GetLogs())
|
||||
}
|
||||
|
||||
func TestLoggerExpressionFilter(t *testing.T) {
|
||||
|
88
modules/packages/terraform/metadata.go
Normal file
88
modules/packages/terraform/metadata.go
Normal file
@ -0,0 +1,88 @@
|
||||
// Copyright 2025 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 2025 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
|
||||
LimitSizeRubyGems int64
|
||||
LimitSizeSwift int64
|
||||
LimitSizeTerraform int64
|
||||
LimitSizeVagrant int64
|
||||
|
||||
DefaultRPMSignEnabled bool
|
||||
@ -100,6 +101,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
|
||||
|
@ -1702,7 +1702,9 @@ issues.time_estimate_invalid = Time estimate format is invalid
|
||||
issues.start_tracking_history = started working %s
|
||||
issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed
|
||||
issues.tracking_already_started = `You have already started time tracking on <a href="%s">another issue</a>!`
|
||||
issues.stop_tracking = Stop Timer
|
||||
issues.stop_tracking_history = worked for <b>%[1]s</b> %[2]s
|
||||
issues.cancel_tracking = Discard
|
||||
issues.cancel_tracking_history = `canceled time tracking %s`
|
||||
issues.del_time = Delete this time log
|
||||
issues.add_time_history = added spent time <b>%[1]s</b> %[2]s
|
||||
|
@ -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"
|
||||
@ -674,6 +675,26 @@ func CommonRoutes() *web.Router {
|
||||
})
|
||||
})
|
||||
}, 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())
|
||||
|
||||
return r
|
||||
|
278
routers/api/packages/terraform/terraform.go
Normal file
278
routers/api/packages/terraform/terraform.go
Normal file
@ -0,0 +1,278 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
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/services/context"
|
||||
"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"`
|
||||
}
|
||||
|
||||
type LockInfo struct {
|
||||
ID string `json:"id"`
|
||||
Created string `json:"created"`
|
||||
}
|
||||
|
||||
var stateLocks = make(map[string]LockInfo)
|
||||
|
||||
func apiError(ctx *context.Context, status int, message string) {
|
||||
log.Error("Terraform API Error: %d - %s", status, message)
|
||||
ctx.JSON(status, map[string]string{"error": message})
|
||||
}
|
||||
|
||||
func getLockID(ctx *context.Context) (string, error) {
|
||||
var lock struct {
|
||||
ID string `json:"ID"`
|
||||
}
|
||||
|
||||
// Read the body of the request and try to parse the JSON
|
||||
body, err := io.ReadAll(ctx.Req.Body)
|
||||
if err == nil && len(body) > 0 {
|
||||
if err := json.Unmarshal(body, &lock); err != nil {
|
||||
log.Error("Failed to unmarshal request body: %v", err)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// We check the presence of lock ID in the request body or request parameters
|
||||
if lock.ID == "" {
|
||||
lock.ID = ctx.Req.URL.Query().Get("ID")
|
||||
}
|
||||
|
||||
if lock.ID == "" {
|
||||
apiError(ctx, http.StatusBadRequest, "Missing lock ID")
|
||||
return "", fmt.Errorf("missing lock ID")
|
||||
}
|
||||
|
||||
log.Info("Extracted lockID: %s", lock.ID)
|
||||
return lock.ID, nil
|
||||
}
|
||||
|
||||
func GetState(ctx *context.Context) {
|
||||
stateName := ctx.PathParam("statename")
|
||||
log.Info("GetState called for: %s", stateName)
|
||||
|
||||
pvs, _, err := packages_model.SearchLatestVersions(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),
|
||||
Sort: packages_model.SortCreatedDesc,
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, "Failed to fetch latest versions")
|
||||
return
|
||||
}
|
||||
|
||||
if len(pvs) == 0 {
|
||||
apiError(ctx, http.StatusNoContent, "No content available")
|
||||
return
|
||||
}
|
||||
|
||||
stream, _, _, err := packages.GetFileStreamByPackageNameAndVersion(ctx, &packages.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeTerraform,
|
||||
Name: stateName,
|
||||
Version: pvs[0].Version,
|
||||
}, &packages.PackageFileInfo{Filename: stateName})
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, packages_model.ErrPackageNotExist):
|
||||
apiError(ctx, http.StatusNotFound, "Package not found")
|
||||
case errors.Is(err, packages_model.ErrPackageFileNotExist):
|
||||
apiError(ctx, http.StatusNotFound, "File not found")
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
var state TFState
|
||||
if err := json.NewDecoder(stream).Decode(&state); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, "Failed to parse state file")
|
||||
return
|
||||
}
|
||||
|
||||
if state.Lineage == "" {
|
||||
state.Lineage = uuid.NewString()
|
||||
log.Info("Generated new lineage for state: %s", state.Lineage)
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", "application/json")
|
||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", stateName))
|
||||
ctx.JSON(http.StatusOK, state)
|
||||
}
|
||||
|
||||
func UpdateState(ctx *context.Context) {
|
||||
stateName := ctx.PathParam("statename")
|
||||
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
|
||||
}
|
||||
|
||||
pvs, _, err := packages_model.SearchLatestVersions(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),
|
||||
Sort: packages_model.SortCreatedDesc,
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
serial := uint64(0)
|
||||
if len(pvs) > 0 {
|
||||
if lastSerial, err := strconv.ParseUint(pvs[0].Version, 10, 64); err == nil {
|
||||
serial = lastSerial + 1
|
||||
}
|
||||
}
|
||||
|
||||
packageVersion := fmt.Sprintf("%d", serial)
|
||||
|
||||
packageInfo := &packages.PackageCreationInfo{
|
||||
PackageInfo: packages.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeTerraform,
|
||||
Name: stateName,
|
||||
Version: packageVersion,
|
||||
},
|
||||
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
|
||||
}
|
||||
_, _, err = packages.CreatePackageOrAddFileToExisting(ctx, packageInfo, &packages.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages.PackageFileInfo{Filename: stateName},
|
||||
Creator: ctx.Doer,
|
||||
Data: buffer,
|
||||
IsLead: true,
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, "Failed to update package")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]string{"message": "State updated successfully", "statename": stateName})
|
||||
}
|
||||
|
||||
func LockState(ctx *context.Context) {
|
||||
stateName := ctx.PathParam("statename")
|
||||
lockID, err := getLockID(ctx)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the state is locked
|
||||
if lockInfo, locked := stateLocks[stateName]; locked {
|
||||
log.Warn("State %s is already locked", stateName)
|
||||
|
||||
// Generate a response for the conflict with information about the current lock
|
||||
response := lockInfo // Return full information about the lock
|
||||
ctx.JSON(http.StatusConflict, response)
|
||||
return
|
||||
}
|
||||
|
||||
// Set the lock
|
||||
stateLocks[stateName] = LockInfo{
|
||||
ID: lockID,
|
||||
Created: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
log.Info("Locked state: %s with ID: %s", stateName, lockID)
|
||||
ctx.JSON(http.StatusOK, map[string]string{"message": "State locked successfully", "statename": stateName})
|
||||
}
|
||||
|
||||
func UnlockState(ctx *context.Context) {
|
||||
stateName := ctx.PathParam("statename")
|
||||
lockID, err := getLockID(ctx)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check the lock status
|
||||
currentLockInfo, locked := stateLocks[stateName]
|
||||
if !locked || currentLockInfo.ID != lockID {
|
||||
log.Warn("Unlock attempt failed for state %s with lock ID %s", stateName, lockID)
|
||||
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("Unlocked state: %s with ID: %s", stateName, lockID)
|
||||
ctx.JSON(http.StatusOK, map[string]string{"message": "State unlocked successfully"})
|
||||
}
|
||||
|
||||
func DeleteState(ctx *context.Context) {
|
||||
stateName := ctx.PathParam("statename")
|
||||
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraform, stateName)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, "Failed to fetch package versions")
|
||||
return
|
||||
}
|
||||
if len(pvs) == 0 {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
for _, pv := range pvs {
|
||||
if err := packages.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, "Failed to delete package version")
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.JSON(http.StatusOK, map[string]string{"message": "State deleted successfully"})
|
||||
}
|
@ -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
|
||||
|
@ -1196,6 +1196,10 @@ func registerRoutes(m *web.Router) {
|
||||
})
|
||||
})
|
||||
}
|
||||
// FIXME: many "pulls" requests are sent to "issues" endpoints correctly, so the issue endpoints have to tolerate pull request permissions at the moment
|
||||
m.Group("/{username}/{reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests))
|
||||
m.Group("/{username}/{reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader)
|
||||
|
||||
m.Group("/{username}/{reponame}", func() {
|
||||
m.Get("/comments/{id}/attachments", repo.GetCommentAttachments)
|
||||
m.Get("/labels", repo.RetrieveLabelsForList, repo.Labels)
|
||||
@ -1203,9 +1207,6 @@ func registerRoutes(m *web.Router) {
|
||||
m.Get("/milestone/{id}", context.RepoRef(), repo.MilestoneIssuesAndPulls)
|
||||
m.Get("/issues/suggestions", repo.IssueSuggestions)
|
||||
}, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) // issue/pull attachments, labels, milestones
|
||||
|
||||
m.Group("/{username}/{reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitIssuesReader)
|
||||
m.Group("/{username}/{reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader)
|
||||
// end "/{username}/{reponame}": view milestone, label, issue, pull, etc
|
||||
|
||||
m.Group("/{username}/{reponame}/{type:issues}", func() {
|
||||
@ -1224,7 +1225,7 @@ func registerRoutes(m *web.Router) {
|
||||
m.Get("/search", repo.SearchRepoIssuesJSON)
|
||||
}, reqUnitIssuesReader)
|
||||
|
||||
addIssuesPullsRoutes := func() {
|
||||
addIssuesPullsUpdateRoutes := func() {
|
||||
// for "/{username}/{reponame}/issues" or "/{username}/{reponame}/pulls"
|
||||
m.Group("/{index}", func() {
|
||||
m.Post("/title", repo.UpdateIssueTitle)
|
||||
@ -1267,8 +1268,9 @@ func registerRoutes(m *web.Router) {
|
||||
m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
|
||||
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
|
||||
}
|
||||
m.Group("/{type:issues}", addIssuesPullsRoutes, reqUnitIssuesReader, context.RepoMustNotBeArchived())
|
||||
m.Group("/{type:pulls}", addIssuesPullsRoutes, reqUnitPullsReader, context.RepoMustNotBeArchived())
|
||||
// FIXME: many "pulls" requests are sent to "issues" endpoints incorrectly, so the issue endpoints have to tolerate pull request permissions at the moment
|
||||
m.Group("/{type:issues}", addIssuesPullsUpdateRoutes, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests), context.RepoMustNotBeArchived())
|
||||
m.Group("/{type:pulls}", addIssuesPullsUpdateRoutes, reqUnitPullsReader, context.RepoMustNotBeArchived())
|
||||
|
||||
m.Group("/comments/{id}", func() {
|
||||
m.Post("", repo.UpdateCommentContent)
|
||||
@ -1292,7 +1294,7 @@ func registerRoutes(m *web.Router) {
|
||||
m.Post("/delete", repo.DeleteMilestone)
|
||||
}, reqRepoIssuesOrPullsWriter, context.RepoRef())
|
||||
|
||||
// FIXME: need to move these routes to the proper place
|
||||
// FIXME: many "pulls" requests are sent to "issues" endpoints incorrectly, need to move these routes to the proper place
|
||||
m.Group("/issues", func() {
|
||||
m.Post("/request_review", repo.UpdatePullReviewRequest)
|
||||
m.Post("/dismiss_review", reqRepoAdmin, web.Bind(forms.DismissReviewForm{}), repo.DismissReview)
|
||||
|
@ -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)"`
|
||||
|
@ -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
|
||||
}
|
||||
|
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}}
|
5
templates/package/metadata/terraform.tmpl
Normal file
5
templates/package/metadata/terraform.tmpl
Normal file
@ -0,0 +1,5 @@
|
||||
{{if eq .PackageDescriptor.Package.Type "terrafomr"}}
|
||||
{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
|
||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
|
||||
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
|
||||
{{end}}
|
@ -37,6 +37,7 @@
|
||||
{{template "package/content/rpm" .}}
|
||||
{{template "package/content/rubygems" .}}
|
||||
{{template "package/content/swift" .}}
|
||||
{{template "package/content/terraform" .}}
|
||||
{{template "package/content/vagrant" .}}
|
||||
</div>
|
||||
<div class="issue-content-right ui segment">
|
||||
@ -68,6 +69,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)}}
|
||||
<div class="item">{{svg "octicon-database"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}</div>
|
||||
|
1
templates/swagger/v1_json.tmpl
generated
1
templates/swagger/v1_json.tmpl
generated
@ -3315,6 +3315,7 @@
|
||||
"rpm",
|
||||
"rubygems",
|
||||
"swift",
|
||||
"terraform",
|
||||
"vagrant"
|
||||
],
|
||||
"type": "string",
|
||||
|
130
tests/integration/api_packages_terraform_test.go
Normal file
130
tests/integration/api_packages_terraform_test.go
Normal file
@ -0,0 +1,130 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
gouuid "github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPackageTerraform(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
// Get token for the user
|
||||
token := "Bearer " + getUserToken(t, user.Name, auth.AccessTokenScopeWritePackage)
|
||||
|
||||
// Define important values
|
||||
lineage := "bca3c5f6-01dc-cdad-5310-d1b12e02e430"
|
||||
terraformVersion := "1.10.4"
|
||||
serial := float64(1)
|
||||
resourceName := "hello"
|
||||
resourceType := "null_resource"
|
||||
id := gouuid.New().String() // Generate a unique ID
|
||||
|
||||
// Build the state JSON
|
||||
buildState := func() string {
|
||||
return `{
|
||||
"version": 4,
|
||||
"terraform_version": "` + terraformVersion + `",
|
||||
"serial": ` + fmt.Sprintf("%.0f", serial) + `,
|
||||
"lineage": "` + lineage + `",
|
||||
"outputs": {},
|
||||
"resources": [{
|
||||
"mode": "managed",
|
||||
"type": "` + resourceType + `",
|
||||
"name": "` + resourceName + `",
|
||||
"provider": "provider[\"registry.terraform.io/hashicorp/null\"]",
|
||||
"instances": [{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"id": "3832416504545530133",
|
||||
"triggers": null
|
||||
},
|
||||
"sensitive_attributes": []
|
||||
}]
|
||||
}],
|
||||
"check_results": null
|
||||
}`
|
||||
}
|
||||
state := buildState()
|
||||
content := []byte(state)
|
||||
root := fmt.Sprintf("/api/packages/%s/terraform/state", user.Name)
|
||||
stateURL := fmt.Sprintf("%s/providers-gitea.tfstate", root)
|
||||
|
||||
// Upload test
|
||||
t.Run("Upload", func(t *testing.T) {
|
||||
uploadURL := fmt.Sprintf("%s?ID=%s", stateURL, id)
|
||||
req := NewRequestWithBody(t, "POST", uploadURL, bytes.NewReader(content)).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK) // Expecting 200 OK
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
assert.Contains(t, resp.Header().Get("Content-Type"), "application/json")
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, bodyBytes)
|
||||
})
|
||||
|
||||
// Download test
|
||||
t.Run("Download", func(t *testing.T) {
|
||||
downloadURL := fmt.Sprintf("%s?ID=%s", stateURL, id)
|
||||
req := NewRequest(t, "GET", downloadURL)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.True(t, strings.HasPrefix(resp.Header().Get("Content-Type"), "application/json"))
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, bodyBytes)
|
||||
|
||||
var jsonResponse map[string]any
|
||||
err = json.Unmarshal(bodyBytes, &jsonResponse)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Validate the response
|
||||
assert.Equal(t, lineage, jsonResponse["lineage"])
|
||||
assert.Equal(t, terraformVersion, jsonResponse["terraform_version"])
|
||||
assert.InEpsilon(t, serial, jsonResponse["serial"].(float64), 0.0001)
|
||||
resource := jsonResponse["resources"].([]any)[0].(map[string]any)
|
||||
assert.Equal(t, resourceName, resource["name"])
|
||||
assert.Equal(t, resourceType, resource["type"])
|
||||
assert.NotContains(t, resource, "sensitive_attributes")
|
||||
})
|
||||
|
||||
// Lock state test
|
||||
t.Run("LockState", func(t *testing.T) {
|
||||
lockURL := fmt.Sprintf("%s/lock?ID=%s", stateURL, id)
|
||||
req := NewRequestWithBody(t, "POST", lockURL, bytes.NewReader(content)).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK) // Expecting 200 OK
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
})
|
||||
|
||||
// Unlock state test
|
||||
t.Run("UnlockState", func(t *testing.T) {
|
||||
unlockURL := fmt.Sprintf("%s/lock?ID=%s", stateURL, id)
|
||||
req := NewRequestWithBody(t, "DELETE", unlockURL, bytes.NewReader(content)).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK) // Expecting 200 OK
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
})
|
||||
|
||||
// Download not found test
|
||||
t.Run("DownloadNotFound", func(t *testing.T) {
|
||||
invalidStateURL := fmt.Sprintf("%s/invalid-state.tfstate?ID=%s", root, id)
|
||||
req := NewRequest(t, "GET", invalidStateURL)
|
||||
resp := MakeRequest(t, req, http.StatusNoContent) // Expecting 204 No Content
|
||||
assert.Equal(t, http.StatusNoContent, resp.Code)
|
||||
})
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user