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

Handle custom decoders as part of setting fields in post_create #13520

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
18 changes: 18 additions & 0 deletions mmv1/api/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,24 @@ func (r Resource) InIdFormat(prop Type) bool {
return slices.Contains(fields, google.Underscore(prop.Name))
}

// Returns true if at least one of the fields in the ID format is computed
func (r Resource) HasComputedIdFormatFields() bool {
idFormatFields := map[string]struct{}{}
for _, f := range r.ExtractIdentifiers(r.GetIdFormat()) {
idFormatFields[f] = struct{}{}
}
for _, p := range r.GettableProperties() {
// Skip fields not in the id format
if _, ok := idFormatFields[google.Underscore(p.Name)]; !ok {
continue
}
if (p.Output || p.DefaultFromApi) && !p.IgnoreRead {
return true
}
}
return false
}

// ====================
// Template Methods
// ====================
Expand Down
132 changes: 132 additions & 0 deletions mmv1/api/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,3 +357,135 @@ func TestMagicianLocation(t *testing.T) {
t.Errorf("Current package is not under %s. Path from magician dir to current dir: %s", RELATIVE_MAGICIAN_LOCATION, relPath)
}
}

func TestHasComputedIdFormatFields(t *testing.T) {
cases := []struct {
name, description string
resource Resource
want bool
}{
{
name: "no properties",
resource: Resource{
IdFormat: "projects/{{project}}/resource/{{resource}}",
},
want: false,
},
{
name: "no computed properties",
resource: Resource{
IdFormat: "projects/{{project}}/resource/{{resource}}",
Properties: []*Type{
{
Name: "resource",
},
},
},
want: false,
},
{
name: "output-only property",
resource: Resource{
IdFormat: "projects/{{project}}/resource/{{resource}}",
Properties: []*Type{
{
Name: "field",
Output: true,
},
},
},
want: false,
},
{
name: "output-only property in id_format",
resource: Resource{
IdFormat: "projects/{{project}}/resource/{{resource}}",
Properties: []*Type{
{
Name: "resource",
Output: true,
},
},
},
want: true,
},
{
name: "output-only property in id_format with ignore_read",
resource: Resource{
IdFormat: "projects/{{project}}/resource/{{resource}}",
Properties: []*Type{
{
Name: "resource",
Output: true,
IgnoreRead: true,
},
},
},
want: false,
},
{
name: "default_from_api property",
resource: Resource{
IdFormat: "projects/{{project}}/resource/{{resource}}",
Properties: []*Type{
{
Name: "field",
DefaultFromApi: true,
},
},
},
want: false,
},
{
name: "default_from_api property in id_format",
resource: Resource{
IdFormat: "projects/{{project}}/resource/{{resource}}",
Properties: []*Type{
{
Name: "resource",
DefaultFromApi: true,
},
},
},
want: true,
},
{
name: "default_from_api property in id_format with ignore_read",
resource: Resource{
IdFormat: "projects/{{project}}/resource/{{resource}}",
Properties: []*Type{
{
Name: "resource",
DefaultFromApi: true,
IgnoreRead: true,
},
},
},
want: false,
},
{
name: "converts prop.name to snake case",
resource: Resource{
IdFormat: "projects/{{project}}/resource/{{resource_id}}",
Properties: []*Type{
{
Name: "resourceId",
Output: true,
},
},
},
want: true,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

got := tc.resource.HasComputedIdFormatFields()
if got != tc.want {
t.Errorf("HasComputedIdFormatFields(%q) returned unexpected value. got %t; want %t.", tc.name, got, tc.want)
}
})
}
}
4 changes: 2 additions & 2 deletions mmv1/products/bigqueryanalyticshub/ListingSubscription.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ immutable: true
import_format:
- 'projects/{{project}}/locations/{{location}}/subscriptions/{{subscription_id}}'
custom_code:
post_create: templates/terraform/post_create/analytics_hub_subscription.go.tmpl
decoder: 'templates/terraform/decoders/bigqueryanalyticshub_listing_subscription.go.tmpl'
pre_read: 'templates/terraform/pre_read/bigqueryanalyticshub_listing_subscription.tmpl'
pre_delete: 'templates/terraform/pre_read/bigqueryanalyticshub_listing_subscription.tmpl'
post_import: templates/terraform/post_import/analytics_hub_subscription.go.tmpl
Expand Down Expand Up @@ -122,7 +122,7 @@ properties:
description: |-
The subscription id used to reference the subscription.
output: true
ignore_read: true
custom_flatten: 'templates/terraform/custom_flatten/id_from_name.tmpl'
- name: 'creationTime'
type: Time
description: |-
Expand Down
5 changes: 5 additions & 0 deletions mmv1/products/monitoring/MonitoredProject.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ timeouts:
insert_minutes: 20
update_minutes: 20
delete_minutes: 20
autogen_async: true
async:
actions: ['create', 'delete']
operation:
base_url: '{{op_id}}'
custom_code:
constants: 'templates/terraform/constants/monitoring_monitored_project.go.tmpl'
encoder: 'templates/terraform/encoders/monitoring_monitored_project.go.tmpl'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// The response from Create nests the resource inside a "subscription" key.
if s, ok := res["subscription"]; ok {
return s.(map[string]interface{}), nil
}
return res, nil

