Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions models/packages/descriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions models/packages/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const (
TypeRpm Type = "rpm"
TypeRubyGems Type = "rubygems"
TypeSwift Type = "swift"
TypeTerraform Type = "terraform"
TypeVagrant Type = "vagrant"
)

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

Expand Down Expand Up @@ -124,6 +126,8 @@ func (pt Type) Name() string {
return "RubyGems"
case TypeSwift:
return "Swift"
case TypeTerraform:
return "Terraform"
case TypeVagrant:
return "Vagrant"
}
Expand Down Expand Up @@ -175,6 +179,8 @@ func (pt Type) SVGName() string {
return "gitea-rubygems"
case TypeSwift:
return "gitea-swift"
case TypeTerraform:
return "gitea-terraform"
case TypeVagrant:
return "gitea-vagrant"
}
Expand Down
88 changes: 88 additions & 0 deletions modules/packages/terraform/metadata.go
Original file line number Diff line number Diff line change
@@ -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 changes: 161 additions & 0 deletions modules/packages/terraform/metadata_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
2 changes: 2 additions & 0 deletions modules/setting/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ var (
LimitSizeRpm int64
LimitSizeRubyGems int64
LimitSizeSwift int64
LimitSizeTerraform int64
LimitSizeVagrant int64

DefaultRPMSignEnabled bool
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions routers/api/packages/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading