From bed215876f5efd42ba97b9ebf60d76b790466774 Mon Sep 17 00:00:00 2001 From: Piotr Roszatycki Date: Mon, 3 May 2021 03:43:28 +0200 Subject: [PATCH] New schemas tfstates3 and tfstategs (#49) Adds new schemes that can allow uses of direct path to Terraform state in the bucket: one for GCS another for S3. The change requires #47 to be applied first as it supports GCS buckets implemented by tfstate-lookup v0.2.0 Resolves #45 --- README.md | 42 ++++++++++++++++++++++++++++ pkg/providers/tfstate/tfstate.go | 27 ++++++++++++++++-- pkg/stringprovider/stringprovider.go | 6 +++- vals.go | 10 ++++++- 4 files changed, 80 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5cc8717..b5f2c28 100644 --- a/README.md +++ b/README.md @@ -335,6 +335,48 @@ Remote backends like S3 or GCS are also supported. When a remote backend is used Just specify the path to that file, so that `vals` is able to transparently make the remote state contents available for you. +### Terraform in GCS bucket (tfstategs) + +- `ref+tfstategs://bucket/path/to/some.tfstate/RESOURCE_NAME` + +Examples: + +- `ref+tfstategs://bucket/path/to/some.tfstate/google_compute_disk.instance.id` + +It allows to use Terraform state stored in GCS bucket with the direct URL to it. You can try to read the state with command: + +``` +$ tfstate-lookup -s gs://bucket-with-terraform-state/terraform.tfstate google_compute_disk.instance.source_image_id +5449927740744213880 +``` + +which is equivalent to the following input for `vals`: + +``` +$ echo 'foo: ref+tfstategs://bucket-with-terraform-state/terraform.tfstate/google_compute_disk.instance.source_image_id' | vals eval -f - +``` + +### Terraform in S3 bucket (tfstates3) + +- `ref+tfstates3://bucket/path/to/some.tfstate/RESOURCE_NAME` + +Examples: + +- `ref+tfstates3://bucket/path/to/some.tfstate/aws_vpc.main.id` + +It allows to use Terraform state stored in AWS S3 bucket with the direct URL to it. You can try to read the state with command: + +``` +$ tfstate-lookup -s s3://bucket-with-terraform-state/terraform.tfstate module.vpc.aws_vpc.this[0].arn +arn:aws:ec2:us-east-2:ACCOUNT_ID:vpc/vpc-0cb48a12e4df7ad4c +``` + +which is equivalent to the following input for `vals`: + +``` +$ echo 'foo: ref+tfstates3://bucket-with-terraform-state/terraform.tfstate/module.vpc.aws_vpc.this[0].arn' | 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 abd7cf0..691a382 100644 --- a/pkg/providers/tfstate/tfstate.go +++ b/pkg/providers/tfstate/tfstate.go @@ -11,10 +11,12 @@ import ( ) type provider struct { + backend string } -func New(cfg api.StaticConfig) *provider { +func New(cfg api.StaticConfig, backend string) *provider { p := &provider{} + p.backend = backend return p } @@ -27,9 +29,9 @@ func (p *provider) GetString(key string) (string, error) { f := strings.Join(splits[:pos], string(os.PathSeparator)) k := strings.Join(splits[pos:], string(os.PathSeparator)) - state, err := tfstate.ReadFile(f) + state, err := p.ReadTFState(f, k) if err != nil { - return "", fmt.Errorf("reading tfstate for %s: %w", key, err) + return "", err } // key is something like "aws_vpc.main.id" (RESOURCE_TYPE.RESOURCE_NAME.FIELD) @@ -42,6 +44,25 @@ func (p *provider) GetString(key string) (string, error) { return attrs.String(), nil } +// Read state either from file or from backend +func (p *provider) ReadTFState(f, k string) (*tfstate.TFState, error) { + switch p.backend { + case "": + state, err := tfstate.ReadFile(f) + 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(url) + if err != nil { + return nil, fmt.Errorf("reading tfstate for %s: %w", k, err) + } + return state, nil + } +} + func (p *provider) GetStringMap(key string) (map[string]interface{}, error) { return nil, fmt.Errorf("path fragment is not supported for tfstate provider") } diff --git a/pkg/stringprovider/stringprovider.go b/pkg/stringprovider/stringprovider.go index 4633115..98321b9 100644 --- a/pkg/stringprovider/stringprovider.go +++ b/pkg/stringprovider/stringprovider.go @@ -31,7 +31,11 @@ func New(provider api.StaticConfig) (api.LazyLoadedStringProvider, error) { case "gcpsecrets": return gcpsecrets.New(provider), nil case "tfstate": - return tfstate.New(provider), nil + return tfstate.New(provider, ""), nil + case "tfstategs": + return tfstate.New(provider, "gs"), nil + case "tfstates3": + return tfstate.New(provider, "s3"), nil case "azurekeyvault": return azurekeyvault.New(provider), nil } diff --git a/vals.go b/vals.go index 2130774..ef8e806 100644 --- a/vals.go +++ b/vals.go @@ -61,6 +61,8 @@ const ( ProviderFile = "file" ProviderGCPSecretManager = "gcpsecrets" ProviderTFState = "tfstate" + ProviderTFStateGS = "tfstategs" + ProviderTFStateS3 = "tfstates3" ProviderAzureKeyVault = "azurekeyvault" ) @@ -158,7 +160,13 @@ func (r *Runtime) Eval(template map[string]interface{}) (map[string]interface{}, p := gcpsecrets.New(conf) return p, nil case ProviderTFState: - p := tfstate.New(conf) + p := tfstate.New(conf, "") + return p, nil + case ProviderTFStateGS: + p := tfstate.New(conf, "gs") + return p, nil + case ProviderTFStateS3: + p := tfstate.New(conf, "s3") return p, nil case ProviderAzureKeyVault: p := azurekeyvault.New(conf)