From 890fa3e2e26b1571670002525ca1b35b30b0ff74 Mon Sep 17 00:00:00 2001 From: Modular Magician Date: Wed, 30 Oct 2024 20:26:16 +0000 Subject: [PATCH] Added google_apigee_api resource (#12036) [upstream:fc0e3024fda3fa86755fb3489c2501b27ea2a217] Signed-off-by: Modular Magician --- .changelog/12036.txt | 3 + google/provider/provider_mmv1_resources.go | 1 + google/services/apigee/apigee_utils.go | 76 +++- google/services/apigee/resource_apigee_api.go | 405 ++++++++++++++++++ .../apigee/resource_apigee_api_sweeper.go | 128 ++++++ .../apigee/resource_apigee_api_test.go | 231 ++++++++++ .../apigee/resource_apigee_sharedflow.go | 72 ---- .../test-fixtures/apigee_api_bundle.zip | Bin 0 -> 1319 bytes .../test-fixtures/apigee_api_bundle2.zip | Bin 0 -> 1855 bytes website/docs/r/apigee_api.html.markdown | 123 ++++++ 10 files changed, 965 insertions(+), 74 deletions(-) create mode 100644 .changelog/12036.txt create mode 100644 google/services/apigee/resource_apigee_api.go create mode 100644 google/services/apigee/resource_apigee_api_sweeper.go create mode 100644 google/services/apigee/resource_apigee_api_test.go create mode 100644 google/services/apigee/test-fixtures/apigee_api_bundle.zip create mode 100644 google/services/apigee/test-fixtures/apigee_api_bundle2.zip create mode 100644 website/docs/r/apigee_api.html.markdown diff --git a/.changelog/12036.txt b/.changelog/12036.txt new file mode 100644 index 00000000000..e88840ff22f --- /dev/null +++ b/.changelog/12036.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +`google_apigee_api` +``` \ No newline at end of file diff --git a/google/provider/provider_mmv1_resources.go b/google/provider/provider_mmv1_resources.go index d14c5ad6dee..7fcd06e2478 100644 --- a/google/provider/provider_mmv1_resources.go +++ b/google/provider/provider_mmv1_resources.go @@ -1185,6 +1185,7 @@ var generatedResources = map[string]*schema.Resource{ var handwrittenResources = map[string]*schema.Resource{ // ####### START handwritten resources ########### "google_app_engine_application": appengine.ResourceAppEngineApplication(), + "google_apigee_api": apigee.ResourceApigeeApi(), "google_apigee_sharedflow": apigee.ResourceApigeeSharedFlow(), "google_apigee_sharedflow_deployment": apigee.ResourceApigeeSharedFlowDeployment(), "google_apigee_flowhook": apigee.ResourceApigeeFlowhook(), diff --git a/google/services/apigee/apigee_utils.go b/google/services/apigee/apigee_utils.go index 8ea7d74d3a1..81c81160c66 100644 --- a/google/services/apigee/apigee_utils.go +++ b/google/services/apigee/apigee_utils.go @@ -3,12 +3,16 @@ package apigee import ( + "encoding/json" "fmt" - "log" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-google/google/tpgresource" transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" + "google.golang.org/api/googleapi" + "io" + "log" + "net/http" + "time" ) func resourceApigeeNatAddressActivate(config *transport_tpg.Config, d *schema.ResourceData, billingProject string, userAgent string) error { @@ -47,3 +51,71 @@ func resourceApigeeNatAddressActivate(config *transport_tpg.Config, d *schema.Re } return nil } + +// sendRequestRawBodyWithTimeout is derived from sendRequestWithTimeout with direct pass through of request body +func sendRequestRawBodyWithTimeout(config *transport_tpg.Config, method, project, rawurl, userAgent string, body io.Reader, contentType string, timeout time.Duration, errorRetryPredicates ...transport_tpg.RetryErrorPredicateFunc) (map[string]interface{}, error) { + log.Printf("[DEBUG] sendRequestRawBodyWithTimeout start") + reqHeaders := make(http.Header) + reqHeaders.Set("User-Agent", userAgent) + reqHeaders.Set("Content-Type", contentType) + + if config.UserProjectOverride && project != "" { + // Pass the project into this fn instead of parsing it from the URL because + // both project names and URLs can have colons in them. + reqHeaders.Set("X-Goog-User-Project", project) + } + + if timeout == 0 { + timeout = time.Duration(1) * time.Minute + } + + var res *http.Response + + log.Printf("[DEBUG] sendRequestRawBodyWithTimeout sending request") + + err := transport_tpg.Retry(transport_tpg.RetryOptions{ + RetryFunc: func() error { + req, err := http.NewRequest(method, rawurl, body) + if err != nil { + return err + } + + req.Header = reqHeaders + res, err = config.Client.Do(req) + if err != nil { + return err + } + + if err := googleapi.CheckResponse(res); err != nil { + googleapi.CloseBody(res) + return err + } + + return nil + }, + Timeout: timeout, + ErrorRetryPredicates: errorRetryPredicates, + }) + if err != nil { + return nil, err + } + + if res == nil { + return nil, fmt.Errorf("Unable to parse server response. This is most likely a terraform problem, please file a bug at https://github.com/hashicorp/terraform-provider-google/issues.") + } + + // The defer call must be made outside of the retryFunc otherwise it's closed too soon. + defer googleapi.CloseBody(res) + + // 204 responses will have no body, so we're going to error with "EOF" if we + // try to parse it. Instead, we can just return nil. + if res.StatusCode == 204 { + return nil, nil + } + result := make(map[string]interface{}) + if err := json.NewDecoder(res.Body).Decode(&result); err != nil { + return nil, err + } + log.Printf("[DEBUG] sendRequestRawBodyWithTimeout returning") + return result, nil +} diff --git a/google/services/apigee/resource_apigee_api.go b/google/services/apigee/resource_apigee_api.go new file mode 100644 index 00000000000..ab09a1cf42e --- /dev/null +++ b/google/services/apigee/resource_apigee_api.go @@ -0,0 +1,405 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +// ---------------------------------------------------------------------------- +// +// This file is partially automatically generated by Magic Modules and with manual +// changes to resourceApigeeApiCreate +// +// ---------------------------------------------------------------------------- + +package apigee + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func ResourceApigeeApi() *schema.Resource { + return &schema.Resource{ + Create: resourceApigeeApiCreate, + Read: resourceApigeeApiRead, + Update: resourceApigeeApiUpdate, + Delete: resourceApigeeApiDelete, + + Importer: &schema.ResourceImporter{ + State: resourceApigeeApiImport, + }, + + CustomizeDiff: customdiff.All( + /* + If any of the config_bundle, detect_md5hash or md5hash is changed, + then an update is expected, so we tell Terraform core to expect update on meta_data, + latest_revision_id and revision + */ + + customdiff.ComputedIf("meta_data", apigeeApiDetectBundleUpdate), + customdiff.ComputedIf("latest_revision_id", apigeeApiDetectBundleUpdate), + customdiff.ComputedIf("revision", apigeeApiDetectBundleUpdate), + ), + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(20 * time.Minute), + Update: schema.DefaultTimeout(20 * time.Minute), + Delete: schema.DefaultTimeout(20 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `Name of the API proxy. This field only accepts the following characters: A-Za-z0-9._-.`, + }, + "org_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The Apigee Organization name associated with the Apigee instance.`, + }, + "latest_revision_id": { + Type: schema.TypeString, + Computed: true, + Description: `The id of the most recently created revision for this API proxy.`, + }, + "meta_data": { + Type: schema.TypeList, + Computed: true, + Description: `Metadata describing the API proxy.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: `Time at which the API proxy was created, in milliseconds since epoch.`, + }, + "last_modified_at": { + Type: schema.TypeString, + Computed: true, + Description: `Time at which the API proxy was most recently modified, in milliseconds since epoch.`, + }, + "sub_type": { + Type: schema.TypeString, + Computed: true, + Description: `The type of entity described`, + }, + }, + }, + }, + "revision": { + Type: schema.TypeList, + Computed: true, + Description: `A list of revisions of this API proxy.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "config_bundle": { + Type: schema.TypeString, + Required: true, + Description: `Path to the config zip bundle`, + }, + "md5hash": { + Type: schema.TypeString, + Computed: true, + Description: `Base 64 MD5 hash of the uploaded config bundle.`, + }, + "detect_md5hash": { + Type: schema.TypeString, + Optional: true, + Description: `A hash of local config bundle in string, user needs to use a Terraform Hash function of their choice. A change in hash will trigger an update.`, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + localMd5Hash := "" + if config_bundle, ok := d.GetOkExists("config_bundle"); ok { + localMd5Hash = tpgresource.GetFileMd5Hash(config_bundle.(string)) + } + if localMd5Hash == "" { + return false + } + + // `old` is the md5 hash we speculated from server responses, + // when apply responded with succeed, hash is set to the hash of uploaded bundle + if old != localMd5Hash { + return false + } + + return true + }, + }, + }, + UseJSONNumber: true, + } +} + +func resourceApigeeApiCreate(d *schema.ResourceData, meta interface{}) error { + ctx := context.TODO() + tflog.Info(ctx, "resourceApigeeApiCreate") + log.Printf("[DEBUG] resourceApigeeApiCreate") + + log.Printf("[DEBUG] resourceApigeeApiCreate, name= %s", d.Get("name").(string)) + log.Printf("[DEBUG] resourceApigeeApiCreate, org_id=, %s", d.Get("org_id").(string)) + log.Printf("[DEBUG] resourceApigeeApiCreate, config_bundle=, %s", d.Get("config_bundle").(string)) + + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + var file *os.File + var localMd5Hash string + if configBundlePath, ok := d.GetOk("config_bundle"); ok { + var err error + file, err = os.Open(configBundlePath.(string)) + if err != nil { + return err + } + localMd5Hash = tpgresource.GetFileMd5Hash(configBundlePath.(string)) + } else { + return fmt.Errorf("Error, \"config_bundle\" must be specified") + } + + url, err := tpgresource.ReplaceVars(d, config, "{{ApigeeBasePath}}organizations/{{org_id}}/apis?name={{name}}&action=import") + if err != nil { + return err + } + billingProject := "" + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + log.Printf("[DEBUG] resourceApigeeApiCreate, url=, %s", url) + res, err := sendRequestRawBodyWithTimeout(config, "POST", billingProject, url, userAgent, file, "application/octet-stream", d.Timeout(schema.TimeoutCreate)) + + log.Printf("[DEBUG] sendRequestRawBodyWithTimeout Done") + if err != nil { + return fmt.Errorf("Error creating API proxy: %s", err) + } + + // Store the ID now + id, err := tpgresource.ReplaceVars(d, config, "organizations/{{org_id}}/apis/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + log.Printf("[DEBUG] create d.SetId done, id = %s", id) + + log.Printf("[DEBUG] Finished creating API proxy %q: %#v", d.Id(), res) + + if resourceApigeeApiRead(d, meta) != nil { + return fmt.Errorf("Error reading API proxy at end of Create: %s", err) + } + + d.Set("md5hash", localMd5Hash) + d.Set("detect_md5hash", localMd5Hash) + + return nil +} + +func resourceApigeeApiUpdate(d *schema.ResourceData, meta interface{}) error { + //For how API proxy api is implemented, just treat an update as create, when the name is same, it will create a new revision + return resourceApigeeApiCreate(d, meta) +} + +func resourceApigeeApiRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + url, err := tpgresource.ReplaceVars(d, config, "{{ApigeeBasePath}}organizations/{{org_id}}/apis/{{name}}") + if err != nil { + return err + } + log.Printf("[DEBUG] API proxy read url is: %s", url) + + billingProject := "" + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + log.Printf("[DEBUG] resourceApigeeApiRead sendRequest") + log.Printf("[DEBUG] resourceApigeeApiRead, url=, %s", url) + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + }) + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("ApigeeApi %q", d.Id())) + } + log.Printf("[DEBUG] resourceApigeeApuRead sendRequest completed") + previousLastModifiedAt := getApigeeApiLastModifiedAt(d) + if err := d.Set("meta_data", flattenApigeeApiMetaData(res["metaData"], d, config)); err != nil { + return fmt.Errorf("Error reading API proxy: %s", err) + } + currentLastModifiedAt := getApigeeApiLastModifiedAt(d) + if err := d.Set("name", flattenApigeeApiName(res["name"], d, config)); err != nil { + return fmt.Errorf("Error reading API proxy: %s", err) + } + if err := d.Set("revision", flattenApigeeApiRevision(res["revision"], d, config)); err != nil { + return fmt.Errorf("Error reading API proxy: %s", err) + } + if err := d.Set("latest_revision_id", flattenApigeeApiLatestRevisionId(res["latestRevisionId"], d, config)); err != nil { + return fmt.Errorf("Error reading API proxy: %s", err) + } + + //setting hash to suggest update + if previousLastModifiedAt != currentLastModifiedAt { + d.Set("md5hash", "UNKNOWN") + d.Set("detect_md5hash", "UNKNOWN") + } + return nil +} + +func getApigeeApiLastModifiedAt(d *schema.ResourceData) string { + + metaDataRaw := d.Get("meta_data").([]interface{}) + if len(metaDataRaw) != 1 { + //in Terraform Schema, a nest in object is implemented as an array of length one, even if it's technically an object + return "UNKNOWN" + } + metaData := metaDataRaw[0].(map[string]interface{}) + if metaData == nil { + return "UNKNOWN" + } + lastModifiedAt := metaData["last_modified_at"].(string) + if lastModifiedAt == "" { + return "UNKNOWN" + } + return lastModifiedAt +} + +func resourceApigeeApiDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + billingProject := "" + + url, err := tpgresource.ReplaceVars(d, config, "{{ApigeeBasePath}}organizations/{{org_id}}/apis/{{name}}") + if err != nil { + return err + } + + var obj map[string]interface{} + log.Printf("[DEBUG] Deleting API proxy %q", d.Id()) + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "DELETE", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutDelete), + }) + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, "Api") + } + + log.Printf("[DEBUG] Finished deleting API proxy %q: %#v", d.Id(), res) + return nil +} + +func resourceApigeeApiImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*transport_tpg.Config) + if err := tpgresource.ParseImportId([]string{ + "organizations/(?P[^/]+)/apis/(?P[^/]+)", + "(?P[^/]+)/(?P[^/]+)", + }, d, config); err != nil { + return nil, err + } + + // Replace import id for the resource id + id, err := tpgresource.ReplaceVars(d, config, "organizations/{{org_id}}/apis/{{name}}") + + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + log.Printf("[DEBUG] resourceApigeeApiImport, id= %s", id) + + return []*schema.ResourceData{d}, nil +} + +func flattenApigeeApiMetaData(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["created_at"] = + flattenApigeeApiMetaDataCreatedAt(original["createdAt"], d, config) + transformed["last_modified_at"] = + flattenApigeeApiMetaDataLastModifiedAt(original["lastModifiedAt"], d, config) + transformed["sub_type"] = + flattenApigeeApiMetaDataSubType(original["subType"], d, config) + return []interface{}{transformed} +} +func flattenApigeeApiMetaDataCreatedAt(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenApigeeApiMetaDataLastModifiedAt(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenApigeeApiMetaDataSubType(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenApigeeApiName(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenApigeeApiRevision(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenApigeeApiLatestRevisionId(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func expandApigeeApiName(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func apigeeApiDetectBundleUpdate(_ context.Context, diff *schema.ResourceDiff, v interface{}) bool { + tmp, _ := diff.GetChange("detect_md5hash") + oldBundleHash := tmp.(string) + currentBundleHash := "" + if config_bundle, ok := diff.GetOkExists("config_bundle"); ok { + currentBundleHash = tpgresource.GetFileMd5Hash(config_bundle.(string)) + } + log.Printf("[DEBUG] apigeeApiDetectUpdate detect_md5hash: %s -> %s", oldBundleHash, currentBundleHash) + + if oldBundleHash != currentBundleHash { + return true + } + return diff.HasChange("config_bundle") || diff.HasChange("md5hash") +} diff --git a/google/services/apigee/resource_apigee_api_sweeper.go b/google/services/apigee/resource_apigee_api_sweeper.go new file mode 100644 index 00000000000..4f8b7d88939 --- /dev/null +++ b/google/services/apigee/resource_apigee_api_sweeper.go @@ -0,0 +1,128 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package apigee + +import ( + "context" + "log" + "strings" + "testing" + + "github.com/hashicorp/terraform-provider-google/google/envvar" + "github.com/hashicorp/terraform-provider-google/google/sweeper" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func init() { + sweeper.AddTestSweepers("ApigeeApi", testSweepApigeeApi) +} + +// At the time of writing, the CI only passes us-central1 as the region +func testSweepApigeeApi(region string) error { + resourceName := "ApigeeApi" + log.Printf("[INFO][SWEEPER_LOG] Starting sweeper for %s", resourceName) + + config, err := sweeper.SharedConfigForRegion(region) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error getting shared config for region: %s", err) + return err + } + + err = config.LoadAndValidate(context.Background()) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error loading: %s", err) + return err + } + + t := &testing.T{} + billingId := envvar.GetTestBillingAccountFromEnv(t) + + // Setup variables to replace in list template + d := &tpgresource.ResourceDataMock{ + FieldsInSchema: map[string]interface{}{ + "project": config.Project, + "region": region, + "location": region, + "zone": "-", + "billing_account": billingId, + }, + } + + listTemplate := strings.Split("https://apigee.googleapis.com/v1/organizations/{{org_id}}/apis/{{name}}", "?")[0] + listUrl, err := tpgresource.ReplaceVars(d, config, listTemplate) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error preparing sweeper list url: %s", err) + return nil + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: config.Project, + RawURL: listUrl, + UserAgent: config.UserAgent, + }) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error in response from request %s: %s", listUrl, err) + return nil + } + + resourceList, ok := res["apis"] + if !ok { + log.Printf("[INFO][SWEEPER_LOG] Nothing found in response.") + return nil + } + + rl := resourceList.([]interface{}) + + log.Printf("[INFO][SWEEPER_LOG] Found %d items in %s list response.", len(rl), resourceName) + // Keep count of items that aren't sweepable for logging. + nonPrefixCount := 0 + for _, ri := range rl { + obj := ri.(map[string]interface{}) + var name string + // Id detected in the delete URL, attempt to use id. + if obj["id"] != nil { + name = tpgresource.GetResourceNameFromSelfLink(obj["id"].(string)) + } else if obj["name"] != nil { + name = tpgresource.GetResourceNameFromSelfLink(obj["name"].(string)) + } else { + log.Printf("[INFO][SWEEPER_LOG] %s resource name and id were nil", resourceName) + return nil + } + // Skip resources that shouldn't be sweeped + if !sweeper.IsSweepableTestResource(name) { + nonPrefixCount++ + continue + } + + deleteTemplate := "https://apigee.googleapis.com/v1/organizations/{{org_id}}/apis/{{name}}" + deleteUrl, err := tpgresource.ReplaceVars(d, config, deleteTemplate) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error preparing delete url: %s", err) + return nil + } + deleteUrl = deleteUrl + name + + // Don't wait on operations as we may have a lot to delete + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "DELETE", + Project: config.Project, + RawURL: deleteUrl, + UserAgent: config.UserAgent, + }) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error deleting for url %s : %s", deleteUrl, err) + } else { + log.Printf("[INFO][SWEEPER_LOG] Sent delete request for %s resource: %s", resourceName, name) + } + } + + if nonPrefixCount > 0 { + log.Printf("[INFO][SWEEPER_LOG] %d items were non-sweepable and skipped.", nonPrefixCount) + } + + return nil +} diff --git a/google/services/apigee/resource_apigee_api_test.go b/google/services/apigee/resource_apigee_api_test.go new file mode 100644 index 00000000000..f489de2ec14 --- /dev/null +++ b/google/services/apigee/resource_apigee_api_test.go @@ -0,0 +1,231 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package apigee_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func TestAccApigeeApi_apigeeApiTestExample(t *testing.T) { + acctest.SkipIfVcr(t) + t.Parallel() + + fmt.Printf("from t: org_id %s", envvar.GetTestOrgFromEnv(t)) + + context := map[string]interface{}{ + "org_id": envvar.GetTestOrgFromEnv(t), + "billing_account": envvar.GetTestBillingAccountFromEnv(t), + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckApigeeApiDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccApigeeApi_apigeeApiTestExample(context), + }, + { + ResourceName: "google_apigee_api.test_apigee_api", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"config_bundle", "detect_md5hash", "md5hash"}, + }, + { + Config: testAccApigeeApi_apigeeApiTestExampleUpdate(context), + }, + { + ResourceName: "google_apigee_api.test_apigee_api", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"config_bundle", "detect_md5hash", "md5hash"}, + }, + }, + }) +} + +func testAccApigeeApi_apigeeApiTestExample(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + project_id = "tf-test%{random_suffix}" + name = "tf-test%{random_suffix}" + org_id = "%{org_id}" + billing_account = "%{billing_account}" + deletion_policy = "DELETE" +} + +resource "google_project_service" "apigee" { + project = google_project.project.project_id + service = "apigee.googleapis.com" +} + +resource "google_project_service" "servicenetworking" { + project = google_project.project.project_id + service = "servicenetworking.googleapis.com" + depends_on = [google_project_service.apigee] +} + +resource "google_project_service" "compute" { + project = google_project.project.project_id + service = "compute.googleapis.com" + depends_on = [google_project_service.servicenetworking] +} + +resource "google_compute_network" "apigee_network" { + name = "apigee-network" + project = google_project.project.project_id + depends_on = [google_project_service.compute] +} + +resource "google_compute_global_address" "apigee_range" { + name = "tf-test-apigee-range%{random_suffix}" + purpose = "VPC_PEERING" + address_type = "INTERNAL" + prefix_length = 16 + network = google_compute_network.apigee_network.id + project = google_project.project.project_id +} + +resource "google_service_networking_connection" "apigee_vpc_connection" { + network = google_compute_network.apigee_network.id + service = "servicenetworking.googleapis.com" + reserved_peering_ranges = [google_compute_global_address.apigee_range.name] + depends_on = [google_project_service.servicenetworking] +} + +resource "google_apigee_organization" "apigee_org" { + analytics_region = "us-central1" + project_id = google_project.project.project_id + authorized_network = google_compute_network.apigee_network.id + depends_on = [ + google_service_networking_connection.apigee_vpc_connection, + google_project_service.apigee, + ] +} + +resource "google_apigee_api" "test_apigee_api" { + name = "tf-test-apigee-api" + org_id = google_project.project.project_id + config_bundle = "./test-fixtures/apigee_api_bundle.zip" + depends_on = [google_apigee_organization.apigee_org] +} +`, context) +} + +func testAccCheckApigeeApiDestroyProducer(t *testing.T) func(s *terraform.State) error { + return func(s *terraform.State) error { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_apigee_api" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := acctest.GoogleProviderConfig(t) + + url, err := tpgresource.ReplaceVarsForTest(config, rs, "{{ApigeeBasePath}}organizations/{{org_id}}/apis/{{name}}") + if err != nil { + return err + } + + billingProject := "" + + if config.BillingProject != "" { + billingProject = config.BillingProject + } + fmt.Printf("testAccCheckApigeeApiDestroyProducer, url %s", url) + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: config.UserAgent, + }) + if err == nil { + return fmt.Errorf("Apigee API proxy still exists at %s", url) + } + } + + return nil + } +} + +func testAccApigeeApi_apigeeApiTestExampleUpdate(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + project_id = "tf-test%{random_suffix}" + name = "tf-test%{random_suffix}" + org_id = "%{org_id}" + billing_account = "%{billing_account}" + deletion_policy = "DELETE" +} + +resource "google_project_service" "apigee" { + project = google_project.project.project_id + service = "apigee.googleapis.com" +} + +resource "google_project_service" "servicenetworking" { + project = google_project.project.project_id + service = "servicenetworking.googleapis.com" + depends_on = [google_project_service.apigee] +} + +resource "google_project_service" "compute" { + project = google_project.project.project_id + service = "compute.googleapis.com" + depends_on = [google_project_service.servicenetworking] +} + +resource "google_compute_network" "apigee_network" { + name = "apigee-network" + project = google_project.project.project_id + depends_on = [google_project_service.compute] +} + +resource "google_compute_global_address" "apigee_range" { + name = "tf-test-apigee-range%{random_suffix}" + purpose = "VPC_PEERING" + address_type = "INTERNAL" + prefix_length = 16 + network = google_compute_network.apigee_network.id + project = google_project.project.project_id +} + +resource "google_service_networking_connection" "apigee_vpc_connection" { + network = google_compute_network.apigee_network.id + service = "servicenetworking.googleapis.com" + reserved_peering_ranges = [google_compute_global_address.apigee_range.name] + depends_on = [google_project_service.servicenetworking] +} + +resource "google_apigee_organization" "apigee_org" { + analytics_region = "us-central1" + project_id = google_project.project.project_id + authorized_network = google_compute_network.apigee_network.id + depends_on = [ + google_service_networking_connection.apigee_vpc_connection, + google_project_service.apigee, + ] +} + +resource "google_apigee_api" "test_apigee_api" { + name = "tf-test-apigee-api" + org_id = google_project.project.project_id + config_bundle = "./test-fixtures/apigee_api_bundle2.zip" + depends_on = [google_apigee_organization.apigee_org] +} +`, context) +} diff --git a/google/services/apigee/resource_apigee_sharedflow.go b/google/services/apigee/resource_apigee_sharedflow.go index 9022a0ea0f3..20976ed6756 100644 --- a/google/services/apigee/resource_apigee_sharedflow.go +++ b/google/services/apigee/resource_apigee_sharedflow.go @@ -11,11 +11,8 @@ package apigee import ( "context" - "encoding/json" "fmt" - "io" "log" - "net/http" "os" "time" @@ -24,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-google/google/tpgresource" transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" - "google.golang.org/api/googleapi" ) func ResourceApigeeSharedFlow() *schema.Resource { @@ -393,74 +389,6 @@ func expandApigeeSharedFlowName(v interface{}, d tpgresource.TerraformResourceDa return v, nil } -// sendRequestRawBodyWithTimeout is derived from sendRequestWithTimeout with direct pass through of request body -func sendRequestRawBodyWithTimeout(config *transport_tpg.Config, method, project, rawurl, userAgent string, body io.Reader, contentType string, timeout time.Duration, errorRetryPredicates ...transport_tpg.RetryErrorPredicateFunc) (map[string]interface{}, error) { - log.Printf("[DEBUG] sendRequestRawBodyWithTimeout start") - reqHeaders := make(http.Header) - reqHeaders.Set("User-Agent", userAgent) - reqHeaders.Set("Content-Type", contentType) - - if config.UserProjectOverride && project != "" { - // Pass the project into this fn instead of parsing it from the URL because - // both project names and URLs can have colons in them. - reqHeaders.Set("X-Goog-User-Project", project) - } - - if timeout == 0 { - timeout = time.Duration(1) * time.Minute - } - - var res *http.Response - - log.Printf("[DEBUG] sendRequestRawBodyWithTimeout sending request") - - err := transport_tpg.Retry(transport_tpg.RetryOptions{ - RetryFunc: func() error { - req, err := http.NewRequest(method, rawurl, body) - if err != nil { - return err - } - - req.Header = reqHeaders - res, err = config.Client.Do(req) - if err != nil { - return err - } - - if err := googleapi.CheckResponse(res); err != nil { - googleapi.CloseBody(res) - return err - } - - return nil - }, - Timeout: timeout, - ErrorRetryPredicates: errorRetryPredicates, - }) - if err != nil { - return nil, err - } - - if res == nil { - return nil, fmt.Errorf("Unable to parse server response. This is most likely a terraform problem, please file a bug at https://github.com/hashicorp/terraform-provider-google/issues.") - } - - // The defer call must be made outside of the retryFunc otherwise it's closed too soon. - defer googleapi.CloseBody(res) - - // 204 responses will have no body, so we're going to error with "EOF" if we - // try to parse it. Instead, we can just return nil. - if res.StatusCode == 204 { - return nil, nil - } - result := make(map[string]interface{}) - if err := json.NewDecoder(res.Body).Decode(&result); err != nil { - return nil, err - } - log.Printf("[DEBUG] sendRequestRawBodyWithTimeout returning") - return result, nil -} - func apigeeSharedflowDetectBundleUpdate(_ context.Context, diff *schema.ResourceDiff, v interface{}) bool { tmp, _ := diff.GetChange("detect_md5hash") oldBundleHash := tmp.(string) diff --git a/google/services/apigee/test-fixtures/apigee_api_bundle.zip b/google/services/apigee/test-fixtures/apigee_api_bundle.zip new file mode 100644 index 0000000000000000000000000000000000000000..d859a552aec7684d746337479301cd2464931efb GIT binary patch literal 1319 zcmWIWW@h1H0D&oW0g+$^l;C8LVMr{}gF~q3LNBGmP12Gn(M1b~lFw6iN75wJN!&o4%ABbhJ z8I_WnmROooqF0fd19qSR&~YG)X6j~6PaQ9R-NLipnkUYm(GEP}ea3fdsF$a&x6WD3 zz)g>X7?`B{Crn`IuzLPy&z>t!CY||X(;?zvW^&@V-r^-o7ToZPmv>Q5efILH_|n(6 zUo@vEsJ%!wKjbEVwaOaodx(=K0-c<=EyBJN<&fCJ8}y(!%uFRV=qrK# z*t4C#e>Tt$LX2oZ4>Jlq=)VCY6NJ(HRO)}4ugO4Q&u3BJ_fdN*HB>Jq*a!vLFF3^I zo8X<0{o#EsgQv-^<1=qoo=C}&{?<^pfiHRA{dX43Ix(3WyMBhwH)o%>!Xs_3^nA@} zb2oeN_;MTSKg_Ffj&aqQvnOM&@21JlElo`i&-lE#$TLB5)zlT8yJov;hGqN{ujfs9 zXXE%z`o~Ng&n+*t7nD11)mZZ5(DW5o4Sa;0Re`$jvyK- zC$K_t0-8^ejl+x#WaFHXjYH%Lps}D_fz?=Cc>-cAFpe0OH1=UL7A2a1Hekse5QpPR z!pJr_0xg1v13W~;J{3$$d2V?LUJt3SoGk)l41kAS=oRg Q$-oAL;y^Qg0Wkvu0OuZQMF0Q* literal 0 HcmV?d00001 diff --git a/google/services/apigee/test-fixtures/apigee_api_bundle2.zip b/google/services/apigee/test-fixtures/apigee_api_bundle2.zip new file mode 100644 index 0000000000000000000000000000000000000000..d745471b7f0406e86b8725145d18f1c32f333d01 GIT binary patch literal 1855 zcmWIWW@h1H0D(_U0g+$^l;C8LVMr{)S7yQxw!*B%2>{lfPPJjpTc^W>nt`Av?JsKPNLeGZpInn+#EMwW3NFaZPIW9QN-ai&!FiyuAdF_TZ;v70Ap-%H>ivgy$xSK|*`~~O zNy)L|z(bi6hb-OG*<0@m2QJ9jx>;&%=5)hX=AL|=x+cG+Og8#EKUm%xeg7@r1*_C` z!4($+TZ%5uuz#a#W01!5R(SdWlEyUIQjLcx);GV-keXe6qQa=L3(%sr4&D zpz)}4A_NkL)2}>wvgk|`DEh1yXi3+mFzQLy&hSHyBfKdTl+@u-L@%|L5JvM;=>%_J?C`jJpR?$c9Fw(JrhtNgcF5GC1_xdjk>yMD z=HE{CI_*Ea{HXqO`qQqUwC4s`hhJ z#ry74{y1pe%-OTL-PC*1ieIm8C?1;iV}<#x6CwhVw=#Dsp9&Y7S`%gZN#EhNvhisfPvT=yw252lOGO-$qtFVC>3yf8UC5?U9j75o9pbc1x9b|`NmJP@b zcLZ7l4+o@@0%$JQQUVmdxXKA+m-k{d7o*ewT7#v;fH)sw4Q8H2wkCrK$@%ac3^Wx> z?nMq1{IU5I*;H753Jn1oOCUBeGbXZQ4VaM}3o{lyHnC*U0B=?{U_t;^O&ko@fKCf$ G0r3E|T@zXW literal 0 HcmV?d00001 diff --git a/website/docs/r/apigee_api.html.markdown b/website/docs/r/apigee_api.html.markdown new file mode 100644 index 00000000000..ad9130bb7d2 --- /dev/null +++ b/website/docs/r/apigee_api.html.markdown @@ -0,0 +1,123 @@ +--- +subcategory: "Apigee" +page_title: "Google: google_apigee_api" +description: |- + An Apigee API proxy is essentially a layer that sits in front of your backend APIs. It acts as an intermediary between your API consumers (like mobile apps or websites) and your backend services.   + + Think of it like a gatekeeper or a middleman: + + * Decoupling: It decouples the app-facing API from your backend services. This means you can make changes to your backend systems without affecting the apps that use your API, as long as the API proxy interface remains consistent.   + * Abstraction: It hides the complexities of your backend systems, presenting a simplified and consistent interface to your API consumers. + * Control: It gives you fine-grained control over how your APIs are accessed and used, allowing you to enforce security policies, rate limits, and other controls. +--- + +# google_apigee_api + +To get more information about API proxies see, see: + +* [API documentation](https://cloud.google.com/apigee/docs/reference/apis/apigee/rest/v1/organizations.apis) +* How-to Guides + * [API proxies](https://cloud.google.com/apigee/docs/resources) + + +## Example Usage + +```hcl +data "archive_file" "bundle" { + type = "zip" + source_dir = "${path.module}/bundle" + output_path = "${path.module}/bundle.zip" + output_file_mode = "0644" +} + +resource "google_apigee_sharedflow" "sharedflow" { + name = "shareflow1" + org_id = var.org_id + config_bundle = data.archive_file.bundle.output_path +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - + (Required) + The ID of the API proxy. + +* `org_id` - + (Required) + The Apigee Organization name associated with the Apigee instance. + +* `config_bundle` - + (Required) + Path to the config zip bundle. + +- - - + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are exported: + +* `id` - an identifier for the resource with format `organizations/{{org_id}}/apis/{{name}}` + +* `meta_data` - + Metadata describing the API proxy. + Structure is [documented below](#nested_meta_data). + +* `revision` - + A list of revisions of this API proxy. + +* `latest_revision_id` - + The id of the most recently created revision for this API proxy. + +* `md5hash` - + (Computed) Base 64 MD5 hash of the uploaded data. It is speculative as remote does not return hash of the bundle. Remote changes are detected using returned last_modified timestamp. + +* `detect_md5hash` - + (Optional) Detect changes to local config bundle file or changes made outside of Terraform. MD5 hash of the data, encoded using base64. Hash is automatically computed without need for user input. + +The `meta_data` block contains: + +* `created_at` - + (Optional) + Time at which the API proxy was created, in milliseconds since epoch. + +* `last_modified_at` - + (Optional) + Time at which the API proxy was most recently modified, in milliseconds since epoch. + +* `sub_type` - + (Optional) + The type of entity described + +## Timeouts + +This resource provides the following +[Timeouts](/docs/configuration/resources.html#timeouts) configuration options: + +* `create` - Default is 20 minutes. +* `delete` - Default is 20 minutes. + +## Import + +An API proxy can be imported using any of these accepted formats: + +* `{{org_id}}/apis/{{name}}` +* `{{org_id}}/{{name}}` + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import API proxy using one of the formats above. For example: + +```tf +import { + id = "{{org_id}}/apis/{{name}}" + to = google_apigee_api.default +} +``` + +When using the [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import), API proxy can be imported using one of the formats above. For example: + +``` +terraform import google_apigee_api.default {{org_id}}/apis/{{name}} +terraform import google_apigee_api.default {{org_id}}/{{name}} +```