diff --git a/README.md b/README.md index 39201d4d..9fa25b3d 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,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) @@ -735,6 +736,36 @@ 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_host}/api/v4/projects/{project_id}/terraform/state/{state_name}/RESOURCE_NAME[?gitlab_user=GITLAB_USER&gitlab_token=GITLAB_TOKEN]` + +* `gitlab_user` defaults to value of `GITLAB_USER` envvar. +* `gitlab_token` defaults to value of `GITLAB_TOKEN` envvar. + +Examples: + +- `ref+tfstategitlab://gitlab.com/api/v4/projects/123/terraform/state/my-state/aws_vpc.main.id` +- `ref+tfstategitlab://my-gitlab.com/api/v4/projects/xx/terraform/state/xxx/output.my_output` + +It allows to use Terraform state stored in GitLab given GitLab host, project ID and state name. You can try to read state with command: + +``` +$ tfstate-lookup -s https://username:password@my-gitlab.com/api/v4/projects/xx/terraform/state/xxx/ output.my_output +``` + +which is equivalent to following input for `vals` (with environment variables `GITLAB_USER` and `GITLAB_TOKEN`): + +``` +$ echo 'foo: ref+tfstategitlab://my-gitlab.com/api/v4/projects/xx/terraform/state/xxx/output.my_output' | vals eval -f - +``` + +Or you can provide credentials via URL parameters: + +``` +$ echo 'foo: ref+tfstategitlab://my-gitlab.com/api/v4/projects/xx/terraform/state/xxx/output.my_output?gitlab_user=username&gitlab_token=token' | vals eval -f - +``` + ### 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]` diff --git a/pkg/providers/tfstate/tfstate.go b/pkg/providers/tfstate/tfstate.go index 9e438b34..4e64fea2 100644 --- a/pkg/providers/tfstate/tfstate.go +++ b/pkg/providers/tfstate/tfstate.go @@ -3,6 +3,7 @@ package tfstate import ( "context" "fmt" + "net/url" "os" "strings" "sync" @@ -94,6 +95,31 @@ func (p *provider) ReadTFState(f, k string) (*tfstate.TFState, error) { return nil, fmt.Errorf("reading tfstate for %s: %w", k, err) } return state, nil + case "gitlab": + stateURL := f + if !strings.HasPrefix(stateURL, "http://") && !strings.HasPrefix(stateURL, "https://") { + stateURL = "https://" + f + } + + user := os.Getenv("GITLAB_USER") + token := os.Getenv("GITLAB_TOKEN") + + parsedURL, err := url.Parse(stateURL) + if err != nil { + return nil, fmt.Errorf("parsing GitLab URL: %w", err) + } + + if user != "" && token != "" { + parsedURL.User = url.UserPassword(user, token) + } else if token != "" { + parsedURL.User = url.UserPassword(token, "") + } + + state, err := tfstate.ReadURL(context.TODO(), parsedURL.String()) + if err != nil { + return nil, fmt.Errorf("reading tfstate for %s: %w", k, err) + } + return state, nil default: url := p.backend + "://" + f state, err := tfstate.ReadURL(context.TODO(), url) diff --git a/pkg/providers/tfstate/tfstate_test.go b/pkg/providers/tfstate/tfstate_test.go new file mode 100644 index 00000000..34d24763 --- /dev/null +++ b/pkg/providers/tfstate/tfstate_test.go @@ -0,0 +1,64 @@ +package tfstate + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/helmfile/vals/pkg/config" +) + +func TestNewGitLabProvider(t *testing.T) { + tests := []struct { + cfg map[string]interface{} + expected *provider + name string + backend string + }{ + { + cfg: map[string]interface{}{}, + expected: &provider{ + backend: "gitlab", + }, + name: "default config", + backend: "gitlab", + }, + { + cfg: map[string]interface{}{ + "gitlab_user": "testuser", + "gitlab_token": "testtoken", + }, + expected: &provider{ + backend: "gitlab", + }, + name: "with gitlab_user and gitlab_token", + backend: "gitlab", + }, + { + cfg: map[string]interface{}{}, + expected: &provider{ + backend: "s3", + }, + name: "s3 backend", + backend: "s3", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := config.MapConfig{M: tt.cfg} + p := New(cfg, tt.backend) + + assert.Equal(t, tt.expected.backend, p.backend) + }) + } +} + +func TestProvider_GetStringMap(t *testing.T) { + cfg := config.MapConfig{M: map[string]interface{}{}} + p := New(cfg, "gitlab") + + _, err := p.GetStringMap("test/path") + assert.Error(t, err) + assert.Contains(t, err.Error(), "path fragment is not supported for tfstate provider") +} diff --git a/pkg/stringprovider/stringprovider.go b/pkg/stringprovider/stringprovider.go index 5cc6815b..6a56cc9e 100644 --- a/pkg/stringprovider/stringprovider.go +++ b/pkg/stringprovider/stringprovider.go @@ -61,6 +61,8 @@ func New(l *log.Logger, provider api.StaticConfig, awsLogLevel string) (api.Lazy 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": diff --git a/vals.go b/vals.go index ef3a644c..d32e1104 100644 --- a/vals.go +++ b/vals.go @@ -98,6 +98,7 @@ const ( ProviderTFStateS3 = "tfstates3" ProviderTFStateAzureRM = "tfstateazurerm" ProviderTFStateRemote = "tfstateremote" + ProviderTFStateGitLab = "tfstategitlab" ProviderAzureKeyVault = "azurekeyvault" ProviderEnvSubst = "envsubst" ProviderKeychain = "keychain" @@ -256,6 +257,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