diff --git a/apis/v1beta1/types.go b/apis/v1beta1/types.go index 9ea0daa..3a3a631 100644 --- a/apis/v1beta1/types.go +++ b/apis/v1beta1/types.go @@ -34,6 +34,13 @@ type ProviderConfigSpec struct { // +optional Configuration *string `json:"configuration,omitempty"` + // Terraform backend file configuration content, + // it has the contents of the backend block as top-level attributes, + // without the need to wrap it in another terraform or backend block. + // More details at https://developer.hashicorp.com/terraform/language/settings/backends/configuration#file. + // +optional + BackendFile *string `json:"backendFile,omitempty"` + // PluginCache enables terraform provider plugin caching mechanism // https://developer.hashicorp.com/terraform/cli/config/config-file#provider-plugin-cache // +optional diff --git a/apis/v1beta1/zz_generated.deepcopy.go b/apis/v1beta1/zz_generated.deepcopy.go index c1beff0..38d741c 100644 --- a/apis/v1beta1/zz_generated.deepcopy.go +++ b/apis/v1beta1/zz_generated.deepcopy.go @@ -114,6 +114,11 @@ func (in *ProviderConfigSpec) DeepCopyInto(out *ProviderConfigSpec) { *out = new(string) **out = **in } + if in.BackendFile != nil { + in, out := &in.BackendFile, &out.BackendFile + *out = new(string) + **out = **in + } if in.PluginCache != nil { in, out := &in.PluginCache, &out.PluginCache *out = new(bool) diff --git a/examples/providerconfig-backend-file.yaml b/examples/providerconfig-backend-file.yaml new file mode 100644 index 0000000..cbc0b58 --- /dev/null +++ b/examples/providerconfig-backend-file.yaml @@ -0,0 +1,36 @@ +apiVersion: tf.upbound.io/v1beta1 +kind: ProviderConfig +metadata: + name: default +spec: + # Note that unlike most provider configs this one supports an array of + # credentials. This is because each Terraform workspace uses a single + # Crossplane provider config, but could use multiple Terraform providers each + # with their own credentials. + credentials: + - filename: gcp-credentials.json + source: Secret + secretRef: + namespace: upbound-system + name: gcp-creds + key: credentials + # This optional configuration block can be used to inject HCL into any + # workspace that uses this provider config, for example to setup Terraform + # providers. + configuration: | + provider "google" { + credentials = "gcp-credentials.json" + project = "official-provider-testing" + } + + // Defining partial backend configuration as documented at + // https://developer.hashicorp.com/terraform/language/settings/backends/configuration#partial-configuration + terraform { + backend "kubernetes" {} + } + # Using backend configuration file as documented at + # https://developer.hashicorp.com/terraform/language/settings/backends/configuration#file + backendFile: | + secret_suffix = "providerconfig-default" + namespace = "upbound-system" + in_cluster_config = true \ No newline at end of file diff --git a/internal/controller/workspace/workspace.go b/internal/controller/workspace/workspace.go index 5c07d61..9829f86 100644 --- a/internal/controller/workspace/workspace.go +++ b/internal/controller/workspace/workspace.go @@ -63,6 +63,7 @@ const ( errWriteGitCreds = "cannot write .git-credentials to /tmp dir" errWriteConfig = "cannot write Terraform configuration " + tfConfig errWriteMain = "cannot write Terraform configuration " + tfMain + errWriteBackend = "cannot write Terraform configuration " + tfBackendFile errInit = "cannot initialize Terraform configuration" errWorkspace = "cannot select Terraform workspace" errResources = "cannot list Terraform resources" @@ -81,9 +82,10 @@ const ( const ( // TODO(negz): Make the Terraform binary path and work dir configurable. - tfPath = "terraform" - tfMain = "main.tf" - tfConfig = "crossplane-provider-config.tf" + tfPath = "terraform" + tfMain = "main.tf" + tfConfig = "crossplane-provider-config.tf" + tfBackendFile = "crossplane.remote.tfbackend" ) func envVarFallback(envvar string, fallback string) string { @@ -268,6 +270,12 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E } } + if pc.Spec.BackendFile != nil { + if err := c.fs.WriteFile(filepath.Join(dir, tfBackendFile), []byte(*pc.Spec.BackendFile), 0600); err != nil { + return nil, errors.Wrap(err, errWriteBackend) + } + } + // NOTE(ytsarev): user tf provider cache mechanism to speed up // reconciliation, see https://developer.hashicorp.com/terraform/cli/config/config-file#provider-plugin-cache if pc.Spec.PluginCache == nil { @@ -288,6 +296,9 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E } o := make([]terraform.InitOption, 0, len(cr.Spec.ForProvider.InitArgs)) + if pc.Spec.BackendFile != nil { + o = append(o, terraform.WithInitArgs([]string{"-backend-config=" + filepath.Join(dir, tfBackendFile)})) + } o = append(o, terraform.WithInitArgs(cr.Spec.ForProvider.InitArgs)) if err := tf.Init(ctx, *pc.Spec.PluginCache, o...); err != nil { return nil, errors.Wrap(err, errInit) diff --git a/internal/controller/workspace/workspace_test.go b/internal/controller/workspace/workspace_test.go index 3bdf368..3068e1b 100644 --- a/internal/controller/workspace/workspace_test.go +++ b/internal/controller/workspace/workspace_test.go @@ -672,6 +672,53 @@ func TestConnect(t *testing.T) { }, want: nil, }, + "SuccessUsingBackendFile": { + reason: "We should not return an error when we successfully 'connect' to Terraform using a Backend file", + fields: fields{ + kube: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + if pc, ok := obj.(*v1beta1.ProviderConfig); ok { + cfg := "I'm HCL!" + backendFile := "I'm a backend!" + pc.Spec.Configuration = &cfg + pc.Spec.BackendFile = &backendFile + } + return nil + }), + }, + usage: resource.TrackerFn(func(_ context.Context, _ resource.Managed) error { return nil }), + fs: afero.Afero{Fs: afero.NewMemMapFs()}, + terraform: func(_ string) tfclient { + return &MockTf{ + MockInit: func(ctx context.Context, cache bool, o ...terraform.InitOption) error { + args := terraform.InitArgsToString(o) + if len(args) != 2 { + return errors.New("two init args are expected") + } else if args[0] != "-backend-config=/tf/no-you-id/crossplane.remote.tfbackend" { + return errors.Errorf("backend config arg has invalid value: %s", args[0]) + } + return nil + }, + MockGenerateChecksum: func(ctx context.Context) (string, error) { return tfChecksum, nil }, + MockWorkspace: func(_ context.Context, _ string) error { return nil }, + } + }, + }, + args: args{ + mg: &v1beta1.Workspace{ + ObjectMeta: metav1.ObjectMeta{UID: uid}, + Spec: v1beta1.WorkspaceSpec{ + ForProvider: v1beta1.WorkspaceParameters{ + InitArgs: []string{"-upgrade=true"}, + }, + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{}, + }, + }, + }, + }, + want: nil, + }, } for name, tc := range cases { diff --git a/internal/terraform/terraform.go b/internal/terraform/terraform.go index 750ac06..41f6b51 100644 --- a/internal/terraform/terraform.go +++ b/internal/terraform/terraform.go @@ -148,6 +148,15 @@ func WithInitArgs(v []string) InitOption { } } +// InitArgsToString converts Terraform init arguments to a list of strings. +func InitArgsToString(o []InitOption) []string { + io := &initOptions{} + for _, fn := range o { + fn(io) + } + return io.args +} + // RWMutex protects the terraform shared cache from corruption. If an init is // performed, it requires a write lock. Only one write lock at a time. If another // action is performed, a read lock is acquired. More than one read locks can be acquired. @@ -157,12 +166,7 @@ var rwmutex = &sync.RWMutex{} // Init initializes a Terraform configuration. func (h Harness) Init(ctx context.Context, cache bool, o ...InitOption) error { - io := &initOptions{} - for _, fn := range o { - fn(io) - } - - args := append([]string{"init", "-input=false", "-no-color"}, io.args...) + args := append([]string{"init", "-input=false", "-no-color"}, InitArgsToString(o)...) cmd := exec.CommandContext(ctx, h.Path, args...) //nolint:gosec cmd.Dir = h.Dir for _, e := range os.Environ() { diff --git a/package/crds/tf.upbound.io_providerconfigs.yaml b/package/crds/tf.upbound.io_providerconfigs.yaml index 82bec67..a67efe3 100644 --- a/package/crds/tf.upbound.io_providerconfigs.yaml +++ b/package/crds/tf.upbound.io_providerconfigs.yaml @@ -46,6 +46,12 @@ spec: spec: description: A ProviderConfigSpec defines the desired state of a ProviderConfig. properties: + backendFile: + description: Terraform backend file configuration content, it has + the contents of the backend block as top-level attributes, without + the need to wrap it in another terraform or backend block. More + details at https://developer.hashicorp.com/terraform/language/settings/backends/configuration#file. + type: string configuration: description: Configuration that should be injected into all workspaces that use this provider config, expressed as inline HCL. This can