diff --git a/mmv1/google/string_utils.go b/mmv1/google/string_utils.go index 57786ba96952..daeaf56baaf6 100644 --- a/mmv1/google/string_utils.go +++ b/mmv1/google/string_utils.go @@ -137,3 +137,34 @@ func Camelize(term string, firstLetter string) string { }) return res } + +/* +Transforms a format string with field markers to a regex string with capture groups. +For instance, + + projects/{{project}}/global/networks/{{name}} + +is transformed to + + projects/(?P[^/]+)/global/networks/(?P[^/]+) + +Values marked with % are URL-encoded, and will match any number of /'s. +Note: ?P indicates a Python-compatible named capture group. Named groups +aren't common in JS-based regex flavours, but are in Perl-based ones +*/ +func Format2Regex(format string) string { + re := regexp.MustCompile(`\{\{%([[:word:]]+)\}\}`) + result := re.ReplaceAllStringFunc(format, func(match string) string { + // TODO: the trims may not be needed with more effecient regex + word := strings.TrimPrefix(match, "{{") + word = strings.TrimSuffix(word, "}}") + return fmt.Sprintf("(?P<%s>.+)", word) + }) + re = regexp.MustCompile(`\{\{([[:word:]]+)\}\}`) + result = re.ReplaceAllStringFunc(result, func(match string) string { + word := strings.TrimPrefix(match, "{{") + word = strings.TrimSuffix(word, "}}") + return fmt.Sprintf("(?P<%s>[^/]+)", word) + }) + return result +} diff --git a/mmv1/provider/template_data.go b/mmv1/provider/template_data.go index 4dd8a2126c4d..4d038f93b978 100644 --- a/mmv1/provider/template_data.go +++ b/mmv1/provider/template_data.go @@ -62,16 +62,17 @@ func wrapMultipleParams(params ...interface{}) (map[string]interface{}, error) { } var TemplateFunctions = template.FuncMap{ - "title": google.SpaceSeparatedTitle, - "replace": strings.Replace, - "camelize": google.Camelize, - "underscore": google.Underscore, - "plural": google.Plural, - "contains": strings.Contains, - "join": strings.Join, - "lower": strings.ToLower, - "upper": strings.ToUpper, - "dict": wrapMultipleParams, + "title": google.SpaceSeparatedTitle, + "replace": strings.Replace, + "camelize": google.Camelize, + "underscore": google.Underscore, + "plural": google.Plural, + "contains": strings.Contains, + "join": strings.Join, + "lower": strings.ToLower, + "upper": strings.ToUpper, + "dict": wrapMultipleParams, + "format2regex": google.Format2Regex, } var GA_VERSION = "ga" diff --git a/mmv1/provider/terraform.go b/mmv1/provider/terraform.go index 7a08685141c7..ac36423cd9f2 100644 --- a/mmv1/provider/terraform.go +++ b/mmv1/provider/terraform.go @@ -864,26 +864,6 @@ func (t Terraform) replaceImportPath(outputFolder, target string) { // // end // -// # Transforms a format string with field markers to a regex string with -// # capture groups. -// # -// # For instance, -// # projects/{{project}}/global/networks/{{name}} -// # is transformed to -// # projects/(?P[^/]+)/global/networks/(?P[^/]+) -// # -// # Values marked with % are URL-encoded, and will match any number of /'s. -// # -// # Note: ?P indicates a Python-compatible named capture group. Named groups -// # aren't common in JS-based regex flavours, but are in Perl-based ones -// def format2regex(format) -// -// format -// .gsub(/\{\{%([[:word:]]+)\}\}/, '(?P<\1>.+)') -// .gsub(/\{\{([[:word:]]+)\}\}/, '(?P<\1>[^/]+)') -// -// end -// // # Capitalize the first letter of a property name. // # E.g. "creationTimestamp" becomes "CreationTimestamp". // def titlelize_property(property) diff --git a/mmv1/templates/terraform/resource.go.tmpl b/mmv1/templates/terraform/resource.go.tmpl index c745b83f9622..7389197edec3 100644 --- a/mmv1/templates/terraform/resource.go.tmpl +++ b/mmv1/templates/terraform/resource.go.tmpl @@ -11,7 +11,7 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. */}} + limitations under the License. */ -}} // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 @@ -1004,3 +1004,171 @@ func resource{{ $.ResourceName -}}Update(d *schema.ResourceData, meta interface{ return resource{{ $.ResourceName -}}Read(d, meta) } {{- end}} +func resource{{ $.ResourceName }}Delete(d *schema.ResourceData, meta interface{}) error { +{{- if and (and ($.GetAsync.IsA "OpAsync") $.GetAsync.IncludeProject) ($.GetAsync.Allow "delete")}} + var project string +{{- end }} +{{- if $.SkipDelete }} + log.Printf("[WARNING] {{ $.ProductMetadata.Name }}{{" "}}{{ $.Name }} resources" + + " cannot be deleted from Google Cloud. The resource %s will be removed from Terraform" + + " state, but will still be present on Google Cloud.", d.Id()) + d.SetId("") + + return nil +{{- else }} + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + +{{- if $.CustomCode.CustomDelete }} +{{/* TODO CustomDelete */}} +{{- else }} + + billingProject := "" + {{ if $.HasProject }} + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for {{ $.Name }}: %s", err) + } + {{- if $.LegacyLongFormProject }} + billingProject = strings.TrimPrefix(project, "projects/") + {{- else }} + billingProject = project + {{- end }} + {{- end }} + {{- if $.Mutex }} + lockName, err := tpgresource.ReplaceVars(d, config, "{{ $.Mutex }}") + if err != nil { + return err + } + transport_tpg.MutexStore.Lock(lockName) + defer transport_tpg.MutexStore.Unlock(lockName) + {{- end }} + + url, err := tpgresource.ReplaceVars{{if $.LegacyLongFormProject -}}ForId{{ end -}}(d, config, "{{"{{"}}{{$.ProductMetadata.Name}}BasePath{{"}}"}}{{$.DeleteUri}}") + if err != nil { + return err + } + {{/*If the deletion of the object requires sending a request body, the custom code will set 'obj' */}} + var obj map[string]interface{} + {{- if and $.NestedQuery $.NestedQuery.ModifyByPatch }} + {{/*Keep this after mutex - patch request data relies on current resource state*/}} + obj, err = resource{{ $.ResourceName }}PatchDeleteEncoder(d, meta, obj) + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, "{{ $.ResourceName }}") + } + {{- if $.UpdateMask }} + url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": "{{- join $.NestedQuery.Keys "," -}}"}) + if err != nil { + return err + } + {{- end }} + {{- end }} + {{- if $.SupportsIndirectUserProjectOverride }} + if parts := regexp.MustCompile(`projects\/([^\/]+)\/`).FindStringSubmatch(url); parts != nil { + billingProject = parts[1] + } + {{- end }} + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + headers := make(http.Header) + {{- if $.CustomCode.PreDelete }} + {{/* TODO PreDelete */}} + {{- end }} + + log.Printf("[DEBUG] Deleting {{ $.Name }} %q", d.Id()) + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "{{ camelize $.DeleteVerb "upper" -}}", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutDelete), + Headers: headers, + {{- if $.ErrorRetryPredicates }} + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{{"{"}}{{- join $.ErrorRetryPredicates "," -}}{{"}"}}, + {{- end }} + {{- if $.ErrorAbortPredicates }} + ErrorAbortPredicates: []transport_tpg.RetryErrorPredicateFunc{{"{"}}{{- join $.ErrorAbortPredicates "," -}}{{"}"}}, + {{- end }} + }) + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, "{{ $.Name }}") + } + {{ if $.GetAsync.Allow "Delete" -}} + {{ if $.GetAsync.IsA "PollAsync" -}} + err = transport_tpg.PollingWaitTime(resource{{ $.ResourceName }}PollRead(d, meta), {{ $.GetAsync.CheckResponseFuncExistence }}, "Deleting {{ $.Name }}}", d.Timeout(schema.TimeoutCreate), {{ $.Async.TargetOccurrences }}) + if err != nil { + {{- if $.Async.SuppressError }} + log.Printf("[ERROR] Unable to confirm eventually consistent {{ $.Name }} %q finished updating: %q", d.Id(), err) + {{- else }} + return fmt.Errorf("Error waiting to delete {{ $.Name }}: %s", err) + {{- end }} + } + {{- else }} + err = {{ $.ClientNamePascal }}OperationWaitTime( + config, res, {{if or $.HasProject $.GetAsync.IncludeProject -}} {{if $.LegacyLongFormProject -}}tpgresource.GetResourceNameFromSelfLink(project){{ else }}project{{ end }}, {{ end -}} "Deleting {{ $.Name -}}", userAgent, + d.Timeout(schema.TimeoutDelete)) + + if err != nil { + return err + } + {{- end }} +{{- end }} +{{- if $.CustomCode.PostDelete }} +{{/* TODO PostDelete */}} +{{- end }} + + log.Printf("[DEBUG] Finished deleting {{ $.Name }} %q: %#v", d.Id(), res) + return nil +{{- end }}{{/* custom code */}} +{{- end }}{{/* pre delete */}} +} + +{{ if not $.ExcludeImport -}} +func resource{{ $.ResourceName }}Import(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + {{- if $.CustomCode.CustomImport }} + {{/* TODO CustomImport */}} + {{- else }} + config := meta.(*transport_tpg.Config) + if err := tpgresource.ParseImportId([]string{ + {{- range $id := $.ImportIdFormatsFromResource }} + "^{{ format2regex $id }}$", + {{- end }} + }, d, config); err != nil { + return nil, err + } + + // Replace import id for the resource id + id, err := tpgresource.ReplaceVars{{ if $.LegacyLongFormProject -}}ForId{{ end -}}(d, config, "{{ $.IdFormat }}") + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + {{- if $.VirtualFields -}} + // Explicitly set virtual fields to default values on import + {{- range $vf := $.VirtualFields }} + {{- if $vf.DefaultValue }} + if err := d.Set("{{ $.vf.Name }}", {{ $.vf.DefaultValue }}); err != nil { + return nil, fmt.Errorf("Error setting {{ $.vf.Name }}: %s", err) + } + {{- end }} + {{- end }} + {{- end }} + {{- if $.CustomCode.PostImport }} + {{/* TODO PostImport */}} + {{- end }} + + return []*schema.ResourceData{d}, nil + {{- end }} +} +{{- end }} +{{/* TODO Flatteners */}} +{{/* TODO Expanders */}} \ No newline at end of file