diff --git a/.changelog/12747.txt b/.changelog/12747.txt new file mode 100644 index 00000000000..154eae5af32 --- /dev/null +++ b/.changelog/12747.txt @@ -0,0 +1,3 @@ +```release-note: enhancement +gemini: promoted resources `gemini_code_repository_index`, `gemini_repository_group` to GA. +``` \ No newline at end of file diff --git a/google/acctest/bootstrap_test_utils.go b/google/acctest/bootstrap_test_utils.go index 015b2acacf9..4afd61d715d 100644 --- a/google/acctest/bootstrap_test_utils.go +++ b/google/acctest/bootstrap_test_utils.go @@ -7,6 +7,7 @@ import ( "fmt" "log" "maps" + "net/http" "os" "strings" "testing" @@ -1426,6 +1427,316 @@ func SetupProjectsAndGetAccessToken(org, billing, pid, service string, config *t return accessToken, nil } +// For bootstrapping Developer Connect git repository link +const SharedGitRepositoryLinkIdPrefix = "tf-bootstrap-git-repository-" + +func BootstrapGitRepository(t *testing.T, gitRepositoryLinkId, location, cloneUri, parentConnectionId string) string { + gitRepositoryLinkId = SharedGitRepositoryLinkIdPrefix + gitRepositoryLinkId + + config := BootstrapConfig(t) + if config == nil { + t.Fatal("Could not bootstrap config.") + } + + log.Printf("[DEBUG] Getting shared git repository link %q", gitRepositoryLinkId) + + getURL := fmt.Sprintf("%sprojects/%s/locations/%s/connections/%s/gitRepositoryLinks/%s", + config.DeveloperConnectBasePath, config.Project, location, parentConnectionId, gitRepositoryLinkId) + + headers := make(http.Header) + _, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: config.Project, + RawURL: getURL, + UserAgent: config.UserAgent, + Headers: headers, + }) + + if err != nil && transport_tpg.IsGoogleApiErrorWithCode(err, 404) { + log.Printf("[DEBUG] Git repository link %q not found, bootstrapping", gitRepositoryLinkId) + obj := map[string]interface{}{ + "clone_uri": cloneUri, + "annotations": map[string]string{}, + } + + postURL := fmt.Sprintf("%sprojects/%s/locations/%s/connections/%s/gitRepositoryLinks?gitRepositoryLinkId=%s", + config.DeveloperConnectBasePath, config.Project, location, parentConnectionId, gitRepositoryLinkId) + headers := make(http.Header) + _, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: config.Project, + RawURL: postURL, + UserAgent: config.UserAgent, + Body: obj, + Timeout: 20 * time.Minute, + Headers: headers, + }) + if err != nil { + t.Fatalf("Error bootstrapping git repository link %q: %s", gitRepositoryLinkId, err) + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: config.Project, + RawURL: getURL, + UserAgent: config.UserAgent, + Timeout: 20 * time.Minute, + Headers: headers, + }) + if err != nil { + t.Fatalf("Error getting git repository link %q: %s", gitRepositoryLinkId, err) + } + } + + return gitRepositoryLinkId +} + +const SharedConnectionIdPrefix = "tf-bootstrap-developer-connect-connection-" + +// For bootstrapping Developer Connect connection resources +func BootstrapDeveloperConnection(t *testing.T, connectionId, location, tokenResource string, appInstallationId int) string { + connectionId = SharedConnectionIdPrefix + connectionId + + config := BootstrapConfig(t) + if config == nil { + t.Fatal("Could not bootstrap config.") + } + + log.Printf("[DEBUG] Getting shared developer connection %q", connectionId) + + getURL := fmt.Sprintf("%sprojects/%s/locations/%s/connections/%s", + config.DeveloperConnectBasePath, config.Project, location, connectionId) + + headers := make(http.Header) + _, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: config.Project, + RawURL: getURL, + UserAgent: config.UserAgent, + Headers: headers, + }) + + if err != nil { + log.Printf("[DEBUG] Developer connection %q not found, bootstrapping", connectionId) + authorizerCredential := map[string]string{ + "oauth_token_secret_version": tokenResource, + } + githubConfig := map[string]interface{}{ + "github_app": "DEVELOPER_CONNECT", + "app_installation_id": appInstallationId, + "authorizer_credential": authorizerCredential, + } + obj := map[string]interface{}{ + "disabled": false, + "github_config": githubConfig, + } + + postURL := fmt.Sprintf("%sprojects/%s/locations/%s/connections?connectionId=%s", + config.DeveloperConnectBasePath, config.Project, location, connectionId) + headers := make(http.Header) + _, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: config.Project, + RawURL: postURL, + UserAgent: config.UserAgent, + Body: obj, + Timeout: 20 * time.Minute, + Headers: headers, + }) + if err != nil { + t.Fatalf("Error bootstrapping developer connection %q: %s", connectionId, err) + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: config.Project, + RawURL: getURL, + UserAgent: config.UserAgent, + Timeout: 20 * time.Minute, + Headers: headers, + }) + if err != nil { + t.Fatalf("Error getting developer connection %q: %s", connectionId, err) + } + } + + return connectionId +} + +const SharedRepositoryGroupPrefix = "tf-bootstrap-repo-group-" + +func BoostrapSharedRepositoryGroup(t *testing.T, repositoryGroupId, location, labels, codeRepositoryIndexId, resource string) string { + repositoryGroupId = SharedRepositoryGroupPrefix + repositoryGroupId + + config := BootstrapConfig(t) + if config == nil { + t.Fatal("Could not bootstrap config.") + } + + log.Printf("[DEBUG] Getting shared repository group %q", repositoryGroupId) + + getURL := fmt.Sprintf("%sprojects/%s/locations/%s/codeRepositoryIndexes/%s/repositoryGroups/%s", + config.GeminiBasePath, config.Project, location, codeRepositoryIndexId, repositoryGroupId) + + headers := make(http.Header) + _, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: config.Project, + RawURL: getURL, + UserAgent: config.UserAgent, + Headers: headers, + }) + if err != nil { + log.Printf("[DEBUG] Repository group %q not found, bootstrapping", codeRepositoryIndexId) + repositories := [1]interface{}{map[string]string{ + "resource": resource, + "branch_pattern": "main", + }} + postURL := fmt.Sprintf("%sprojects/%s/locations/%s/codeRepositoryIndexes/%s/repositoryGroups?repositoryGroupId=%s", + config.GeminiBasePath, config.Project, location, codeRepositoryIndexId, repositoryGroupId) + obj := map[string]interface{}{ + "repositories": repositories, + } + if labels != "" { + obj["labels"] = labels + } + + headers := make(http.Header) + for { + _, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: config.Project, + RawURL: postURL, + UserAgent: config.UserAgent, + Body: obj, + Timeout: 20 * time.Minute, + Headers: headers, + }) + if err != nil { + if transport_tpg.IsGoogleApiErrorWithCode(err, 409) { + errMsg := fmt.Sprintf("%s", err) + if strings.Contains(errMsg, "unable to queue the operation") { + log.Printf("[DEBUG] Waiting for enqueued operation to finish before creating RepositoryGroup: %#v", obj) + time.Sleep(10 * time.Second) + } else if strings.Contains(errMsg, "parent resource not in ready state") { + log.Printf("[DEBUG] Waiting for parent resource to become active before creating RepositoryGroup: %#v", obj) + time.Sleep(1 * time.Minute) + } else { + t.Fatalf("Error creating RepositoryGroup: %s", err) + } + } else { + t.Fatalf("Error creating repository group %q: %s", repositoryGroupId, err) + } + } else { + break + } + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: config.Project, + RawURL: getURL, + UserAgent: config.UserAgent, + Timeout: 20 * time.Minute, + Headers: headers, + }) + if err != nil { + t.Errorf("Error getting repository group %q: %s", repositoryGroupId, err) + } + } + + return repositoryGroupId +} + +// BootstrapSharedCodeRepositoryIndex will create a code repository index +// if it hasn't been created in the test project. +// +// BootstrapSharedCodeRepositoryIndex returns a persistent code repository index +// for a test or set of tests. +// +// Deletion of code repository index takes a few minutes, and creation of it +// currently takes about half an hour. +// That is the reason to use the shared code repository indexes for test resources. +const SharedCodeRepositoryIndexPrefix = "tf-bootstrap-cri-" + +func BootstrapSharedCodeRepositoryIndex(t *testing.T, codeRepositoryIndexId, location, kmsKey string, labels map[string]string) string { + codeRepositoryIndexId = SharedCodeRepositoryIndexPrefix + codeRepositoryIndexId + + config := BootstrapConfig(t) + if config == nil { + t.Fatal("Could not bootstrap config.") + } + + log.Printf("[DEBUG] Getting shared code repository index %q", codeRepositoryIndexId) + + getURL := fmt.Sprintf("%sprojects/%s/locations/%s/codeRepositoryIndexes/%s", config.GeminiBasePath, config.Project, location, codeRepositoryIndexId) + + headers := make(http.Header) + _, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: config.Project, + RawURL: getURL, + UserAgent: config.UserAgent, + Timeout: 90 * time.Minute, + Headers: headers, + }) + + // CRI not found responds with 404 not found + if err != nil && transport_tpg.IsGoogleApiErrorWithCode(err, 404) { + log.Printf("[DEBUG] Code repository index %q not found, bootstrapping", codeRepositoryIndexId) + postURL := fmt.Sprintf("%sprojects/%s/locations/%s/codeRepositoryIndexes?codeRepositoryIndexId=%s", config.GeminiBasePath, config.Project, location, codeRepositoryIndexId) + obj := make(map[string]interface{}) + if labels != nil { + obj["labels"] = labels + } + if kmsKey != "" { + obj["kmsKey"] = kmsKey + } + + headers := make(http.Header) + _, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: config.Project, + RawURL: postURL, + UserAgent: config.UserAgent, + Body: obj, + Timeout: 90 * time.Minute, + Headers: headers, + }) + if err != nil { + t.Fatalf("Error creating code repository index %q: %s", codeRepositoryIndexId, err) + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: config.Project, + RawURL: getURL, + UserAgent: config.UserAgent, + Timeout: 90 * time.Minute, + Headers: headers, + }) + if err != nil { + t.Fatalf("Error getting code repository index %q: %s", codeRepositoryIndexId, err) + } + } else if err != nil { + t.Fatalf("Error getting code repository index %q: %s", codeRepositoryIndexId, err) + } + + return codeRepositoryIndexId +} + const sharedTagKeyPrefix = "tf-bootstrap-tagkey" func BootstrapSharedTestTagKey(t *testing.T, testId string) string { diff --git a/google/fwmodels/provider_model.go b/google/fwmodels/provider_model.go index 3db059300e6..3abd43d0531 100644 --- a/google/fwmodels/provider_model.go +++ b/google/fwmodels/provider_model.go @@ -96,6 +96,7 @@ type ProviderModel struct { FilestoreCustomEndpoint types.String `tfsdk:"filestore_custom_endpoint"` FirebaseAppCheckCustomEndpoint types.String `tfsdk:"firebase_app_check_custom_endpoint"` FirestoreCustomEndpoint types.String `tfsdk:"firestore_custom_endpoint"` + GeminiCustomEndpoint types.String `tfsdk:"gemini_custom_endpoint"` GKEBackupCustomEndpoint types.String `tfsdk:"gke_backup_custom_endpoint"` GKEHubCustomEndpoint types.String `tfsdk:"gke_hub_custom_endpoint"` GKEHub2CustomEndpoint types.String `tfsdk:"gke_hub2_custom_endpoint"` diff --git a/google/fwprovider/framework_provider.go b/google/fwprovider/framework_provider.go index 66cceae8b4c..4a75771bfe1 100644 --- a/google/fwprovider/framework_provider.go +++ b/google/fwprovider/framework_provider.go @@ -556,6 +556,12 @@ func (p *FrameworkProvider) Schema(_ context.Context, _ provider.SchemaRequest, transport_tpg.CustomEndpointValidator(), }, }, + "gemini_custom_endpoint": &schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + transport_tpg.CustomEndpointValidator(), + }, + }, "gke_backup_custom_endpoint": &schema.StringAttribute{ Optional: true, Validators: []validator.String{ diff --git a/google/provider/provider.go b/google/provider/provider.go index 38435922add..ca1595cada1 100644 --- a/google/provider/provider.go +++ b/google/provider/provider.go @@ -478,6 +478,11 @@ func Provider() *schema.Provider { Optional: true, ValidateFunc: transport_tpg.ValidateCustomEndpoint, }, + "gemini_custom_endpoint": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: transport_tpg.ValidateCustomEndpoint, + }, "gke_backup_custom_endpoint": { Type: schema.TypeString, Optional: true, @@ -1060,6 +1065,7 @@ func ProviderConfigure(ctx context.Context, d *schema.ResourceData, p *schema.Pr config.FilestoreBasePath = d.Get("filestore_custom_endpoint").(string) config.FirebaseAppCheckBasePath = d.Get("firebase_app_check_custom_endpoint").(string) config.FirestoreBasePath = d.Get("firestore_custom_endpoint").(string) + config.GeminiBasePath = d.Get("gemini_custom_endpoint").(string) config.GKEBackupBasePath = d.Get("gke_backup_custom_endpoint").(string) config.GKEHubBasePath = d.Get("gke_hub_custom_endpoint").(string) config.GKEHub2BasePath = d.Get("gke_hub2_custom_endpoint").(string) diff --git a/google/provider/provider_mmv1_resources.go b/google/provider/provider_mmv1_resources.go index aefc5730d00..4aa7e9f5bf9 100644 --- a/google/provider/provider_mmv1_resources.go +++ b/google/provider/provider_mmv1_resources.go @@ -70,6 +70,7 @@ import ( "github.com/hashicorp/terraform-provider-google/google/services/filestore" "github.com/hashicorp/terraform-provider-google/google/services/firebaseappcheck" "github.com/hashicorp/terraform-provider-google/google/services/firestore" + "github.com/hashicorp/terraform-provider-google/google/services/gemini" "github.com/hashicorp/terraform-provider-google/google/services/gkebackup" "github.com/hashicorp/terraform-provider-google/google/services/gkehub" "github.com/hashicorp/terraform-provider-google/google/services/gkehub2" @@ -398,6 +399,7 @@ var generatedIAMDatasources = map[string]*schema.Resource{ "google_dataproc_metastore_federation_iam_policy": tpgiamresource.DataSourceIamPolicy(dataprocmetastore.DataprocMetastoreFederationIamSchema, dataprocmetastore.DataprocMetastoreFederationIamUpdaterProducer), "google_dataproc_metastore_service_iam_policy": tpgiamresource.DataSourceIamPolicy(dataprocmetastore.DataprocMetastoreServiceIamSchema, dataprocmetastore.DataprocMetastoreServiceIamUpdaterProducer), "google_dns_managed_zone_iam_policy": tpgiamresource.DataSourceIamPolicy(dns.DNSManagedZoneIamSchema, dns.DNSManagedZoneIamUpdaterProducer), + "google_gemini_repository_group_iam_policy": tpgiamresource.DataSourceIamPolicy(gemini.GeminiRepositoryGroupIamSchema, gemini.GeminiRepositoryGroupIamUpdaterProducer), "google_gke_backup_backup_plan_iam_policy": tpgiamresource.DataSourceIamPolicy(gkebackup.GKEBackupBackupPlanIamSchema, gkebackup.GKEBackupBackupPlanIamUpdaterProducer), "google_gke_backup_restore_plan_iam_policy": tpgiamresource.DataSourceIamPolicy(gkebackup.GKEBackupRestorePlanIamSchema, gkebackup.GKEBackupRestorePlanIamUpdaterProducer), "google_gke_hub_membership_iam_policy": tpgiamresource.DataSourceIamPolicy(gkehub.GKEHubMembershipIamSchema, gkehub.GKEHubMembershipIamUpdaterProducer), @@ -465,9 +467,9 @@ var handwrittenIAMDatasources = map[string]*schema.Resource{ } // Resources -// Generated resources: 508 -// Generated IAM resources: 261 -// Total generated resources: 769 +// Generated resources: 510 +// Generated IAM resources: 264 +// Total generated resources: 774 var generatedResources = map[string]*schema.Resource{ "google_folder_access_approval_settings": accessapproval.ResourceAccessApprovalFolderSettings(), "google_organization_access_approval_settings": accessapproval.ResourceAccessApprovalOrganizationSettings(), @@ -881,6 +883,11 @@ var generatedResources = map[string]*schema.Resource{ "google_firestore_document": firestore.ResourceFirestoreDocument(), "google_firestore_field": firestore.ResourceFirestoreField(), "google_firestore_index": firestore.ResourceFirestoreIndex(), + "google_gemini_code_repository_index": gemini.ResourceGeminiCodeRepositoryIndex(), + "google_gemini_repository_group": gemini.ResourceGeminiRepositoryGroup(), + "google_gemini_repository_group_iam_binding": tpgiamresource.ResourceIamBinding(gemini.GeminiRepositoryGroupIamSchema, gemini.GeminiRepositoryGroupIamUpdaterProducer, gemini.GeminiRepositoryGroupIdParseFunc), + "google_gemini_repository_group_iam_member": tpgiamresource.ResourceIamMember(gemini.GeminiRepositoryGroupIamSchema, gemini.GeminiRepositoryGroupIamUpdaterProducer, gemini.GeminiRepositoryGroupIdParseFunc), + "google_gemini_repository_group_iam_policy": tpgiamresource.ResourceIamPolicy(gemini.GeminiRepositoryGroupIamSchema, gemini.GeminiRepositoryGroupIamUpdaterProducer, gemini.GeminiRepositoryGroupIdParseFunc), "google_gke_backup_backup_plan": gkebackup.ResourceGKEBackupBackupPlan(), "google_gke_backup_backup_plan_iam_binding": tpgiamresource.ResourceIamBinding(gkebackup.GKEBackupBackupPlanIamSchema, gkebackup.GKEBackupBackupPlanIamUpdaterProducer, gkebackup.GKEBackupBackupPlanIdParseFunc), "google_gke_backup_backup_plan_iam_member": tpgiamresource.ResourceIamMember(gkebackup.GKEBackupBackupPlanIamSchema, gkebackup.GKEBackupBackupPlanIamUpdaterProducer, gkebackup.GKEBackupBackupPlanIdParseFunc), diff --git a/google/services/gemini/gemini_operation.go b/google/services/gemini/gemini_operation.go new file mode 100644 index 00000000000..9a0aa18c727 --- /dev/null +++ b/google/services/gemini/gemini_operation.go @@ -0,0 +1,93 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package gemini + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +type GeminiOperationWaiter struct { + Config *transport_tpg.Config + UserAgent string + Project string + tpgresource.CommonOperationWaiter +} + +func (w *GeminiOperationWaiter) QueryOp() (interface{}, error) { + if w == nil { + return nil, fmt.Errorf("Cannot query operation, it's unset or nil.") + } + // Returns the proper get. + url := fmt.Sprintf("%s%s", w.Config.GeminiBasePath, w.CommonOperationWaiter.Op.Name) + + return transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: w.Config, + Method: "GET", + Project: w.Project, + RawURL: url, + UserAgent: w.UserAgent, + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsCodeRepositoryIndexUnreadyError, transport_tpg.IsRepositoryGroupQueueError}, + }) +} + +func createGeminiWaiter(config *transport_tpg.Config, op map[string]interface{}, project, activity, userAgent string) (*GeminiOperationWaiter, error) { + w := &GeminiOperationWaiter{ + Config: config, + UserAgent: userAgent, + Project: project, + } + if err := w.CommonOperationWaiter.SetOp(op); err != nil { + return nil, err + } + return w, nil +} + +// nolint: deadcode,unused +func GeminiOperationWaitTimeWithResponse(config *transport_tpg.Config, op map[string]interface{}, response *map[string]interface{}, project, activity, userAgent string, timeout time.Duration) error { + w, err := createGeminiWaiter(config, op, project, activity, userAgent) + if err != nil { + return err + } + if err := tpgresource.OperationWait(w, activity, timeout, config.PollInterval); err != nil { + return err + } + rawResponse := []byte(w.CommonOperationWaiter.Op.Response) + if len(rawResponse) == 0 { + return errors.New("`resource` not set in operation response") + } + return json.Unmarshal(rawResponse, response) +} + +func GeminiOperationWaitTime(config *transport_tpg.Config, op map[string]interface{}, project, activity, userAgent string, timeout time.Duration) error { + if val, ok := op["name"]; !ok || val == "" { + // This was a synchronous call - there is no operation to wait for. + return nil + } + w, err := createGeminiWaiter(config, op, project, activity, userAgent) + if err != nil { + // If w is nil, the op was synchronous. + return err + } + return tpgresource.OperationWait(w, activity, timeout, config.PollInterval) +} diff --git a/google/services/gemini/iam_gemini_repository_group.go b/google/services/gemini/iam_gemini_repository_group.go new file mode 100644 index 00000000000..6690843a072 --- /dev/null +++ b/google/services/gemini/iam_gemini_repository_group.go @@ -0,0 +1,262 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package gemini + +import ( + "fmt" + + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "google.golang.org/api/cloudresourcemanager/v1" + + "github.com/hashicorp/terraform-provider-google/google/tpgiamresource" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +var GeminiRepositoryGroupIamSchema = map[string]*schema.Schema{ + "project": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + }, + "location": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + }, + "code_repository_index": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "repository_group_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: tpgresource.CompareSelfLinkOrResourceName, + }, +} + +type GeminiRepositoryGroupIamUpdater struct { + project string + location string + codeRepositoryIndex string + repositoryGroupId string + d tpgresource.TerraformResourceData + Config *transport_tpg.Config +} + +func GeminiRepositoryGroupIamUpdaterProducer(d tpgresource.TerraformResourceData, config *transport_tpg.Config) (tpgiamresource.ResourceIamUpdater, error) { + values := make(map[string]string) + + project, _ := tpgresource.GetProject(d, config) + if project != "" { + if err := d.Set("project", project); err != nil { + return nil, fmt.Errorf("Error setting project: %s", err) + } + } + values["project"] = project + location, _ := tpgresource.GetLocation(d, config) + if location != "" { + if err := d.Set("location", location); err != nil { + return nil, fmt.Errorf("Error setting location: %s", err) + } + } + values["location"] = location + if v, ok := d.GetOk("code_repository_index"); ok { + values["code_repository_index"] = v.(string) + } + + if v, ok := d.GetOk("repository_group_id"); ok { + values["repository_group_id"] = v.(string) + } + + // We may have gotten either a long or short name, so attempt to parse long name if possible + m, err := tpgresource.GetImportIdQualifiers([]string{"projects/(?P<project>[^/]+)/locations/(?P<location>[^/]+)/codeRepositoryIndexes/(?P<code_repository_index>[^/]+)/repositoryGroups/(?P<repository_group_id>[^/]+)", "(?P<project>[^/]+)/(?P<location>[^/]+)/(?P<code_repository_index>[^/]+)/(?P<repository_group_id>[^/]+)", "(?P<location>[^/]+)/(?P<code_repository_index>[^/]+)/(?P<repository_group_id>[^/]+)", "(?P<repository_group_id>[^/]+)"}, d, config, d.Get("repository_group_id").(string)) + if err != nil { + return nil, err + } + + for k, v := range m { + values[k] = v + } + + u := &GeminiRepositoryGroupIamUpdater{ + project: values["project"], + location: values["location"], + codeRepositoryIndex: values["code_repository_index"], + repositoryGroupId: values["repository_group_id"], + d: d, + Config: config, + } + + if err := d.Set("project", u.project); err != nil { + return nil, fmt.Errorf("Error setting project: %s", err) + } + if err := d.Set("location", u.location); err != nil { + return nil, fmt.Errorf("Error setting location: %s", err) + } + if err := d.Set("code_repository_index", u.codeRepositoryIndex); err != nil { + return nil, fmt.Errorf("Error setting code_repository_index: %s", err) + } + if err := d.Set("repository_group_id", u.GetResourceId()); err != nil { + return nil, fmt.Errorf("Error setting repository_group_id: %s", err) + } + + return u, nil +} + +func GeminiRepositoryGroupIdParseFunc(d *schema.ResourceData, config *transport_tpg.Config) error { + values := make(map[string]string) + + project, _ := tpgresource.GetProject(d, config) + if project != "" { + values["project"] = project + } + + location, _ := tpgresource.GetLocation(d, config) + if location != "" { + values["location"] = location + } + + m, err := tpgresource.GetImportIdQualifiers([]string{"projects/(?P<project>[^/]+)/locations/(?P<location>[^/]+)/codeRepositoryIndexes/(?P<code_repository_index>[^/]+)/repositoryGroups/(?P<repository_group_id>[^/]+)", "(?P<project>[^/]+)/(?P<location>[^/]+)/(?P<code_repository_index>[^/]+)/(?P<repository_group_id>[^/]+)", "(?P<location>[^/]+)/(?P<code_repository_index>[^/]+)/(?P<repository_group_id>[^/]+)", "(?P<repository_group_id>[^/]+)"}, d, config, d.Id()) + if err != nil { + return err + } + + for k, v := range m { + values[k] = v + } + + u := &GeminiRepositoryGroupIamUpdater{ + project: values["project"], + location: values["location"], + codeRepositoryIndex: values["code_repository_index"], + repositoryGroupId: values["repository_group_id"], + d: d, + Config: config, + } + if err := d.Set("repository_group_id", u.GetResourceId()); err != nil { + return fmt.Errorf("Error setting repository_group_id: %s", err) + } + d.SetId(u.GetResourceId()) + return nil +} + +func (u *GeminiRepositoryGroupIamUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) { + url, err := u.qualifyRepositoryGroupUrl("getIamPolicy") + if err != nil { + return nil, err + } + + project, err := tpgresource.GetProject(u.d, u.Config) + if err != nil { + return nil, err + } + var obj map[string]interface{} + + userAgent, err := tpgresource.GenerateUserAgentString(u.d, u.Config.UserAgent) + if err != nil { + return nil, err + } + + policy, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: u.Config, + Method: "GET", + Project: project, + RawURL: url, + UserAgent: userAgent, + Body: obj, + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsCodeRepositoryIndexUnreadyError, transport_tpg.IsRepositoryGroupQueueError}, + }) + if err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("Error retrieving IAM policy for %s: {{err}}", u.DescribeResource()), err) + } + + out := &cloudresourcemanager.Policy{} + err = tpgresource.Convert(policy, out) + if err != nil { + return nil, errwrap.Wrapf("Cannot convert a policy to a resource manager policy: {{err}}", err) + } + + return out, nil +} + +func (u *GeminiRepositoryGroupIamUpdater) SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error { + json, err := tpgresource.ConvertToMap(policy) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + obj["policy"] = json + + url, err := u.qualifyRepositoryGroupUrl("setIamPolicy") + if err != nil { + return err + } + project, err := tpgresource.GetProject(u.d, u.Config) + if err != nil { + return err + } + + userAgent, err := tpgresource.GenerateUserAgentString(u.d, u.Config.UserAgent) + if err != nil { + return err + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: u.Config, + Method: "POST", + Project: project, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: u.d.Timeout(schema.TimeoutCreate), + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsCodeRepositoryIndexUnreadyError, transport_tpg.IsRepositoryGroupQueueError}, + }) + if err != nil { + return errwrap.Wrapf(fmt.Sprintf("Error setting IAM policy for %s: {{err}}", u.DescribeResource()), err) + } + + return nil +} + +func (u *GeminiRepositoryGroupIamUpdater) qualifyRepositoryGroupUrl(methodIdentifier string) (string, error) { + urlTemplate := fmt.Sprintf("{{GeminiBasePath}}%s:%s", fmt.Sprintf("projects/%s/locations/%s/codeRepositoryIndexes/%s/repositoryGroups/%s", u.project, u.location, u.codeRepositoryIndex, u.repositoryGroupId), methodIdentifier) + url, err := tpgresource.ReplaceVars(u.d, u.Config, urlTemplate) + if err != nil { + return "", err + } + return url, nil +} + +func (u *GeminiRepositoryGroupIamUpdater) GetResourceId() string { + return fmt.Sprintf("projects/%s/locations/%s/codeRepositoryIndexes/%s/repositoryGroups/%s", u.project, u.location, u.codeRepositoryIndex, u.repositoryGroupId) +} + +func (u *GeminiRepositoryGroupIamUpdater) GetMutexKey() string { + return fmt.Sprintf("iam-gemini-repositorygroup-%s", u.GetResourceId()) +} + +func (u *GeminiRepositoryGroupIamUpdater) DescribeResource() string { + return fmt.Sprintf("gemini repositorygroup %q", u.GetResourceId()) +} diff --git a/google/services/gemini/iam_gemini_repository_group_test.go b/google/services/gemini/iam_gemini_repository_group_test.go index 40f66691570..6be9446972a 100644 --- a/google/services/gemini/iam_gemini_repository_group_test.go +++ b/google/services/gemini/iam_gemini_repository_group_test.go @@ -1,3 +1,277 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package gemini_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" +) + +// To run tests locally please replace the `oauth_token_secret_version` with your secret manager version. +// More details: https://cloud.google.com/developer-connect/docs/connect-github-repo#before_you_begin + +func TestAccGeminiRepositoryGroupIamBinding(t *testing.T) { + location := "us-central1" + codeRepositoryIndexId := acctest.BootstrapSharedCodeRepositoryIndex(t, "basic", location, "", map[string]string{"ccfe_debug_note": "terraform_e2e_should_be_deleted"}) + developerConnectionId := acctest.BootstrapDeveloperConnection(t, "basic", location, "projects/502367051001/secrets/tf-test-cloudaicompanion-github-oauthtoken-c42e5c/versions/1", 54180648) + gitRepositoryLinkId := acctest.BootstrapGitRepository(t, "basic", location, "https://github.com/CC-R-github-robot/tf-test.git", developerConnectionId) + repositoryGroupId := "tf-test-iam-repository-group-id-" + acctest.RandString(t, 10) + + context := map[string]interface{}{ + "role": "roles/cloudaicompanion.repositoryGroupsUser", + "code_repository_index": codeRepositoryIndexId, + "location": location, + "project": envvar.GetTestProjectFromEnv(), + "connection_id": developerConnectionId, + "git_link_id": gitRepositoryLinkId, + "repository_group_id": repositoryGroupId, + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccGeminiRepositoryGroupIamBinding_basic(context), + }, + { + ResourceName: "google_gemini_repository_group_iam_binding.foo", + ImportStateId: fmt.Sprintf("projects/%s/locations/%s/codeRepositoryIndexes/%s/repositoryGroups/%s roles/cloudaicompanion.repositoryGroupsUser", envvar.GetTestProjectFromEnv(), envvar.GetTestRegionFromEnv(), codeRepositoryIndexId, repositoryGroupId), + ImportState: true, + ImportStateVerify: true, + }, + { + // Test Iam Binding update + Config: testAccGeminiRepositoryGroupIamBinding_update(context), + }, + { + ResourceName: "google_gemini_repository_group_iam_binding.foo", + ImportStateId: fmt.Sprintf("projects/%s/locations/%s/codeRepositoryIndexes/%s/repositoryGroups/%s roles/cloudaicompanion.repositoryGroupsUser", envvar.GetTestProjectFromEnv(), envvar.GetTestRegionFromEnv(), codeRepositoryIndexId, repositoryGroupId), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccGeminiRepositoryGroupIamMember(t *testing.T) { + location := "us-central1" + codeRepositoryIndexId := acctest.BootstrapSharedCodeRepositoryIndex(t, "basic", location, "", map[string]string{"ccfe_debug_note": "terraform_e2e_should_be_deleted"}) + developerConnectionId := acctest.BootstrapDeveloperConnection(t, "basic", location, "projects/502367051001/secrets/tf-test-cloudaicompanion-github-oauthtoken-c42e5c/versions/1", 54180648) + gitRepositoryLinkId := acctest.BootstrapGitRepository(t, "basic", location, "https://github.com/CC-R-github-robot/tf-test.git", developerConnectionId) + repositoryGroupId := "tf-test-iam-repository-group-id-" + acctest.RandString(t, 10) + + context := map[string]interface{}{ + "role": "roles/cloudaicompanion.repositoryGroupsUser", + "code_repository_index": codeRepositoryIndexId, + "location": location, + "project": envvar.GetTestProjectFromEnv(), + "connection_id": developerConnectionId, + "git_link_id": gitRepositoryLinkId, + "repository_group_id": repositoryGroupId, + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + // Test Iam Member creation (no update for member, no need to test) + Config: testAccGeminiRepositoryGroupIamMember_basic(context), + }, + { + ResourceName: "google_gemini_repository_group_iam_member.foo", + ImportStateId: fmt.Sprintf("projects/%s/locations/%s/codeRepositoryIndexes/%s/repositoryGroups/%s roles/cloudaicompanion.repositoryGroupsUser user:admin@hashicorptest.com", envvar.GetTestProjectFromEnv(), envvar.GetTestRegionFromEnv(), codeRepositoryIndexId, repositoryGroupId), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccGeminiRepositoryGroupIamPolicy(t *testing.T) { + location := "us-central1" + codeRepositoryIndexId := acctest.BootstrapSharedCodeRepositoryIndex(t, "basic", location, "", map[string]string{"ccfe_debug_note": "terraform_e2e_should_be_deleted"}) + developerConnectionId := acctest.BootstrapDeveloperConnection(t, "basic", location, "projects/502367051001/secrets/tf-test-cloudaicompanion-github-oauthtoken-c42e5c/versions/1", 54180648) + gitRepositoryLinkId := acctest.BootstrapGitRepository(t, "basic", location, "https://github.com/CC-R-github-robot/tf-test.git", developerConnectionId) + repositoryGroupId := "tf-test-iam-repository-group-id-" + acctest.RandString(t, 10) + + context := map[string]interface{}{ + "role": "roles/cloudaicompanion.repositoryGroupsUser", + "code_repository_index": codeRepositoryIndexId, + "location": location, + "project": envvar.GetTestProjectFromEnv(), + "connection_id": developerConnectionId, + "git_link_id": gitRepositoryLinkId, + "repository_group_id": repositoryGroupId, + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccGeminiRepositoryGroupIamPolicy_basic(context), + Check: resource.TestCheckResourceAttrSet("data.google_gemini_repository_group_iam_policy.foo", "policy_data"), + }, + { + ResourceName: "google_gemini_repository_group_iam_policy.foo", + ImportStateId: fmt.Sprintf("projects/%s/locations/%s/codeRepositoryIndexes/%s/repositoryGroups/%s", envvar.GetTestProjectFromEnv(), envvar.GetTestRegionFromEnv(), codeRepositoryIndexId, repositoryGroupId), + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccGeminiRepositoryGroupIamPolicy_emptyBinding(context), + }, + { + ResourceName: "google_gemini_repository_group_iam_policy.foo", + ImportStateId: fmt.Sprintf("projects/%s/locations/%s/codeRepositoryIndexes/%s/repositoryGroups/%s", envvar.GetTestProjectFromEnv(), envvar.GetTestRegionFromEnv(), codeRepositoryIndexId, repositoryGroupId), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccGeminiRepositoryGroupIamMember_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_gemini_repository_group_iam_member" "foo" { + project = "%{project}" + location = "%{location}" + code_repository_index = "%{code_repository_index}" + repository_group_id = google_gemini_repository_group.example.repository_group_id + role = "%{role}" + member = "user:admin@hashicorptest.com" +} + +resource "google_gemini_repository_group" "example" { + location = "us-central1" + code_repository_index = "%{code_repository_index}" + repository_group_id = "%{repository_group_id}" + repositories { + resource = "projects/%{project}/locations/us-central1/connections/%{connection_id}/gitRepositoryLinks/%{git_link_id}" + branch_pattern = "main" + } + labels = {"label1": "value1"} +} +`, context) +} + +func testAccGeminiRepositoryGroupIamPolicy_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_iam_policy" "foo" { + binding { + role = "%{role}" + members = ["user:admin@hashicorptest.com"] + } +} + +resource "google_gemini_repository_group_iam_policy" "foo" { + project = "%{project}" + location = "%{location}" + code_repository_index = "%{code_repository_index}" + repository_group_id = google_gemini_repository_group.example.repository_group_id + policy_data = data.google_iam_policy.foo.policy_data +} + +data "google_gemini_repository_group_iam_policy" "foo" { + project = "%{project}" + location = "%{location}" + code_repository_index = "%{code_repository_index}" + repository_group_id = google_gemini_repository_group.example.repository_group_id + depends_on = [ + google_gemini_repository_group_iam_policy.foo + ] +} + +resource "google_gemini_repository_group" "example" { + location = "us-central1" + code_repository_index = "%{code_repository_index}" + repository_group_id = "%{repository_group_id}" + repositories { + resource = "projects/%{project}/locations/us-central1/connections/%{connection_id}/gitRepositoryLinks/%{git_link_id}" + branch_pattern = "main" + } + labels = {"label1": "value1"} +} +`, context) +} + +func testAccGeminiRepositoryGroupIamPolicy_emptyBinding(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_iam_policy" "foo" { +} + +resource "google_gemini_repository_group_iam_policy" "foo" { + project = "%{project}" + location = "%{location}" + code_repository_index = "%{code_repository_index}" + repository_group_id = google_gemini_repository_group.example.repository_group_id + policy_data = data.google_iam_policy.foo.policy_data +} + +resource "google_gemini_repository_group" "example" { + location = "us-central1" + code_repository_index = "%{code_repository_index}" + repository_group_id = "%{repository_group_id}" + repositories { + resource = "projects/%{project}/locations/us-central1/connections/%{connection_id}/gitRepositoryLinks/%{git_link_id}" + branch_pattern = "main" + } + labels = {"label1": "value1"} +} +`, context) +} + +func testAccGeminiRepositoryGroupIamBinding_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_gemini_repository_group_iam_binding" "foo" { + project = "%{project}" + location = "%{location}" + code_repository_index = "%{code_repository_index}" + repository_group_id = google_gemini_repository_group.example.repository_group_id + role = "%{role}" + members = ["user:admin@hashicorptest.com"] +} + +resource "google_gemini_repository_group" "example" { + location = "us-central1" + code_repository_index = "%{code_repository_index}" + repository_group_id = "%{repository_group_id}" + repositories { + resource = "projects/%{project}/locations/us-central1/connections/%{connection_id}/gitRepositoryLinks/%{git_link_id}" + branch_pattern = "main" + } + labels = {"label1": "value1"} +} +`, context) +} + +func testAccGeminiRepositoryGroupIamBinding_update(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_gemini_repository_group_iam_binding" "foo" { + project = "%{project}" + location = "%{location}" + code_repository_index = "%{code_repository_index}" + repository_group_id = google_gemini_repository_group.example.repository_group_id + role = "%{role}" + members = ["user:admin@hashicorptest.com", "user:gterraformtest1@gmail.com"] +} + +resource "google_gemini_repository_group" "example" { + location = "us-central1" + code_repository_index = "%{code_repository_index}" + repository_group_id = "%{repository_group_id}" + repositories { + resource = "projects/%{project}/locations/us-central1/connections/%{connection_id}/gitRepositoryLinks/%{git_link_id}" + branch_pattern = "main" + } + labels = {"label1": "value1"} +} +`, context) +} diff --git a/google/services/gemini/resource_gemini_code_repository_index.go b/google/services/gemini/resource_gemini_code_repository_index.go new file mode 100644 index 00000000000..36df0ee07c0 --- /dev/null +++ b/google/services/gemini/resource_gemini_code_repository_index.go @@ -0,0 +1,555 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package gemini + +import ( + "fmt" + "log" + "net/http" + "reflect" + "strings" + "time" + + "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 ResourceGeminiCodeRepositoryIndex() *schema.Resource { + return &schema.Resource{ + Create: resourceGeminiCodeRepositoryIndexCreate, + Read: resourceGeminiCodeRepositoryIndexRead, + Update: resourceGeminiCodeRepositoryIndexUpdate, + Delete: resourceGeminiCodeRepositoryIndexDelete, + + Importer: &schema.ResourceImporter{ + State: resourceGeminiCodeRepositoryIndexImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(90 * time.Minute), + Update: schema.DefaultTimeout(90 * time.Minute), + Delete: schema.DefaultTimeout(90 * time.Minute), + }, + + CustomizeDiff: customdiff.All( + tpgresource.SetLabelsDiff, + tpgresource.DefaultProviderProject, + ), + + Schema: map[string]*schema.Schema{ + "code_repository_index_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `Required. Id of the Code Repository Index.`, + }, + "location": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The location of the Code Repository Index, for example 'us-central1'.`, + }, + "kms_key": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `Optional. Immutable. Customer-managed encryption key name, in the format +'projects/*/locations/*/keyRings/*/cryptoKeys/*'.`, + }, + "labels": { + Type: schema.TypeMap, + Optional: true, + Description: `Optional. Labels as key value pairs. + +**Note**: This field is non-authoritative, and will only manage the labels present in your configuration. +Please refer to the field 'effective_labels' for all of the labels present on the resource.`, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + Description: `Output only. Create time stamp.`, + }, + "effective_labels": { + Type: schema.TypeMap, + Computed: true, + Description: `All of labels (key/value pairs) present on the resource in GCP, including the labels configured through Terraform, other clients and services.`, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: `Immutable. Identifier. Name of Code Repository Index.`, + }, + "state": { + Type: schema.TypeString, + Computed: true, + Description: `Output only. Code Repository Index instance State. +Possible values are: 'STATE_UNSPECIFIED', 'CREATING', 'ACTIVE', 'DELETING', 'SUSPENDED'.`, + }, + "terraform_labels": { + Type: schema.TypeMap, + Computed: true, + Description: `The combination of labels configured directly on the resource + and default labels configured on the provider.`, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "update_time": { + Type: schema.TypeString, + Computed: true, + Description: `Output only. Update time stamp.`, + }, + "force_destroy": { + Type: schema.TypeBool, + Optional: true, + Description: `If set to true, will allow deletion of the CodeRepositoryIndex even if there are existing RepositoryGroups for the resource. These RepositoryGroups will also be deleted.`, + Default: false, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + UseJSONNumber: true, + } +} + +func resourceGeminiCodeRepositoryIndexCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + kmsKeyProp, err := expandGeminiCodeRepositoryIndexKmsKey(d.Get("kms_key"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("kms_key"); !tpgresource.IsEmptyValue(reflect.ValueOf(kmsKeyProp)) && (ok || !reflect.DeepEqual(v, kmsKeyProp)) { + obj["kmsKey"] = kmsKeyProp + } + labelsProp, err := expandGeminiCodeRepositoryIndexEffectiveLabels(d.Get("effective_labels"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("effective_labels"); !tpgresource.IsEmptyValue(reflect.ValueOf(labelsProp)) && (ok || !reflect.DeepEqual(v, labelsProp)) { + obj["labels"] = labelsProp + } + + lockName, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index_id}}") + if err != nil { + return err + } + transport_tpg.MutexStore.Lock(lockName) + defer transport_tpg.MutexStore.Unlock(lockName) + + url, err := tpgresource.ReplaceVars(d, config, "{{GeminiBasePath}}projects/{{project}}/locations/{{location}}/codeRepositoryIndexes?codeRepositoryIndexId={{code_repository_index_id}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new CodeRepositoryIndex: %#v", obj) + billingProject := "" + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for CodeRepositoryIndex: %s", err) + } + billingProject = project + + // 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) + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutCreate), + Headers: headers, + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsCodeRepositoryIndexUnreadyError, transport_tpg.IsRepositoryGroupQueueError}, + }) + if err != nil { + return fmt.Errorf("Error creating CodeRepositoryIndex: %s", err) + } + + // Store the ID now + id, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index_id}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + // Use the resource in the operation response to populate + // identity fields and d.Id() before read + var opRes map[string]interface{} + err = GeminiOperationWaitTimeWithResponse( + config, res, &opRes, project, "Creating CodeRepositoryIndex", userAgent, + d.Timeout(schema.TimeoutCreate)) + if err != nil { + // The resource didn't actually create + d.SetId("") + + return fmt.Errorf("Error waiting to create CodeRepositoryIndex: %s", err) + } + + if err := d.Set("name", flattenGeminiCodeRepositoryIndexName(opRes["name"], d, config)); err != nil { + return err + } + + // This may have caused the ID to update - update it if so. + id, err = tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index_id}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + log.Printf("[DEBUG] Finished creating CodeRepositoryIndex %q: %#v", d.Id(), res) + + return resourceGeminiCodeRepositoryIndexRead(d, meta) +} + +func resourceGeminiCodeRepositoryIndexRead(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, "{{GeminiBasePath}}projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index_id}}") + if err != nil { + return err + } + + billingProject := "" + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for CodeRepositoryIndex: %s", err) + } + billingProject = project + + // 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) + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Headers: headers, + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsCodeRepositoryIndexUnreadyError, transport_tpg.IsRepositoryGroupQueueError}, + }) + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("GeminiCodeRepositoryIndex %q", d.Id())) + } + + // Explicitly set virtual fields to default values if unset + if _, ok := d.GetOkExists("force_destroy"); !ok { + if err := d.Set("force_destroy", false); err != nil { + return fmt.Errorf("Error setting force_destroy: %s", err) + } + } + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error reading CodeRepositoryIndex: %s", err) + } + + if err := d.Set("update_time", flattenGeminiCodeRepositoryIndexUpdateTime(res["updateTime"], d, config)); err != nil { + return fmt.Errorf("Error reading CodeRepositoryIndex: %s", err) + } + if err := d.Set("state", flattenGeminiCodeRepositoryIndexState(res["state"], d, config)); err != nil { + return fmt.Errorf("Error reading CodeRepositoryIndex: %s", err) + } + if err := d.Set("labels", flattenGeminiCodeRepositoryIndexLabels(res["labels"], d, config)); err != nil { + return fmt.Errorf("Error reading CodeRepositoryIndex: %s", err) + } + if err := d.Set("kms_key", flattenGeminiCodeRepositoryIndexKmsKey(res["kmsKey"], d, config)); err != nil { + return fmt.Errorf("Error reading CodeRepositoryIndex: %s", err) + } + if err := d.Set("name", flattenGeminiCodeRepositoryIndexName(res["name"], d, config)); err != nil { + return fmt.Errorf("Error reading CodeRepositoryIndex: %s", err) + } + if err := d.Set("create_time", flattenGeminiCodeRepositoryIndexCreateTime(res["createTime"], d, config)); err != nil { + return fmt.Errorf("Error reading CodeRepositoryIndex: %s", err) + } + if err := d.Set("terraform_labels", flattenGeminiCodeRepositoryIndexTerraformLabels(res["labels"], d, config)); err != nil { + return fmt.Errorf("Error reading CodeRepositoryIndex: %s", err) + } + if err := d.Set("effective_labels", flattenGeminiCodeRepositoryIndexEffectiveLabels(res["labels"], d, config)); err != nil { + return fmt.Errorf("Error reading CodeRepositoryIndex: %s", err) + } + + return nil +} + +func resourceGeminiCodeRepositoryIndexUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + billingProject := "" + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for CodeRepositoryIndex: %s", err) + } + billingProject = project + + obj := make(map[string]interface{}) + labelsProp, err := expandGeminiCodeRepositoryIndexEffectiveLabels(d.Get("effective_labels"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("effective_labels"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, labelsProp)) { + obj["labels"] = labelsProp + } + + lockName, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index_id}}") + if err != nil { + return err + } + transport_tpg.MutexStore.Lock(lockName) + defer transport_tpg.MutexStore.Unlock(lockName) + + url, err := tpgresource.ReplaceVars(d, config, "{{GeminiBasePath}}projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index_id}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Updating CodeRepositoryIndex %q: %#v", d.Id(), obj) + headers := make(http.Header) + updateMask := []string{} + + if d.HasChange("effective_labels") { + updateMask = append(updateMask, "labels") + } + // updateMask is a URL parameter but not present in the schema, so ReplaceVars + // won't set it + url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) + if err != nil { + return err + } + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + // if updateMask is empty we are not updating anything so skip the post + if len(updateMask) > 0 { + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutUpdate), + Headers: headers, + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsCodeRepositoryIndexUnreadyError, transport_tpg.IsRepositoryGroupQueueError}, + }) + + if err != nil { + return fmt.Errorf("Error updating CodeRepositoryIndex %q: %s", d.Id(), err) + } else { + log.Printf("[DEBUG] Finished updating CodeRepositoryIndex %q: %#v", d.Id(), res) + } + + err = GeminiOperationWaitTime( + config, res, project, "Updating CodeRepositoryIndex", userAgent, + d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return err + } + } + + return resourceGeminiCodeRepositoryIndexRead(d, meta) +} + +func resourceGeminiCodeRepositoryIndexDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + billingProject := "" + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for CodeRepositoryIndex: %s", err) + } + billingProject = project + + lockName, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index_id}}") + if err != nil { + return err + } + transport_tpg.MutexStore.Lock(lockName) + defer transport_tpg.MutexStore.Unlock(lockName) + + url, err := tpgresource.ReplaceVars(d, config, "{{GeminiBasePath}}projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index_id}}") + if err != nil { + return err + } + + var obj map[string]interface{} + + // 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) + + log.Printf("[DEBUG] Deleting CodeRepositoryIndex %q", d.Id()) + 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), + Headers: headers, + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsCodeRepositoryIndexUnreadyError, transport_tpg.IsRepositoryGroupQueueError}, + }) + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, "CodeRepositoryIndex") + } + + err = GeminiOperationWaitTime( + config, res, project, "Deleting CodeRepositoryIndex", userAgent, + d.Timeout(schema.TimeoutDelete)) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Finished deleting CodeRepositoryIndex %q: %#v", d.Id(), res) + return nil +} + +func resourceGeminiCodeRepositoryIndexImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*transport_tpg.Config) + if err := tpgresource.ParseImportId([]string{ + "^projects/(?P<project>[^/]+)/locations/(?P<location>[^/]+)/codeRepositoryIndexes/(?P<code_repository_index_id>[^/]+)$", + "^(?P<project>[^/]+)/(?P<location>[^/]+)/(?P<code_repository_index_id>[^/]+)$", + "^(?P<location>[^/]+)/(?P<code_repository_index_id>[^/]+)$", + }, d, config); err != nil { + return nil, err + } + + // Replace import id for the resource id + id, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index_id}}") + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + // Explicitly set virtual fields to default values on import + if err := d.Set("force_destroy", false); err != nil { + return nil, fmt.Errorf("Error setting force_destroy: %s", err) + } + + return []*schema.ResourceData{d}, nil +} + +func flattenGeminiCodeRepositoryIndexUpdateTime(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenGeminiCodeRepositoryIndexState(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenGeminiCodeRepositoryIndexLabels(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + + transformed := make(map[string]interface{}) + if l, ok := d.GetOkExists("labels"); ok { + for k := range l.(map[string]interface{}) { + transformed[k] = v.(map[string]interface{})[k] + } + } + + return transformed +} + +func flattenGeminiCodeRepositoryIndexKmsKey(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenGeminiCodeRepositoryIndexName(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenGeminiCodeRepositoryIndexCreateTime(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenGeminiCodeRepositoryIndexTerraformLabels(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + + transformed := make(map[string]interface{}) + if l, ok := d.GetOkExists("terraform_labels"); ok { + for k := range l.(map[string]interface{}) { + transformed[k] = v.(map[string]interface{})[k] + } + } + + return transformed +} + +func flattenGeminiCodeRepositoryIndexEffectiveLabels(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func expandGeminiCodeRepositoryIndexKmsKey(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandGeminiCodeRepositoryIndexEffectiveLabels(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (map[string]string, error) { + if v == nil { + return map[string]string{}, nil + } + m := make(map[string]string) + for k, val := range v.(map[string]interface{}) { + m[k] = val.(string) + } + return m, nil +} diff --git a/google/services/gemini/resource_gemini_code_repository_index_generated_meta.yaml b/google/services/gemini/resource_gemini_code_repository_index_generated_meta.yaml new file mode 100644 index 00000000000..e457d0b20fd --- /dev/null +++ b/google/services/gemini/resource_gemini_code_repository_index_generated_meta.yaml @@ -0,0 +1,5 @@ +resource: 'google_gemini_code_repository_index' +generation_type: 'mmv1' +api_service_name: 'cloudaicompanion.googleapis.com' +api_version: 'v1' +api_resource_type_kind: 'CodeRepositoryIndex' diff --git a/google/services/gemini/resource_gemini_code_repository_index_test.go b/google/services/gemini/resource_gemini_code_repository_index_test.go index 40f66691570..633aab169a1 100644 --- a/google/services/gemini/resource_gemini_code_repository_index_test.go +++ b/google/services/gemini/resource_gemini_code_repository_index_test.go @@ -1,3 +1,204 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package gemini_test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/hashicorp/terraform-provider-google/google/acctest" +) + +func TestAccGeminiCodeRepositoryIndex_update(t *testing.T) { + bootstrappedKMS := acctest.BootstrapKMSKeyInLocation(t, "us-central1") + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + "project_id": os.Getenv("GOOGLE_PROJECT"), + "kms_key": bootstrappedKMS.CryptoKey.Name, + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccGeminiCodeRepositoryIndex_basic(context), + }, + { + ResourceName: "google_gemini_code_repository_index.example", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"code_repository_index_id", "labels", "location", "terraform_labels"}, + }, + { + Config: testAccGeminiCodeRepositoryIndex_update(context), + }, + { + ResourceName: "google_gemini_code_repository_index.example", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"code_repository_index_id", "labels", "location", "terraform_labels"}, + }, + }, + }) +} + +// TestAccGeminiCodeRepositoryIndex_delete checks if there is no error in deleting CRI along with children resource +// note: this is an example of a bad usage, where RGs refer to the CRI using a string id, not a reference, as they +// will be force-removed upon CRI deletion, because the CRI provider uses --force option by default +// The plan after the _delete function should not be empty due to the child resource in plan +func TestAccGeminiCodeRepositoryIndex_delete(t *testing.T) { + bootstrappedKMS := acctest.BootstrapKMSKeyInLocation(t, "us-central1") + randomSuffix := acctest.RandString(t, 10) + context := map[string]interface{}{ + "random_suffix": randomSuffix, + "project_id": os.Getenv("GOOGLE_PROJECT"), + "kms_key": bootstrappedKMS.CryptoKey.Name, + "cri_id": fmt.Sprintf("tf-test-cri-index-delete-example-%s", randomSuffix), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccGeminiCodeRepositoryIndex_withChildren_basic(context), + }, + { + ResourceName: "google_gemini_code_repository_index.example", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"code_repository_index_id", "labels", "location", "terraform_labels", "force_destroy"}, + }, + { + Config: testAccGeminiCodeRepositoryIndex_withChildren_delete(context), + ExpectNonEmptyPlan: true, + PlanOnly: true, + }, + }, + }) +} + +func testAccGeminiCodeRepositoryIndex_withChildren_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_gemini_code_repository_index" "example" { + labels = {"ccfe_debug_note": "terraform_e2e_should_be_deleted"} + location = "us-central1" + code_repository_index_id = "%{cri_id}" + force_destroy = true +} + +resource "google_gemini_repository_group" "example" { + location = "us-central1" + code_repository_index = "%{cri_id}" + repository_group_id = "tf-test-rg-repository-group-id-%{random_suffix}" + repositories { + resource = "projects/%{project_id}/locations/us-central1/connections/${google_developer_connect_connection.github_conn.connection_id}/gitRepositoryLinks/${google_developer_connect_git_repository_link.conn.git_repository_link_id}" + branch_pattern = "main" + } + labels = {"label1": "value1"} + depends_on = [ + google_gemini_code_repository_index.example + ] +} + +resource "google_developer_connect_git_repository_link" "conn" { + git_repository_link_id = "tf-test-repository-conn-delete" + parent_connection = google_developer_connect_connection.github_conn.connection_id + clone_uri = "https://github.com/CC-R-github-robot/tf-test.git" + location = "us-central1" + annotations = {} +} + +resource "google_developer_connect_connection" "github_conn" { + location = "us-central1" + connection_id = "tf-test-cloudaicompanion-delete-%{random_suffix}" + disabled = false + + github_config { + github_app = "DEVELOPER_CONNECT" + app_installation_id = 54180648 + + authorizer_credential { + oauth_token_secret_version = "projects/502367051001/secrets/tf-test-cloudaicompanion-github-oauthtoken-c42e5c/versions/1" + } + } +} +`, context) +} + +// Removed depends_on to not break plan test +func testAccGeminiCodeRepositoryIndex_withChildren_delete(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_gemini_repository_group" "example" { + location = "us-central1" + code_repository_index = "%{cri_id}" + repository_group_id = "tf-test-rg-repository-group-id-%{random_suffix}" + repositories { + resource = "projects/%{project_id}/locations/us-central1/connections/${google_developer_connect_connection.github_conn.connection_id}/gitRepositoryLinks/${google_developer_connect_git_repository_link.conn.git_repository_link_id}" + branch_pattern = "main" + } + labels = {"label1": "value1"} +} + +resource "google_developer_connect_git_repository_link" "conn" { + git_repository_link_id = "tf-test-repository-conn-delete" + parent_connection = google_developer_connect_connection.github_conn.connection_id + clone_uri = "https://github.com/CC-R-github-robot/tf-test.git" + location = "us-central1" + annotations = {} +} + +resource "google_developer_connect_connection" "github_conn" { + location = "us-central1" + connection_id = "tf-test-cloudaicompanion-delete-%{random_suffix}" + disabled = false + + github_config { + github_app = "DEVELOPER_CONNECT" + app_installation_id = 54180648 + + authorizer_credential { + oauth_token_secret_version = "projects/502367051001/secrets/tf-test-cloudaicompanion-github-oauthtoken-c42e5c/versions/1" + } + } +} +`, context) +} + +func testAccGeminiCodeRepositoryIndex_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_gemini_code_repository_index" "example" { + labels = {"ccfe_debug_note": "terraform_e2e_should_be_deleted"} + location = "us-central1" + code_repository_index_id = "tf-test-cri-index-example-%{random_suffix}" + kms_key = "%{kms_key}" + depends_on = [google_kms_crypto_key_iam_binding.crypto_key_binding] +} + +data "google_project" "project" { +} + +resource "google_kms_crypto_key_iam_binding" "crypto_key_binding" { + crypto_key_id = "%{kms_key}" + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + members = [ + "serviceAccount:service-${data.google_project.project.number}@gcp-sa-cloudaicompanion.iam.gserviceaccount.com", + ] +} +`, context) +} + +func testAccGeminiCodeRepositoryIndex_update(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_gemini_code_repository_index" "example" { + labels = {"ccfe_debug_note": "terraform_e2e_should_be_deleted", "new_label": "new_val"} + location = "us-central1" + code_repository_index_id = "tf-test-cri-index-example-%{random_suffix}" + kms_key = "%{kms_key}" +} +`, context) +} diff --git a/google/services/gemini/resource_gemini_repository_group.go b/google/services/gemini/resource_gemini_repository_group.go new file mode 100644 index 00000000000..c669968409f --- /dev/null +++ b/google/services/gemini/resource_gemini_repository_group.go @@ -0,0 +1,611 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package gemini + +import ( + "fmt" + "log" + "net/http" + "reflect" + "strings" + "time" + + "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 ResourceGeminiRepositoryGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceGeminiRepositoryGroupCreate, + Read: resourceGeminiRepositoryGroupRead, + Update: resourceGeminiRepositoryGroupUpdate, + Delete: resourceGeminiRepositoryGroupDelete, + + Importer: &schema.ResourceImporter{ + State: resourceGeminiRepositoryGroupImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + CustomizeDiff: customdiff.All( + tpgresource.SetLabelsDiff, + tpgresource.DefaultProviderProject, + ), + + Schema: map[string]*schema.Schema{ + "code_repository_index": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `Required. Id of the Code Repository Index.`, + }, + "location": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The location of the Code Repository Index, for example 'us-central1'.`, + }, + "repositories": { + Type: schema.TypeList, + Required: true, + Description: `Required. List of repositories to group.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "branch_pattern": { + Type: schema.TypeString, + Required: true, + Description: `Required. The Git branch pattern used for indexing in RE2 syntax. +See https://github.com/google/re2/wiki/syntax for syntax.`, + }, + "resource": { + Type: schema.TypeString, + Required: true, + Description: `Required. The DeveloperConnect repository full resource name, relative resource name +or resource URL to be indexed.`, + }, + }, + }, + }, + "repository_group_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `Required. Id of the Repository Group.`, + }, + "labels": { + Type: schema.TypeMap, + Optional: true, + Description: `Optional. Labels as key value pairs. + +**Note**: This field is non-authoritative, and will only manage the labels present in your configuration. +Please refer to the field 'effective_labels' for all of the labels present on the resource.`, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + Description: `Output only. Create time stamp.`, + }, + "effective_labels": { + Type: schema.TypeMap, + Computed: true, + Description: `All of labels (key/value pairs) present on the resource in GCP, including the labels configured through Terraform, other clients and services.`, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: `Immutable. Identifier. Name of Repository Group.`, + }, + "terraform_labels": { + Type: schema.TypeMap, + Computed: true, + Description: `The combination of labels configured directly on the resource + and default labels configured on the provider.`, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "update_time": { + Type: schema.TypeString, + Computed: true, + Description: `Output only. Update time stamp.`, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + UseJSONNumber: true, + } +} + +func resourceGeminiRepositoryGroupCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + repositoriesProp, err := expandGeminiRepositoryGroupRepositories(d.Get("repositories"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("repositories"); !tpgresource.IsEmptyValue(reflect.ValueOf(repositoriesProp)) && (ok || !reflect.DeepEqual(v, repositoriesProp)) { + obj["repositories"] = repositoriesProp + } + labelsProp, err := expandGeminiRepositoryGroupEffectiveLabels(d.Get("effective_labels"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("effective_labels"); !tpgresource.IsEmptyValue(reflect.ValueOf(labelsProp)) && (ok || !reflect.DeepEqual(v, labelsProp)) { + obj["labels"] = labelsProp + } + + lockName, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index}}") + if err != nil { + return err + } + transport_tpg.MutexStore.Lock(lockName) + defer transport_tpg.MutexStore.Unlock(lockName) + + url, err := tpgresource.ReplaceVars(d, config, "{{GeminiBasePath}}projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index}}/repositoryGroups?repositoryGroupId={{repository_group_id}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new RepositoryGroup: %#v", obj) + billingProject := "" + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for RepositoryGroup: %s", err) + } + billingProject = project + + // 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) + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutCreate), + Headers: headers, + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsCodeRepositoryIndexUnreadyError, transport_tpg.IsRepositoryGroupQueueError}, + }) + if err != nil { + return fmt.Errorf("Error creating RepositoryGroup: %s", err) + } + + // Store the ID now + id, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index}}/repositoryGroups/{{repository_group_id}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + // Use the resource in the operation response to populate + // identity fields and d.Id() before read + var opRes map[string]interface{} + err = GeminiOperationWaitTimeWithResponse( + config, res, &opRes, project, "Creating RepositoryGroup", userAgent, + d.Timeout(schema.TimeoutCreate)) + if err != nil { + // The resource didn't actually create + d.SetId("") + + return fmt.Errorf("Error waiting to create RepositoryGroup: %s", err) + } + + if err := d.Set("name", flattenGeminiRepositoryGroupName(opRes["name"], d, config)); err != nil { + return err + } + + // This may have caused the ID to update - update it if so. + id, err = tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index}}/repositoryGroups/{{repository_group_id}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + log.Printf("[DEBUG] Finished creating RepositoryGroup %q: %#v", d.Id(), res) + + return resourceGeminiRepositoryGroupRead(d, meta) +} + +func resourceGeminiRepositoryGroupRead(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, "{{GeminiBasePath}}projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index}}/repositoryGroups/{{repository_group_id}}") + if err != nil { + return err + } + + billingProject := "" + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for RepositoryGroup: %s", err) + } + billingProject = project + + // 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) + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Headers: headers, + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsCodeRepositoryIndexUnreadyError, transport_tpg.IsRepositoryGroupQueueError}, + }) + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("GeminiRepositoryGroup %q", d.Id())) + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error reading RepositoryGroup: %s", err) + } + + if err := d.Set("repositories", flattenGeminiRepositoryGroupRepositories(res["repositories"], d, config)); err != nil { + return fmt.Errorf("Error reading RepositoryGroup: %s", err) + } + if err := d.Set("name", flattenGeminiRepositoryGroupName(res["name"], d, config)); err != nil { + return fmt.Errorf("Error reading RepositoryGroup: %s", err) + } + if err := d.Set("create_time", flattenGeminiRepositoryGroupCreateTime(res["createTime"], d, config)); err != nil { + return fmt.Errorf("Error reading RepositoryGroup: %s", err) + } + if err := d.Set("update_time", flattenGeminiRepositoryGroupUpdateTime(res["updateTime"], d, config)); err != nil { + return fmt.Errorf("Error reading RepositoryGroup: %s", err) + } + if err := d.Set("labels", flattenGeminiRepositoryGroupLabels(res["labels"], d, config)); err != nil { + return fmt.Errorf("Error reading RepositoryGroup: %s", err) + } + if err := d.Set("terraform_labels", flattenGeminiRepositoryGroupTerraformLabels(res["labels"], d, config)); err != nil { + return fmt.Errorf("Error reading RepositoryGroup: %s", err) + } + if err := d.Set("effective_labels", flattenGeminiRepositoryGroupEffectiveLabels(res["labels"], d, config)); err != nil { + return fmt.Errorf("Error reading RepositoryGroup: %s", err) + } + + return nil +} + +func resourceGeminiRepositoryGroupUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + billingProject := "" + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for RepositoryGroup: %s", err) + } + billingProject = project + + obj := make(map[string]interface{}) + repositoriesProp, err := expandGeminiRepositoryGroupRepositories(d.Get("repositories"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("repositories"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, repositoriesProp)) { + obj["repositories"] = repositoriesProp + } + labelsProp, err := expandGeminiRepositoryGroupEffectiveLabels(d.Get("effective_labels"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("effective_labels"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, labelsProp)) { + obj["labels"] = labelsProp + } + + lockName, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index}}") + if err != nil { + return err + } + transport_tpg.MutexStore.Lock(lockName) + defer transport_tpg.MutexStore.Unlock(lockName) + + url, err := tpgresource.ReplaceVars(d, config, "{{GeminiBasePath}}projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index}}/repositoryGroups/{{repository_group_id}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Updating RepositoryGroup %q: %#v", d.Id(), obj) + headers := make(http.Header) + updateMask := []string{} + + if d.HasChange("repositories") { + updateMask = append(updateMask, "repositories") + } + + if d.HasChange("effective_labels") { + updateMask = append(updateMask, "labels") + } + // updateMask is a URL parameter but not present in the schema, so ReplaceVars + // won't set it + url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) + if err != nil { + return err + } + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + // if updateMask is empty we are not updating anything so skip the post + if len(updateMask) > 0 { + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutUpdate), + Headers: headers, + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsCodeRepositoryIndexUnreadyError, transport_tpg.IsRepositoryGroupQueueError}, + }) + + if err != nil { + return fmt.Errorf("Error updating RepositoryGroup %q: %s", d.Id(), err) + } else { + log.Printf("[DEBUG] Finished updating RepositoryGroup %q: %#v", d.Id(), res) + } + + err = GeminiOperationWaitTime( + config, res, project, "Updating RepositoryGroup", userAgent, + d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return err + } + } + + return resourceGeminiRepositoryGroupRead(d, meta) +} + +func resourceGeminiRepositoryGroupDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + billingProject := "" + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for RepositoryGroup: %s", err) + } + billingProject = project + + lockName, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index}}") + if err != nil { + return err + } + transport_tpg.MutexStore.Lock(lockName) + defer transport_tpg.MutexStore.Unlock(lockName) + + url, err := tpgresource.ReplaceVars(d, config, "{{GeminiBasePath}}projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index}}/repositoryGroups/{{repository_group_id}}") + if err != nil { + return err + } + + var obj map[string]interface{} + + // 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) + + log.Printf("[DEBUG] Deleting RepositoryGroup %q", d.Id()) + 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), + Headers: headers, + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsCodeRepositoryIndexUnreadyError, transport_tpg.IsRepositoryGroupQueueError}, + }) + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, "RepositoryGroup") + } + + err = GeminiOperationWaitTime( + config, res, project, "Deleting RepositoryGroup", userAgent, + d.Timeout(schema.TimeoutDelete)) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Finished deleting RepositoryGroup %q: %#v", d.Id(), res) + return nil +} + +func resourceGeminiRepositoryGroupImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*transport_tpg.Config) + if err := tpgresource.ParseImportId([]string{ + "^projects/(?P<project>[^/]+)/locations/(?P<location>[^/]+)/codeRepositoryIndexes/(?P<code_repository_index>[^/]+)/repositoryGroups/(?P<repository_group_id>[^/]+)$", + "^(?P<project>[^/]+)/(?P<location>[^/]+)/(?P<code_repository_index>[^/]+)/(?P<repository_group_id>[^/]+)$", + "^(?P<location>[^/]+)/(?P<code_repository_index>[^/]+)/(?P<repository_group_id>[^/]+)$", + }, d, config); err != nil { + return nil, err + } + + // Replace import id for the resource id + id, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index}}/repositoryGroups/{{repository_group_id}}") + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + return []*schema.ResourceData{d}, nil +} + +func flattenGeminiRepositoryGroupRepositories(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + l := v.([]interface{}) + transformed := make([]interface{}, 0, len(l)) + for _, raw := range l { + original := raw.(map[string]interface{}) + if len(original) < 1 { + // Do not include empty json objects coming back from the api + continue + } + transformed = append(transformed, map[string]interface{}{ + "resource": flattenGeminiRepositoryGroupRepositoriesResource(original["resource"], d, config), + "branch_pattern": flattenGeminiRepositoryGroupRepositoriesBranchPattern(original["branchPattern"], d, config), + }) + } + return transformed +} +func flattenGeminiRepositoryGroupRepositoriesResource(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenGeminiRepositoryGroupRepositoriesBranchPattern(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenGeminiRepositoryGroupName(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenGeminiRepositoryGroupCreateTime(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenGeminiRepositoryGroupUpdateTime(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenGeminiRepositoryGroupLabels(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + + transformed := make(map[string]interface{}) + if l, ok := d.GetOkExists("labels"); ok { + for k := range l.(map[string]interface{}) { + transformed[k] = v.(map[string]interface{})[k] + } + } + + return transformed +} + +func flattenGeminiRepositoryGroupTerraformLabels(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + + transformed := make(map[string]interface{}) + if l, ok := d.GetOkExists("terraform_labels"); ok { + for k := range l.(map[string]interface{}) { + transformed[k] = v.(map[string]interface{})[k] + } + } + + return transformed +} + +func flattenGeminiRepositoryGroupEffectiveLabels(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func expandGeminiRepositoryGroupRepositories(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + l := v.([]interface{}) + req := make([]interface{}, 0, len(l)) + for _, raw := range l { + if raw == nil { + continue + } + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedResource, err := expandGeminiRepositoryGroupRepositoriesResource(original["resource"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedResource); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["resource"] = transformedResource + } + + transformedBranchPattern, err := expandGeminiRepositoryGroupRepositoriesBranchPattern(original["branch_pattern"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedBranchPattern); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["branchPattern"] = transformedBranchPattern + } + + req = append(req, transformed) + } + return req, nil +} + +func expandGeminiRepositoryGroupRepositoriesResource(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandGeminiRepositoryGroupRepositoriesBranchPattern(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandGeminiRepositoryGroupEffectiveLabels(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (map[string]string, error) { + if v == nil { + return map[string]string{}, nil + } + m := make(map[string]string) + for k, val := range v.(map[string]interface{}) { + m[k] = val.(string) + } + return m, nil +} diff --git a/google/services/gemini/resource_gemini_repository_group_generated_meta.yaml b/google/services/gemini/resource_gemini_repository_group_generated_meta.yaml new file mode 100644 index 00000000000..e4d2d6a2326 --- /dev/null +++ b/google/services/gemini/resource_gemini_repository_group_generated_meta.yaml @@ -0,0 +1,5 @@ +resource: 'google_gemini_repository_group' +generation_type: 'mmv1' +api_service_name: 'cloudaicompanion.googleapis.com' +api_version: 'v1' +api_resource_type_kind: 'RepositoryGroup' diff --git a/google/services/gemini/resource_gemini_repository_group_sweeper.go b/google/services/gemini/resource_gemini_repository_group_sweeper.go new file mode 100644 index 00000000000..d18ad4440ca --- /dev/null +++ b/google/services/gemini/resource_gemini_repository_group_sweeper.go @@ -0,0 +1,143 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package gemini + +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("GeminiRepositoryGroup", testSweepGeminiRepositoryGroup) +} + +// At the time of writing, the CI only passes us-central1 as the region +func testSweepGeminiRepositoryGroup(region string) error { + resourceName := "GeminiRepositoryGroup" + 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://cloudaicompanion.googleapis.com/v1/projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index}}/repositoryGroups", "?")[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["repositoryGroups"] + 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://cloudaicompanion.googleapis.com/v1/projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index}}/repositoryGroups/{{repository_group_id}}" + 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/gemini/resource_gemini_repository_group_test.go b/google/services/gemini/resource_gemini_repository_group_test.go index 40f66691570..7e6ed392f52 100644 --- a/google/services/gemini/resource_gemini_repository_group_test.go +++ b/google/services/gemini/resource_gemini_repository_group_test.go @@ -1,3 +1,235 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package gemini_test + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/hashicorp/terraform-provider-google/google/acctest" +) + +// To run tests locally please replace the `oauth_token_secret_version` with your secret manager version. +// More details: https://cloud.google.com/developer-connect/docs/connect-github-repo#before_you_begin + +func TestAccGeminiRepositoryGroup_update(t *testing.T) { + codeRepositoryIndexId := acctest.BootstrapSharedCodeRepositoryIndex(t, "basic-rg-test", "us-central1", "", map[string]string{"ccfe_debug_note": "terraform_e2e_should_be_deleted"}) + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + "project_id": os.Getenv("GOOGLE_PROJECT"), + "code_repository_index": codeRepositoryIndexId, + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccGeminiRepositoryGroup_basic(context), + }, + { + ResourceName: "google_gemini_repository_group.example", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"code_repository_index", "labels", "location", "repository_group_id", "terraform_labels"}, + }, + { + Config: testAccGeminiRepositoryGroup_update(context), + }, + { + ResourceName: "google_gemini_repository_group.example", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"code_repository_index", "labels", "location", "repository_group_id", "terraform_labels"}, + }, + }, + }) +} + +func TestAccGeminiRepositoryGroup_noBootstrap(t *testing.T) { + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + "project_id": os.Getenv("GOOGLE_PROJECT"), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccGeminiRepositoryGroup_noBootstrap(context), + }, + { + ResourceName: "google_gemini_repository_group.example_e", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"code_repository_index", "labels", "location", "repository_group_id", "terraform_labels"}, + }, + }, + }) +} + +func testAccGeminiRepositoryGroup_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_gemini_repository_group" "example" { + location = "us-central1" + code_repository_index = "%{code_repository_index}" + repository_group_id = "tf-test-rg-repository-group-id-%{random_suffix}" + repositories { + resource = "projects/%{project_id}/locations/us-central1/connections/${google_developer_connect_connection.github_conn.connection_id}/gitRepositoryLinks/${google_developer_connect_git_repository_link.conn.git_repository_link_id}" + branch_pattern = "main" + } + labels = {"label1": "value1"} +} + +resource "google_developer_connect_git_repository_link" "conn" { + git_repository_link_id = "tf-test-repository-conn" + parent_connection = google_developer_connect_connection.github_conn.connection_id + clone_uri = "https://github.com/CC-R-github-robot/tf-test.git" + location = "us-central1" + annotations = {} +} + +resource "google_developer_connect_connection" "github_conn" { + location = "us-central1" + connection_id = "tf-test-cloudaicompanion2-%{random_suffix}" + disabled = false + + github_config { + github_app = "DEVELOPER_CONNECT" + app_installation_id = 54180648 + + authorizer_credential { + oauth_token_secret_version = "projects/502367051001/secrets/tf-test-cloudaicompanion-github-oauthtoken-c42e5c/versions/1" + } + } +} +`, context) +} +func testAccGeminiRepositoryGroup_update(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_gemini_repository_group" "example" { + location = "us-central1" + code_repository_index = "%{code_repository_index}" + repository_group_id = "tf-test-rg-repository-group-id-%{random_suffix}" + repositories { + resource = "projects/%{project_id}/locations/us-central1/connections/${google_developer_connect_connection.github_conn.connection_id}/gitRepositoryLinks/${google_developer_connect_git_repository_link.conn.git_repository_link_id}" + branch_pattern = "main" + } + labels = {"label1": "value1", "label2": "value2"} +} + +resource "google_developer_connect_git_repository_link" "conn" { + git_repository_link_id = "tf-test-repository-conn" + parent_connection = google_developer_connect_connection.github_conn.connection_id + clone_uri = "https://github.com/CC-R-github-robot/tf-test.git" + location = "us-central1" + annotations = {} +} + +resource "google_developer_connect_connection" "github_conn" { + location = "us-central1" + connection_id = "tf-test-cloudaicompanion3-%{random_suffix}" + disabled = false + + github_config { + github_app = "DEVELOPER_CONNECT" + app_installation_id = 54180648 + + authorizer_credential { + oauth_token_secret_version = "projects/502367051001/secrets/tf-test-cloudaicompanion-github-oauthtoken-c42e5c/versions/1" + } + } +} +`, context) +} + +func testAccGeminiRepositoryGroup_noBootstrap(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_gemini_code_repository_index" "cri" { + labels = {"ccfe_debug_note": "terraform_e2e_should_be_deleted"} + location = "us-central1" + code_repository_index_id = "tf-test-rg-index-example-%{random_suffix}" +} + +resource "google_gemini_repository_group" "example_a" { + location = "us-central1" + code_repository_index = google_gemini_code_repository_index.cri.code_repository_index_id + repository_group_id = "tf-test-rg-nb-repository-group-id1-%{random_suffix}" + repositories { + resource = "projects/%{project_id}/locations/us-central1/connections/${google_developer_connect_connection.github_conn.connection_id}/gitRepositoryLinks/${google_developer_connect_git_repository_link.conn.git_repository_link_id}" + branch_pattern = "main" + } + labels = {"label1": "value1"} +} + +resource "google_gemini_repository_group" "example_b" { + location = "us-central1" + code_repository_index = google_gemini_code_repository_index.cri.code_repository_index_id + repository_group_id = "tf-test-rg-nb-repository-group-id2-%{random_suffix}" + repositories { + resource = "projects/%{project_id}/locations/us-central1/connections/${google_developer_connect_connection.github_conn.connection_id}/gitRepositoryLinks/${google_developer_connect_git_repository_link.conn.git_repository_link_id}" + branch_pattern = "main" + } + labels = {"label1": "value1"} +} + +resource "google_gemini_repository_group" "example_c" { + location = "us-central1" + code_repository_index = google_gemini_code_repository_index.cri.code_repository_index_id + repository_group_id = "tf-test-rg-nb-repository-group-id3-%{random_suffix}" + repositories { + resource = "projects/%{project_id}/locations/us-central1/connections/${google_developer_connect_connection.github_conn.connection_id}/gitRepositoryLinks/${google_developer_connect_git_repository_link.conn.git_repository_link_id}" + branch_pattern = "main" + } + labels = {"label1": "value1"} +} + +resource "google_gemini_repository_group" "example_d" { + location = "us-central1" + code_repository_index = google_gemini_code_repository_index.cri.code_repository_index_id + repository_group_id = "tf-test-rg-nb-repository-group-id4-%{random_suffix}" + repositories { + resource = "projects/%{project_id}/locations/us-central1/connections/${google_developer_connect_connection.github_conn.connection_id}/gitRepositoryLinks/${google_developer_connect_git_repository_link.conn.git_repository_link_id}" + branch_pattern = "main" + } + labels = {"label1": "value1"} +} + +resource "google_gemini_repository_group" "example_e" { + location = "us-central1" + code_repository_index = google_gemini_code_repository_index.cri.code_repository_index_id + repository_group_id = "tf-test-rg-nb-repository-group-id5-%{random_suffix}" + repositories { + resource = "projects/%{project_id}/locations/us-central1/connections/${google_developer_connect_connection.github_conn.connection_id}/gitRepositoryLinks/${google_developer_connect_git_repository_link.conn.git_repository_link_id}" + branch_pattern = "main" + } + labels = {"label1": "value1"} +} + +resource "google_developer_connect_git_repository_link" "conn" { + git_repository_link_id = "tf-test-repository-conn" + parent_connection = google_developer_connect_connection.github_conn.connection_id + clone_uri = "https://github.com/CC-R-github-robot/tf-test.git" + location = "us-central1" + annotations = {} +} + +resource "google_developer_connect_connection" "github_conn" { + location = "us-central1" + connection_id = "tf-test-cloudaicompanion1-%{random_suffix}" + disabled = false + + github_config { + github_app = "DEVELOPER_CONNECT" + app_installation_id = 54180648 + + authorizer_credential { + oauth_token_secret_version = "projects/502367051001/secrets/tf-test-cloudaicompanion-github-oauthtoken-c42e5c/versions/1" + } + } +} +`, context) +} diff --git a/google/sweeper/gcp_sweeper_test.go b/google/sweeper/gcp_sweeper_test.go index 752ac723ff8..942b7a71fab 100644 --- a/google/sweeper/gcp_sweeper_test.go +++ b/google/sweeper/gcp_sweeper_test.go @@ -72,6 +72,7 @@ import ( _ "github.com/hashicorp/terraform-provider-google/google/services/filestore" _ "github.com/hashicorp/terraform-provider-google/google/services/firebaseappcheck" _ "github.com/hashicorp/terraform-provider-google/google/services/firestore" + _ "github.com/hashicorp/terraform-provider-google/google/services/gemini" _ "github.com/hashicorp/terraform-provider-google/google/services/gkebackup" _ "github.com/hashicorp/terraform-provider-google/google/services/gkehub" _ "github.com/hashicorp/terraform-provider-google/google/services/gkehub2" diff --git a/google/transport/config.go b/google/transport/config.go index 8cd361d9f30..71c5a886814 100644 --- a/google/transport/config.go +++ b/google/transport/config.go @@ -254,6 +254,7 @@ type Config struct { FilestoreBasePath string FirebaseAppCheckBasePath string FirestoreBasePath string + GeminiBasePath string GKEBackupBasePath string GKEHubBasePath string GKEHub2BasePath string @@ -404,6 +405,7 @@ const EssentialContactsBasePathKey = "EssentialContacts" const FilestoreBasePathKey = "Filestore" const FirebaseAppCheckBasePathKey = "FirebaseAppCheck" const FirestoreBasePathKey = "Firestore" +const GeminiBasePathKey = "Gemini" const GKEBackupBasePathKey = "GKEBackup" const GKEHubBasePathKey = "GKEHub" const GKEHub2BasePathKey = "GKEHub2" @@ -548,6 +550,7 @@ var DefaultBasePaths = map[string]string{ FilestoreBasePathKey: "https://file.googleapis.com/v1/", FirebaseAppCheckBasePathKey: "https://firebaseappcheck.googleapis.com/v1/", FirestoreBasePathKey: "https://firestore.googleapis.com/v1/", + GeminiBasePathKey: "https://cloudaicompanion.googleapis.com/v1/", GKEBackupBasePathKey: "https://gkebackup.googleapis.com/v1/", GKEHubBasePathKey: "https://gkehub.googleapis.com/v1/", GKEHub2BasePathKey: "https://gkehub.googleapis.com/v1/", @@ -1027,6 +1030,11 @@ func SetEndpointDefaults(d *schema.ResourceData) error { "GOOGLE_FIRESTORE_CUSTOM_ENDPOINT", }, DefaultBasePaths[FirestoreBasePathKey])) } + if d.Get("gemini_custom_endpoint") == "" { + d.Set("gemini_custom_endpoint", MultiEnvDefault([]string{ + "GOOGLE_GEMINI_CUSTOM_ENDPOINT", + }, DefaultBasePaths[GeminiBasePathKey])) + } if d.Get("gke_backup_custom_endpoint") == "" { d.Set("gke_backup_custom_endpoint", MultiEnvDefault([]string{ "GOOGLE_GKE_BACKUP_CUSTOM_ENDPOINT", @@ -2338,6 +2346,7 @@ func ConfigureBasePaths(c *Config) { c.FilestoreBasePath = DefaultBasePaths[FilestoreBasePathKey] c.FirebaseAppCheckBasePath = DefaultBasePaths[FirebaseAppCheckBasePathKey] c.FirestoreBasePath = DefaultBasePaths[FirestoreBasePathKey] + c.GeminiBasePath = DefaultBasePaths[GeminiBasePathKey] c.GKEBackupBasePath = DefaultBasePaths[GKEBackupBasePathKey] c.GKEHubBasePath = DefaultBasePaths[GKEHubBasePathKey] c.GKEHub2BasePath = DefaultBasePaths[GKEHub2BasePathKey] diff --git a/website/docs/d/gemini_repository_group_iam_policy.html.markdown b/website/docs/d/gemini_repository_group_iam_policy.html.markdown index 291c1d66362..aa031163e13 100644 --- a/website/docs/d/gemini_repository_group_iam_policy.html.markdown +++ b/website/docs/d/gemini_repository_group_iam_policy.html.markdown @@ -20,15 +20,12 @@ description: |- # `google_gemini_repository_group_iam_policy` Retrieves the current IAM policy data for repositorygroup -~> **Warning:** This datasource is in beta, and should be used with the terraform-provider-google-beta provider. -See [Provider Versions](https://terraform.io/docs/providers/google/guides/provider_versions.html) for more details on beta resources. ## example ```hcl data "google_gemini_repository_group_iam_policy" "policy" { - provider = google-beta project = google_gemini_repository_group.example.project location = google_gemini_repository_group.example.location code_repository_index = google_gemini_repository_group.example.code_repository_index diff --git a/website/docs/r/gemini_code_repository_index.html.markdown b/website/docs/r/gemini_code_repository_index.html.markdown index 309c9cd722e..b245517202e 100644 --- a/website/docs/r/gemini_code_repository_index.html.markdown +++ b/website/docs/r/gemini_code_repository_index.html.markdown @@ -21,18 +21,20 @@ description: |- The resource for managing Code Repository Index for Gemini Code Assist. -~> **Warning:** This resource is in beta, and should be used with the terraform-provider-google-beta provider. -See [Provider Versions](https://terraform.io/docs/providers/google/guides/provider_versions.html) for more details on beta resources. +To get more information about CodeRepositoryIndex, see: + +* [API documentation](https://cloud.google.com/gemini/docs/api/reference/rest/v1/projects.locations.codeRepositoryIndexes) +* How-to Guides + * [Gemini Code Assist overview](https://cloud.google.com/gemini/docs/codeassist/overview) ## Example Usage - Gemini Code Repository Index Basic ```hcl resource "google_gemini_code_repository_index" "example" { - provider = google-beta location = "us-central1" - code_repository_index_id = "" + code_repository_index_id = "code-repository-index-example" kms_key = "projects/projectExample/locations/locationExample/keyRings/keyRingExample/cryptoKeys/cryptoKeyExample" } ``` @@ -63,7 +65,7 @@ The following arguments are supported: * `kms_key` - (Optional) Optional. Immutable. Customer-managed encryption key name, in the format - projects/*/locations/*/keyRings/*/cryptoKeys/*. + `projects/*/locations/*/keyRings/*/cryptoKeys/*`. * `project` - (Optional) The ID of the project in which the resource belongs. If it is not provided, the provider project is used. @@ -81,12 +83,7 @@ In addition to the arguments listed above, the following computed attributes are * `state` - Output only. Code Repository Index instance State. - Possible values: - STATE_UNSPECIFIED - CREATING - ACTIVE - DELETING - SUSPENDED + Possible values are: `STATE_UNSPECIFIED`, `CREATING`, `ACTIVE`, `DELETING`, `SUSPENDED`. * `name` - Immutable. Identifier. Name of Code Repository Index. diff --git a/website/docs/r/gemini_repository_group.html.markdown b/website/docs/r/gemini_repository_group.html.markdown index b25dfbb06c2..00083aa50d2 100644 --- a/website/docs/r/gemini_repository_group.html.markdown +++ b/website/docs/r/gemini_repository_group.html.markdown @@ -21,19 +21,19 @@ description: |- The resource for managing Repository Group for Gemini Code Assist. -~> **Warning:** This resource is in beta, and should be used with the terraform-provider-google-beta provider. -See [Provider Versions](https://terraform.io/docs/providers/google/guides/provider_versions.html) for more details on beta resources. +To get more information about RepositoryGroup, see: + +* [API documentation](https://cloud.google.com/gemini/docs/api/reference/rest/v1/projects.locations.codeRepositoryIndexes.repositoryGroups) ## Example Usage - Gemini Repository Group Basic ```hcl resource "google_gemini_repository_group" "example" { - provider = google-beta location = "us-central1" - code_repository_index = "%{cri_id}" - repository_group_id = "gen-repository-group-" + code_repository_index = "example-cri" + repository_group_id = "example-repository-group" repositories { resource = "projects/example-project/locations/us-central1/connections/example-connection/gitRepositoryLinks/example-repo" branch_pattern = "main" @@ -49,7 +49,7 @@ The following arguments are supported: * `repositories` - (Required) - Required. List of repositories to group + Required. List of repositories to group. Structure is [documented below](#nested_repositories). * `location` - @@ -82,7 +82,7 @@ The following arguments are supported: * `labels` - (Optional) - Optional. Labels as key value pairs + Optional. Labels as key value pairs. **Note**: This field is non-authoritative, and will only manage the labels present in your configuration. Please refer to the field `effective_labels` for all of the labels present on the resource. @@ -97,13 +97,13 @@ In addition to the arguments listed above, the following computed attributes are * `id` - an identifier for the resource with format `projects/{{project}}/locations/{{location}}/codeRepositoryIndexes/{{code_repository_index}}/repositoryGroups/{{repository_group_id}}` * `name` - - Immutable. Identifier. name of resource + Immutable. Identifier. Name of Repository Group. * `create_time` - - Output only. Create time stamp + Output only. Create time stamp. * `update_time` - - Output only. Update time stamp + Output only. Update time stamp. * `terraform_labels` - The combination of labels configured directly on the resource diff --git a/website/docs/r/gemini_repository_group_iam.html.markdown b/website/docs/r/gemini_repository_group_iam.html.markdown index 93eb25467b2..2ae4c1f5db0 100644 --- a/website/docs/r/gemini_repository_group_iam.html.markdown +++ b/website/docs/r/gemini_repository_group_iam.html.markdown @@ -33,14 +33,11 @@ A data source can be used to retrieve policy data in advent you do not need crea ~> **Note:** `google_gemini_repository_group_iam_binding` resources **can be** used in conjunction with `google_gemini_repository_group_iam_member` resources **only if** they do not grant privilege to the same role. -~> **Warning:** This resource is in beta, and should be used with the terraform-provider-google-beta provider. -See [Provider Versions](https://terraform.io/docs/providers/google/guides/provider_versions.html) for more details on beta resources. ## google_gemini_repository_group_iam_policy ```hcl data "google_iam_policy" "admin" { - provider = google-beta binding { role = "roles/cloudaicompanion.repositoryGroupsUser" members = [ @@ -50,7 +47,6 @@ data "google_iam_policy" "admin" { } resource "google_gemini_repository_group_iam_policy" "policy" { - provider = google-beta project = google_gemini_repository_group.example.project location = google_gemini_repository_group.example.location code_repository_index = google_gemini_repository_group.example.code_repository_index @@ -63,7 +59,6 @@ resource "google_gemini_repository_group_iam_policy" "policy" { ```hcl resource "google_gemini_repository_group_iam_binding" "binding" { - provider = google-beta project = google_gemini_repository_group.example.project location = google_gemini_repository_group.example.location code_repository_index = google_gemini_repository_group.example.code_repository_index @@ -79,7 +74,6 @@ resource "google_gemini_repository_group_iam_binding" "binding" { ```hcl resource "google_gemini_repository_group_iam_member" "member" { - provider = google-beta project = google_gemini_repository_group.example.project location = google_gemini_repository_group.example.location code_repository_index = google_gemini_repository_group.example.code_repository_index