This file was deleted.

41 changes: 28 additions & 13 deletions mmv1/templates/terraform/resource.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -288,32 +288,48 @@ func resource{{ $.ResourceName -}}Create(d *schema.ResourceData, meta interface{
{{- end}}
return fmt.Errorf("Error creating {{ $.Name -}}: %s", err)
}
{{- /* Set output-only resource properties from create API response (as long as Create doesn't use an async operation) */}}
{{- /* This is necessary so that the ID is built correctly. */}}
{{- if or (not $.GetAsync) (not (and ($.GetAsync.IsA "OpAsync") ($.GetAsync.Allow "Create"))) }}
{{- /* Set computed resource properties required for building the ID from create API response (as long as Create doesn't use an async operation) */}}
{{- /* This is necessary so that the ID is set correctly (and so that the following Read can succeed.) */}}
{{- /* Technically this should possibly use the read URL explicitly, since id_format could differ - but that might need to be in addition to id_format anyway. */}}
{{- if and $.HasComputedIdFormatFields (or (or (not $.GetAsync) (not ($.GetAsync.Allow "Create"))) (and $.GetAsync (and ($.GetAsync.IsA "PollAsync") ($.GetAsync.Allow "Create"))))}}
// Set computed resource properties from create API response so that they're available on the subsequent Read
// call.
{{- /* Don't render decoder for PollAsync resources - their decoders are expected to return `nil` until the resource completion completes, but we need to set their computed fields in order to call PollRead - so there can never be a dependency on the decoder. */}}
{{- if and $.CustomCode.Decoder (or (not $.GetAsync) (not ($.GetAsync.IsA "PollAsync"))) }}
res, err = resource{{ $.ResourceName -}}Decoder(d, meta, res)
if err != nil {
return fmt.Errorf("decoding response: %w", err)
}
if res == nil {
return fmt.Errorf("decoding response, could not find object")
}
{{- end }}
{{- $renderedIdFromName := "false" }}
{{- range $prop := $.GettableProperties }}
{{- if $.InIdFormat $prop }}
{{- if eq $prop.CustomFlatten "templates/terraform/custom_flatten/id_from_name.tmpl" }}
{{- /* Check if prop is potentially computed */}}
{{- if and ($.InIdFormat $prop) (and (or $prop.Output $prop.DefaultFromApi) (not $prop.IgnoreRead)) }}
{{- if and (eq $prop.CustomFlatten "templates/terraform/custom_flatten/id_from_name.tmpl") (eq $renderedIdFromName "false") }}
// Setting `name` field so that `id_from_name` flattener will work properly.
if err := d.Set("name", flatten{{ if $.NestedQuery -}}Nested{{end}}{{ $.ResourceName -}}Name(res["name"], d, config)); err != nil {
return fmt.Errorf(`Error setting computed identity field "name": %s`, err)
}
{{- end }}
{{- if and $prop.Output (not $prop.IgnoreRead) }}
{{- $renderedIdFromName = "true" }}
{{- end }}
{{- if and $prop.Output (not $prop.IgnoreRead) }}
if err := d.Set("{{ underscore $prop.Name -}}", flatten{{ if $.NestedQuery -}}Nested{{end}}{{ $.ResourceName -}}{{ camelize $prop.Name "upper" -}}(res["{{ $prop.ApiName -}}"], d, config)); err != nil {
return fmt.Errorf(`Error setting computed identity field "{{ underscore $prop.Name }}": %s`, err)
}
{{- else if and $prop.DefaultFromApi (not $prop.IgnoreRead) }}
{{- else if and $prop.DefaultFromApi (not $prop.IgnoreRead) }}
// {{ underscore $prop.Name }} is set by API when unset
if tpgresource.IsEmptyValue(reflect.ValueOf(d.Get("{{ underscore $prop.Name }}"))) {
if err := d.Set("{{ underscore $prop.Name -}}", flatten{{ if $.NestedQuery -}}Nested{{end}}{{ $.ResourceName -}}{{ camelize $prop.Name "upper" -}}(res["{{ $prop.ApiName -}}"], d, config)); err != nil {
return fmt.Errorf(`Error setting computed identity field "{{ underscore $prop.Name }}": %s`, err)
}
}
{{- end }}
{{- end}}
{{- end}}
{{- end}}
{{- end }}
{{- end }}{{/* prop is potentially computed */}}
{{- end }}{{/* range */}}
{{- end}}{{/* Set computed resource properties... */}}

// Store the ID now
id, err := tpgresource.ReplaceVars{{if $.LegacyLongFormProject -}}ForId{{ end -}}(d, config, "{{ $.IdFormat -}}")
Expand Down Expand Up @@ -420,7 +436,6 @@ func resource{{ $.ResourceName -}}Create(d *schema.ResourceData, meta interface{
return fmt.Errorf("Error waiting to create {{ $.Name -}}: %s", err)
{{- end}}
}

{{- end}}
{{- end}}

Expand Down
Loading