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

terraform: accept extra client metadata for ID calculations #79

Merged
merged 2 commits into from
Sep 1, 2022
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
28 changes: 14 additions & 14 deletions pkg/config/externalname.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const (
)

var (
externalNameRegex = regexp.MustCompile(`{{\ *\.externalName\b\ *}}`)
externalNameRegex = regexp.MustCompile(`{{\ *\.external_name\b\ *}}`)
)

var (
Expand Down Expand Up @@ -73,13 +73,13 @@ func ParameterAsIdentifier(param string) ExternalName {
// take a look at the TF registry provider configuration object
// to see what's available. Not to be confused with ProviderConfig
// custom resource of the Crossplane provider.
// externalName: The value of external name annotation of the custom resource.
// external_name: The value of external name annotation of the custom resource.
// It is required to use this as part of the template.
//
// Example usages:
// TemplatedStringAsIdentifier("index_name", "/subscriptions/{{ .terraformProviderConfig.subscription }}/{{ .externalName }}")
// TemplatedStringAsIdentifier("index.name", "/resource/{{ .externalName }}/static")
// TemplatedStringAsIdentifier("index.name", "{{ .parameters.cluster_id }}:{{ .parameters.node_id }}:{{ .externalName }}")
// TemplatedStringAsIdentifier("index_name", "/subscriptions/{{ .terraformProviderConfig.subscription }}/{{ .external_name }}")
// TemplatedStringAsIdentifier("index.name", "/resource/{{ .external_name }}/static")
// TemplatedStringAsIdentifier("index.name", "{{ .parameters.cluster_id }}:{{ .parameters.node_id }}:{{ .external_name }}")
func TemplatedStringAsIdentifier(nameFieldPath, tmpl string) ExternalName {
t, err := template.New("getid").Parse(tmpl)
if err != nil {
Expand All @@ -102,11 +102,11 @@ func TemplatedStringAsIdentifier(nameFieldPath, tmpl string) ExternalName {
nameFieldPath,
nameFieldPath + "_prefix",
},
GetIDFn: func(ctx context.Context, externalName string, parameters map[string]any, terraformProviderConfig map[string]any) (string, error) {
GetIDFn: func(ctx context.Context, externalName string, parameters map[string]any, setup map[string]any) (string, error) {
o := map[string]any{
"externalName": externalName,
"parameters": parameters,
"terraformProviderConfig": terraformProviderConfig,
"external_name": externalName,
"parameters": parameters,
"setup": setup,
}
b := bytes.Buffer{}
if err := t.Execute(&b, o); err != nil {
Expand All @@ -126,7 +126,7 @@ func TemplatedStringAsIdentifier(nameFieldPath, tmpl string) ExternalName {

// GetExternalNameFromTemplated takes a Terraform ID and the template it's produced
// from and reverse it to get the external name. For example, you can supply
// "/subscription/{{ .paramters.some }}/{{ .externalName }}" with
// "/subscription/{{ .paramters.some }}/{{ .external_name }}" with
// "/subscription/someval/myname" and get "myname" returned.
func GetExternalNameFromTemplated(tmpl, val string) (string, error) { //nolint:gocyclo
// gocyclo: I couldn't find any more room.
Expand All @@ -148,13 +148,13 @@ func GetExternalNameFromTemplated(tmpl, val string) (string, error) { //nolint:g
}

switch {
// {{ .externalName }}
// {{ .external_name }}
case leftSeparator == "" && rightSeparator == "":
return val, nil
// {{ .externalName }}/someother
// {{ .external_name }}/someother
case leftSeparator == "" && rightSeparator != "":
return strings.Split(val, rightSeparator)[0], nil
// /another/{{ .externalName }}/someother
// /another/{{ .external_name }}/someother
case leftSeparator != "" && rightSeparator != "":
leftSeparatorCount := strings.Count(tmpl[:leftIndex+1], leftSeparator)
// ["", "another","myname/someother"]
Expand All @@ -163,7 +163,7 @@ func GetExternalNameFromTemplated(tmpl, val string) (string, error) { //nolint:g
rightString := separatedLeft[len(separatedLeft)-1]
// myname
return strings.Split(rightString, rightSeparator)[0], nil
// /another/{{ .externalName }}
// /another/{{ .external_name }}
case leftSeparator != "" && rightSeparator == "":
separated := strings.Split(val, leftSeparator)
return separated[len(separated)-1], nil
Expand Down
52 changes: 27 additions & 25 deletions pkg/config/externalname_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestGetExternalNameFromTemplated(t *testing.T) {
"OnlyExternalName": {
reason: "Should work with bare external name.",
args: args{
tmpl: "{{ .externalName }}",
tmpl: "{{ .external_name }}",
val: "myname",
},
want: want{
Expand All @@ -41,7 +41,7 @@ func TestGetExternalNameFromTemplated(t *testing.T) {
"ExternalNameWithPrefix": {
reason: "Should work with prefixed external names.",
args: args{
tmpl: "/some:other/prefix:{{ .externalName }}",
tmpl: "/some:other/prefix:{{ .external_name }}",
val: "/some:other/prefix:myname",
},
want: want{
Expand All @@ -51,7 +51,7 @@ func TestGetExternalNameFromTemplated(t *testing.T) {
"ExternalNameWithSuffix": {
reason: "Should work with suffixed external name.",
args: args{
tmpl: "{{ .externalName }}/olala:{{ .another }}/ola",
tmpl: "{{ .external_name }}/olala:{{ .another }}/ola",
val: "myname/olala:omama/ola",
},
want: want{
Expand All @@ -61,7 +61,7 @@ func TestGetExternalNameFromTemplated(t *testing.T) {
"ExternalNameInTheMiddle": {
reason: "Should work with external name that is both prefixed and suffixed.",
args: args{
tmpl: "olala:{{ .externalName }}:omama:{{ .someOther }}",
tmpl: "olala:{{ .external_name }}:omama:{{ .someOther }}",
val: "olala:myname:omama:okaka",
},
want: want{
Expand All @@ -72,7 +72,7 @@ func TestGetExternalNameFromTemplated(t *testing.T) {
"ExternalNameInTheMiddleWithLessSpaceInTemplateVar": {
reason: "Should work with external name that is both prefixed and suffixed.",
args: args{
tmpl: "olala:{{.externalName}}:omama:{{ .someOther }}",
tmpl: "olala:{{.external_name}}:omama:{{ .someOther }}",
val: "olala:myname:omama:okaka",
},
want: want{
Expand Down Expand Up @@ -149,7 +149,7 @@ func TestTemplatedSetIdentifierArgumentFn(t *testing.T) {
}
for n, tc := range cases {
t.Run(n, func(t *testing.T) {
TemplatedStringAsIdentifier(tc.args.nameFieldPath, "{{ .externalName }}").SetIdentifierArgumentFn(tc.args.base, tc.args.externalName)
TemplatedStringAsIdentifier(tc.args.nameFieldPath, "{{ .external_name }}").SetIdentifierArgumentFn(tc.args.base, tc.args.externalName)
if diff := cmp.Diff(tc.want.base, tc.args.base); diff != "" {
t.Fatalf("TemplatedStringAsIdentifier.SetIdentifierArgumentFn(...): -want, +got: %s", diff)
}
Expand All @@ -159,10 +159,10 @@ func TestTemplatedSetIdentifierArgumentFn(t *testing.T) {

func TestTemplatedGetIDFn(t *testing.T) {
type args struct {
tmpl string
externalName string
parameters map[string]any
terraformProviderConfig map[string]any
tmpl string
externalName string
parameters map[string]any
setup map[string]any
}
type want struct {
id string
Expand All @@ -174,7 +174,7 @@ func TestTemplatedGetIDFn(t *testing.T) {
want want
}{
"NoExternalName": {
reason: "Should work when only externalName is used.",
reason: "Should work when only external_name is used.",
args: args{
tmpl: "olala/{{ .parameters.somethingElse }}",
parameters: map[string]any{
Expand All @@ -186,9 +186,9 @@ func TestTemplatedGetIDFn(t *testing.T) {
},
},
"OnlyExternalName": {
reason: "Should work when only externalName is used.",
reason: "Should work when only external_name is used.",
args: args{
tmpl: "olala/{{ .externalName }}",
tmpl: "olala/{{ .external_name }}",
externalName: "myname",
},
want: want{
Expand All @@ -198,13 +198,15 @@ func TestTemplatedGetIDFn(t *testing.T) {
"MultipleParameters": {
reason: "Should work when parameters and terraformProviderConfig are used as well.",
args: args{
tmpl: "olala/{{ .parameters.ola }}:{{ .externalName }}/{{ .terraformProviderConfig.oma }}",
tmpl: "olala/{{ .parameters.ola }}:{{ .external_name }}/{{ .setup.configuration.oma }}",
externalName: "myname",
parameters: map[string]any{
"ola": "paramval",
},
terraformProviderConfig: map[string]any{
"oma": "configval",
setup: map[string]any{
"configuration": map[string]any{
"oma": "configval",
},
},
},
want: want{
Expand All @@ -218,7 +220,7 @@ func TestTemplatedGetIDFn(t *testing.T) {
GetIDFn(context.TODO(),
tc.args.externalName,
tc.args.parameters,
tc.args.terraformProviderConfig,
tc.args.setup,
)
if diff := cmp.Diff(tc.want.err, err); diff != "" {
t.Fatalf("TemplatedStringAsIdentifier.GetIDFn(...): -want, +got: %s", diff)
Expand All @@ -245,7 +247,7 @@ func TestTemplatedGetExternalNameFn(t *testing.T) {
want want
}{
"NoExternalName": {
reason: "Should work when no externalName is used.",
reason: "Should work when no external_name is used.",
args: args{
tmpl: "olala/{{ .parameters.somethingElse }}",
tfstate: map[string]any{
Expand All @@ -257,9 +259,9 @@ func TestTemplatedGetExternalNameFn(t *testing.T) {
},
},
"BareExternalName": {
reason: "Should work when only externalName is used in template.",
reason: "Should work when only external_name is used in template.",
args: args{
tmpl: "{{ .externalName }}",
tmpl: "{{ .external_name }}",
tfstate: map[string]any{
"id": "myname",
},
Expand All @@ -269,9 +271,9 @@ func TestTemplatedGetExternalNameFn(t *testing.T) {
},
},
"ExternalNameSpaces": {
reason: "Should work when externalName variable has random space characters.",
reason: "Should work when external_name variable has random space characters.",
args: args{
tmpl: "another/thing:{{ .externalName }}/something",
tmpl: "another/thing:{{ .external_name }}/something",
tfstate: map[string]any{
"id": "another/thing:myname/something",
},
Expand All @@ -281,9 +283,9 @@ func TestTemplatedGetExternalNameFn(t *testing.T) {
},
},
"DifferentLeftRightSeparators": {
reason: "Should work when externalName has different left and right separators.",
reason: "Should work when external_name has different left and right separators.",
args: args{
tmpl: "another/{{ .parameters.another }}:{{ .externalName }}/somethingelse",
tmpl: "another/{{ .parameters.another }}:{{ .external_name }}/somethingelse",
tfstate: map[string]any{
"id": "another/thing:myname/somethingelse",
},
Expand All @@ -295,7 +297,7 @@ func TestTemplatedGetExternalNameFn(t *testing.T) {
"NoID": {
reason: "Should not work when ID cannot be found.",
args: args{
tmpl: "{{ .externalName }}",
tmpl: "{{ .external_name }}",
tfstate: map[string]any{
"another": "myname",
},
Expand Down
2 changes: 1 addition & 1 deletion pkg/terraform/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (fp *FileProducer) WriteTFState(ctx context.Context) error {
for k, v := range fp.observation {
base[k] = v
}
id, err := fp.Config.ExternalName.GetIDFn(ctx, meta.GetExternalName(fp.Resource), fp.parameters, fp.Setup.Configuration)
id, err := fp.Config.ExternalName.GetIDFn(ctx, meta.GetExternalName(fp.Resource), fp.parameters, fp.Setup.Map())
if err != nil {
return errors.Wrap(err, "cannot get id")
}
Expand Down
3 changes: 0 additions & 3 deletions pkg/terraform/files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ func TestWriteMainTF(t *testing.T) {
Version: "1.2.3",
},
Configuration: nil,
Env: nil,
},
},
want: want{
Expand Down Expand Up @@ -193,7 +192,6 @@ func TestWriteMainTF(t *testing.T) {
Version: "1.2.3",
},
Configuration: nil,
Env: nil,
},
},
want: want{
Expand Down Expand Up @@ -226,7 +224,6 @@ func TestWriteMainTF(t *testing.T) {
Version: "1.2.3",
},
Configuration: nil,
Env: nil,
},
},
want: want{
Expand Down
41 changes: 36 additions & 5 deletions pkg/terraform/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ type SetupFn func(ctx context.Context, client client.Client, mg xpresource.Manag

// ProviderRequirement holds values for the Terraform HCL setup requirements
type ProviderRequirement struct {
Source string
// Source of the provider. An example value is "hashicorp/aws".
Source string

// Version of the provider. An example value is "4.0"
Version string
}

Expand All @@ -44,10 +47,39 @@ type ProviderConfiguration map[string]any
// Setup holds values for the Terraform version and setup
// requirements and configuration body
type Setup struct {
Version string
Requirement ProviderRequirement
// Version is the version of Terraform that this workspace would require as
// minimum.
Version string

// Requirement contains the provider requirements of the workspace to work,
// which is mostly the version and source of the provider.
Requirement ProviderRequirement

// Configuration contains the provider configuration parameters of the given
// Terraform provider, such as access token.
Configuration ProviderConfiguration
Env []string

// ClientMetadata contains arbitrary metadata that the provider would like
// to pass but not available as part of Terraform's provider configuration.
// For example, AWS account id is needed for certain ID calculations but is
// not part of the Terraform AWS Provider configuration, so it could be
// made available only by this map.
ClientMetadata map[string]string
}

// Map returns the Setup object in map form. The initial reason was so that
// we don't import the terraform package in places where GetIDFn is overridden
// because it can cause circular dependency.
func (s Setup) Map() map[string]any {
return map[string]any{
"version": s.Version,
"requirement": map[string]string{
"source": s.Requirement.Source,
"version": s.Requirement.Version,
},
"configuration": s.Configuration,
"client_metadata": s.ClientMetadata,
}
}

// WorkspaceStoreOption lets you configure the workspace store.
Expand Down Expand Up @@ -138,7 +170,6 @@ func (ws *WorkspaceStore) Workspace(ctx context.Context, c resource.SecretClient
if xpresource.Ignore(os.IsNotExist, err) != nil {
return nil, errors.Wrap(err, "cannot stat init lock file")
}
w.env = ts.Env
w.env = append(w.env, fmt.Sprintf(fmtEnv, envReattachConfig, attachmentConfig))

// We need to initialize only if the workspace hasn't been initialized yet.
Expand Down