Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding support for defining the backend file content #213

Merged
merged 1 commit into from
Jan 5, 2024
Merged
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
7 changes: 7 additions & 0 deletions apis/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions apis/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions examples/providerconfig-backend-file.yaml
Original file line number Diff line number Diff line change
@@ -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
17 changes: 14 additions & 3 deletions internal/controller/workspace/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -259,6 +261,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 {
Expand All @@ -279,6 +287,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)
Expand Down
47 changes: 47 additions & 0 deletions internal/controller/workspace/workspace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
16 changes: 10 additions & 6 deletions internal/terraform/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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() {
Expand Down
6 changes: 6 additions & 0 deletions package/crds/tf.upbound.io_providerconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading