Skip to content
Open
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
5 changes: 5 additions & 0 deletions mmv1/api/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ type Resource struct {
// types that share the same API URL (e.g. engines filtered by solutionType).
ListFilter string `yaml:"list_filter,omitempty"`

// [Optional] If true, the list API response is a bare JSON array instead of
// a wrapped object with a named key. Use ListArrayPages instead of ListPages
// when generating the list function.
ListResponseIsArray bool `yaml:"list_response_is_array,omitempty"`

// If true, skip sweeper generation for this resource
ExcludeSweeper bool `yaml:"exclude_sweeper,omitempty"`

Expand Down
4 changes: 4 additions & 0 deletions mmv1/products/apigee/AddonsConfig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ update_url: 'organizations/{{org}}:setAddons'
update_verb: 'POST'
delete_url: 'organizations/{{org}}:setAddons'
delete_verb: 'POST'
collection_url_key: 'organizations'
generate_list_resource: true

timeouts:
insert_minutes: 20
update_minutes: 20
Expand All @@ -39,6 +42,7 @@ async:
result:
resource_inside_response: false
custom_code:
decoder: 'templates/terraform/decoders/apigee_addons_config.go.tmpl'
custom_import: 'templates/terraform/custom_import/apigee_addons.go.tmpl'
test_check_destroy: 'templates/terraform/custom_check_destroy/apigee_addons_override.go.tmpl'
custom_identity:
Expand Down
3 changes: 3 additions & 0 deletions mmv1/products/apigee/ApiProduct.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ samples:
test_env_vars:
org_id: ORG_ID
billing_account: BILLING_ACCT
collection_url_key: 'apiProduct'
generate_list_resource: true

parameters:
- name: orgId
type: String
Expand Down
3 changes: 3 additions & 0 deletions mmv1/products/apigee/EnvironmentKeyvaluemaps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ delete_url: '{{env_id}}/keyvaluemaps/{{name}}'
import_format:
- '{{env_id}}/keyvaluemaps/{{name}}'
- '{{env_id}}/{{name}}'
generate_list_resource: true
list_response_is_array: true

timeouts:
insert_minutes: 1
update_minutes: 20
Expand Down
1 change: 1 addition & 0 deletions mmv1/products/apigee/Organization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ sweeper:
identifier_field: "organization"
custom_code:
encoder: 'templates/terraform/encoders/apigee_organization.go.tmpl'
decoder: 'templates/terraform/decoders/apigee_organization.go.tmpl'
custom_import: 'templates/terraform/custom_import/apigee_organization.go.tmpl'
samples:
- name: 'apigee_organization_cloud_basic'
Expand Down
3 changes: 3 additions & 0 deletions mmv1/products/apigee/TargetServer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ update_url: '{{env_id}}/targetservers/{{name}}'
import_format:
- '{{env_id}}/targetservers/{{name}}'
- '{{env_id}}/{{name}}'
generate_list_resource: true
list_response_is_array: true

timeouts:
insert_minutes: 1
update_minutes: 1
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// The list response returns organization objects where the org name is in "organization".
// Extract the org name and set it on the resource data so the
// identity and id can be constructed correctly.
if orgName, ok := res["organization"].(string); ok && orgName != "" {
if err := d.Set("org", orgName); err != nil {
return nil, fmt.Errorf("error setting org from list response: %w", err)
}
}
return res, nil
9 changes: 9 additions & 0 deletions mmv1/templates/terraform/decoders/apigee_organization.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// The list response returns items where the org name is in "organization".
// For a direct read response the org name is already in "name". Normalise so
// the identity field ("name") is always populated.
if name, ok := res["name"].(string); !ok || name == "" {
if orgName, ok := res["organization"].(string); ok && orgName != "" {
res["name"] = orgName
}
}
return res, nil
2 changes: 2 additions & 0 deletions mmv1/templates/terraform/list_resource.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ import (

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/list"
{{- if $.ListScopeProperties }}
"github.com/hashicorp/terraform-plugin-framework/types"
{{- end }}
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"

Expand Down
10 changes: 10 additions & 0 deletions mmv1/templates/terraform/list_resource_method.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ func List{{ $.ResourceName }}s(config *transport_tpg.Config,
return err
}

{{- if $.ListResponseIsArray }}
return transport_tpg.ListArrayPages(transport_tpg.ListArrayPagesOptions{
Config: config,
TempData: resourceData,
Resource: Resource{{ $.ResourceName -}}(),
ListURL: url,
BillingProject: billingProject,
UserAgent: userAgent,
{{- else }}
return transport_tpg.ListPages(transport_tpg.ListPagesOptions{
Config: config,
TempData: resourceData,
Expand All @@ -62,6 +71,7 @@ func List{{ $.ResourceName }}s(config *transport_tpg.Config,
{{- if $.ListFilter }}
Filter: "{{ $.ListFilter }}",
{{- end }}
{{- end }}
Flattener: func(res map[string]interface{}, d *schema.ResourceData, config *transport_tpg.Config) error {
headers := make(http.Header)
var err error
Expand Down
90 changes: 90 additions & 0 deletions mmv1/third_party/terraform/transport/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,3 +387,93 @@ func ListPages(opt ListPagesOptions) error {
}
return nil
}

type ListArrayPagesOptions struct {
Config *Config
TempData *schema.ResourceData
Resource *schema.Resource
ListURL string
BillingProject string
UserAgent string
Flattener func(item map[string]interface{}, d *schema.ResourceData, config *Config) error
Callback func(rd *schema.ResourceData) error
}

// ListArrayPages performs a GET request against ListURL where the response body is a
// JSON array (not an object with a nested array key). Each element is passed to
// Flattener and then to Callback. Rate-limited responses (HTTP 429) are retried.
func ListArrayPages(opt ListArrayPagesOptions) error {
url, err := AddQueryParams(opt.ListURL, map[string]string{})
if err != nil {
return err
}

reqHeaders := make(http.Header)
reqHeaders.Set("User-Agent", opt.UserAgent)
reqHeaders.Set("Content-Type", "application/json")
if opt.Config.UserProjectOverride && opt.BillingProject != "" {
if opt.BillingProject == "NO_BILLING_PROJECT_OVERRIDE" {
reqHeaders.Set("X-Goog-User-Project", "")
} else {
reqHeaders.Set("X-Goog-User-Project", opt.BillingProject)
}
}

var res *http.Response
err = Retry(RetryOptions{
RetryFunc: func() error {
u, err := AddQueryParams(url, map[string]string{"alt": "json"})
if err != nil {
return err
}
req, err := http.NewRequest("GET", u, nil)
if err != nil {
return err
}
req.Header = reqHeaders
res, err = opt.Config.Client.Do(req)
if err != nil {
return err
}
if err := googleapi.CheckResponse(res); err != nil {
googleapi.CloseBody(res)
return err
}
return nil
},
Timeout: DefaultRequestTimeout,
ErrorRetryPredicates: []RetryErrorPredicateFunc{Is429RetryableQuotaError},
})
if err != nil {
return HandleListGoogleApiError(err, url)
}
if res == nil {
return fmt.Errorf("unable to parse server response for list at %s", url)
}
defer googleapi.CloseBody(res)

if res.StatusCode == 204 {
return nil
}

var items []interface{}
if err := json.NewDecoder(res.Body).Decode(&items); err != nil {
return fmt.Errorf("error decoding array response from %s: %w", url, err)
}

seedState := opt.TempData.State()
for _, item := range items {
itemMap, ok := item.(map[string]interface{})
if !ok {
return fmt.Errorf("expected item to be map[string]interface{}, got %T", item)
}
itemResourceData := opt.Resource.Data(seedState)
if err := opt.Flattener(itemMap, itemResourceData, opt.Config); err != nil {
return fmt.Errorf("error flattening instance: %s", err)
}
if err := opt.Callback(itemResourceData); err != nil {
return err
}
}
return nil
}
Loading