Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ Please see the [relevant unit test cases](https://github.com/helmfile/vals/blob/
- [Terraform in S3 bucket (tfstates3)](#terraform-in-s3-bucket-tfstates3)
- [Terraform in AzureRM Blob storage (tfstateazurerm)](#terraform-in-azurerm-blob-storage-tfstateazurerm)
- [Terraform in Terraform Cloud / Terraform Enterprise (tfstateremote)](#terraform-in-terraform-cloud--terraform-enterprise-tfstateremote)
- [Terraform in GitLab (tfstategitlab)](#terraform-in-gitlab-tfstategitlab)
- [SOPS](#sops)
- [Keychain](#keychain)
- [Echo](#echo)
Expand Down Expand Up @@ -607,6 +608,37 @@ which is equivalent to the following input for `vals`:
$ echo 'foo: ref+tfstateremote://app.terraform.io/myorg/myworkspace/output.virtual_network.name' | vals eval -f -
```

### Terraform in GitLab (tfstategitlab)

- `ref+tfstategitlab://gitlab.com/api/v4/projects/{project_id}/terraform/state/{state_name}/RESOURCE_NAME`

Examples:

- `ref+tfstategitlab://gitlab.com/api/v4/projects/123/terraform/state/default/aws_vpc.main.id`
- `ref+tfstategitlab://my-gitlab.example.com/api/v4/projects/456/terraform/state/production/output.database_url`

It allows to use Terraform state stored in GitLab using the GitLab Terraform state API. You can try to read the state with command (with exported variable `GITLAB_TOKEN` or `TFE_TOKEN`):

```
$ tfstate-lookup -s https://gitlab.com/api/v4/projects/123/terraform/state/default aws_vpc.main.id
```

which is equivalent to the following input for `vals`:

```
$ echo 'foo: ref+tfstategitlab://gitlab.com/api/v4/projects/123/terraform/state/default/aws_vpc.main.id' | vals eval -f -
```

#### Authentication

The provider supports several authentication methods:

- **GitLab Private Token**: Set `GITLAB_TOKEN` environment variable with your GitLab personal access token
- **TFE Token**: Set `TFE_TOKEN` environment variable (for compatibility with Terraform Enterprise workflows)
- **Basic Authentication**: Set both `TFE_USER` and `TFE_TOKEN` environment variables for HTTP basic authentication

The GitLab project must have the Terraform state feature enabled and the token must have sufficient permissions to read the Terraform state.

### SOPS

- The whole content of a SOPS-encrypted file: `ref+sops://base64_data_or_path_to_file?key_type=[filepath|base64]&format=[binary|dotenv|yaml]`
Expand Down
116 changes: 116 additions & 0 deletions pkg/providers/tfstate/tfstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package tfstate
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"sync"
Expand All @@ -16,13 +19,17 @@ type provider struct {
backend string
awsProfile string
azSubscriptionId string
gitlabToken string
tfeToken string
}

func New(cfg api.StaticConfig, backend string) *provider {
p := &provider{}
p.backend = backend
p.awsProfile = cfg.String("aws_profile")
p.azSubscriptionId = cfg.String("az_subscription_id")
p.gitlabToken = cfg.String("gitlab_token")
p.tfeToken = cfg.String("tfe_token")
return p
}

Expand Down Expand Up @@ -59,6 +66,58 @@ var (
tfstateMu sync.Mutex
)

// readGitLabHTTP reads Terraform state from GitLab HTTP API with authentication
func (p *provider) readGitLabHTTP(ctx context.Context, url string) (io.ReadCloser, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}

// Try to get GitLab token from provider config, then from environment variables
token := p.gitlabToken
if token == "" {
token = os.Getenv("GITLAB_TOKEN")
}
if token == "" {
token = os.Getenv("TFE_TOKEN")
}

// Check for TFE_USER for basic authentication
tfeUser := os.Getenv("TFE_USER")

if token != "" {
if tfeUser != "" {
// Use basic authentication with TFE_USER and TFE_TOKEN
req.SetBasicAuth(tfeUser, token)
} else {
// Use GitLab private token authentication
req.Header.Set("PRIVATE-TOKEN", token)
}
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
return resp.Body, nil
}

// isGitLabURL checks if the URL is a GitLab Terraform state API URL
func (p *provider) isGitLabURL(urlStr string) bool {
// Check if it's an HTTP/HTTPS URL with GitLab API pattern
u, err := url.Parse(urlStr)
if err != nil {
return false
}

// Check for GitLab API pattern in path
// GitLab Terraform state API URLs typically look like:
// https://gitlab.com/api/v4/projects/PROJECT_ID/terraform/state/STATE_NAME
return (u.Scheme == "http" || u.Scheme == "https") &&
strings.Contains(u.Path, "/api/v4/projects/") &&
strings.Contains(u.Path, "/terraform/state/")
}

// Read state either from file or from backend
func (p *provider) ReadTFState(f, k string) (*tfstate.TFState, error) {
tfstateMu.Lock()
Expand Down Expand Up @@ -87,15 +146,72 @@ func (p *provider) ReadTFState(f, k string) (*tfstate.TFState, error) {
}()
}

// Allow setting GitLab token via provider config
if p.gitlabToken != "" {
v := os.Getenv("GITLAB_TOKEN")
err := os.Setenv("GITLAB_TOKEN", p.gitlabToken)
if err != nil {
return nil, fmt.Errorf("setting GITLAB_TOKEN envvar: %w", err)
}
defer func() {
_ = os.Setenv("GITLAB_TOKEN", v)
}()
}

// Allow setting TFE token via provider config
if p.tfeToken != "" {
v := os.Getenv("TFE_TOKEN")
err := os.Setenv("TFE_TOKEN", p.tfeToken)
if err != nil {
return nil, fmt.Errorf("setting TFE_TOKEN envvar: %w", err)
}
defer func() {
_ = os.Setenv("TFE_TOKEN", v)
}()
}

switch p.backend {
case "":
state, err := tfstate.ReadFile(context.TODO(), f)
if err != nil {
return nil, fmt.Errorf("reading tfstate for %s: %w", k, err)
}
return state, nil
case "gitlab":
// For GitLab, f contains the full URL path
fullURL := "https://" + f
if p.isGitLabURL(fullURL) {
// Read GitLab state with authentication
src, err := p.readGitLabHTTP(context.TODO(), fullURL)
if err != nil {
return nil, fmt.Errorf("reading GitLab tfstate for %s: %w", k, err)
}
defer func() {
_ = src.Close()
}()
return tfstate.Read(context.TODO(), src)
}
// Fall back to regular HTTP reading
state, err := tfstate.ReadURL(context.TODO(), fullURL)
if err != nil {
return nil, fmt.Errorf("reading tfstate for %s: %w", k, err)
}
return state, nil
default:
url := p.backend + "://" + f

// Check if this is a GitLab URL even for other backends (like remote)
if p.isGitLabURL(url) {
src, err := p.readGitLabHTTP(context.TODO(), url)
if err != nil {
return nil, fmt.Errorf("reading GitLab tfstate for %s: %w", k, err)
}
defer func() {
_ = src.Close()
}()
return tfstate.Read(context.TODO(), src)
}

state, err := tfstate.ReadURL(context.TODO(), url)
if err != nil {
return nil, fmt.Errorf("reading tfstate for %s: %w", k, err)
Expand Down
2 changes: 2 additions & 0 deletions pkg/stringprovider/stringprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ func New(l *log.Logger, provider api.StaticConfig) (api.LazyLoadedStringProvider
return tfstate.New(provider, "azurerm"), nil
case "tfstateremote":
return tfstate.New(provider, "remote"), nil
case "tfstategitlab":
return tfstate.New(provider, "gitlab"), nil
case "azurekeyvault":
return azurekeyvault.New(provider), nil
case "gitlab":
Expand Down
4 changes: 4 additions & 0 deletions vals.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const (
ProviderTFStateS3 = "tfstates3"
ProviderTFStateAzureRM = "tfstateazurerm"
ProviderTFStateRemote = "tfstateremote"
ProviderTFStateGitLab = "tfstategitlab"
ProviderAzureKeyVault = "azurekeyvault"
ProviderEnvSubst = "envsubst"
ProviderKeychain = "keychain"
Expand Down Expand Up @@ -240,6 +241,9 @@ func (r *Runtime) prepare() (*expansion.ExpandRegexMatch, error) {
case ProviderTFStateRemote:
p := tfstate.New(conf, "remote")
return p, nil
case ProviderTFStateGitLab:
p := tfstate.New(conf, "gitlab")
return p, nil
case ProviderAzureKeyVault:
p := azurekeyvault.New(conf)
return p, nil
Expand Down