diff --git a/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.tmpl b/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.tmpl index f51145ab32cf..3f7820893c71 100644 --- a/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.tmpl +++ b/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.tmpl @@ -304,6 +304,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/mmv1/third_party/terraform/services/apigee/apigee_utils.go b/mmv1/third_party/terraform/services/apigee/apigee_utils.go index a76e5145096b..bb1af1935451 100644 --- a/mmv1/third_party/terraform/services/apigee/apigee_utils.go +++ b/mmv1/third_party/terraform/services/apigee/apigee_utils.go @@ -1,12 +1,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 { @@ -45,3 +49,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/mmv1/third_party/terraform/services/apigee/resource_apigee_api.go b/mmv1/third_party/terraform/services/apigee/resource_apigee_api.go new file mode 100644 index 000000000000..57375e59f2ae --- /dev/null +++ b/mmv1/third_party/terraform/services/apigee/resource_apigee_api.go @@ -0,0 +1,403 @@ +// ---------------------------------------------------------------------------- +// +// 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/mmv1/third_party/terraform/services/apigee/resource_apigee_api_sweeper.go b/mmv1/third_party/terraform/services/apigee/resource_apigee_api_sweeper.go new file mode 100644 index 000000000000..575202fc9f3a --- /dev/null +++ b/mmv1/third_party/terraform/services/apigee/resource_apigee_api_sweeper.go @@ -0,0 +1,126 @@ +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/mmv1/third_party/terraform/services/apigee/resource_apigee_api_test.go b/mmv1/third_party/terraform/services/apigee/resource_apigee_api_test.go new file mode 100644 index 000000000000..c22f49e30239 --- /dev/null +++ b/mmv1/third_party/terraform/services/apigee/resource_apigee_api_test.go @@ -0,0 +1,229 @@ +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/mmv1/third_party/terraform/services/apigee/resource_apigee_sharedflow.go b/mmv1/third_party/terraform/services/apigee/resource_apigee_sharedflow.go index e38f06092132..4820b95768d7 100644 --- a/mmv1/third_party/terraform/services/apigee/resource_apigee_sharedflow.go +++ b/mmv1/third_party/terraform/services/apigee/resource_apigee_sharedflow.go @@ -9,11 +9,8 @@ package apigee import ( "context" - "encoding/json" "fmt" - "io" "log" - "net/http" "os" "time" @@ -22,7 +19,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 { @@ -391,74 +387,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/mmv1/third_party/terraform/services/apigee/test-fixtures/apigee_api_bundle.zip b/mmv1/third_party/terraform/services/apigee/test-fixtures/apigee_api_bundle.zip new file mode 100644 index 000000000000..d859a552aec7 Binary files /dev/null and b/mmv1/third_party/terraform/services/apigee/test-fixtures/apigee_api_bundle.zip differ diff --git a/mmv1/third_party/terraform/services/apigee/test-fixtures/apigee_api_bundle2.zip b/mmv1/third_party/terraform/services/apigee/test-fixtures/apigee_api_bundle2.zip new file mode 100644 index 000000000000..d745471b7f04 Binary files /dev/null and b/mmv1/third_party/terraform/services/apigee/test-fixtures/apigee_api_bundle2.zip differ diff --git a/mmv1/third_party/terraform/website/docs/r/apigee_api.html.markdown b/mmv1/third_party/terraform/website/docs/r/apigee_api.html.markdown new file mode 100644 index 000000000000..ad9130bb7d26 --- /dev/null +++ b/mmv1/third_party/terraform/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}} +```