diff --git a/castai/data_source_omni_cluster.go b/castai/data_source_omni_cluster.go new file mode 100644 index 000000000..dca3440b9 --- /dev/null +++ b/castai/data_source_omni_cluster.go @@ -0,0 +1,159 @@ +package castai + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ datasource.DataSource = (*omniClusterDataSource)(nil) + _ datasource.DataSourceWithConfigure = (*omniClusterDataSource)(nil) +) + +type omniClusterDataSource struct { + client *ProviderConfig +} + +type omniClusterDataSourceModel struct { + ID types.String `tfsdk:"id"` + OrganizationID types.String `tfsdk:"organization_id"` + ClusterID types.String `tfsdk:"cluster_id"` + Name types.String `tfsdk:"name"` + State types.String `tfsdk:"state"` + ProviderType types.String `tfsdk:"provider_type"` + ServiceAccountID types.String `tfsdk:"service_account_id"` + CastaiOidcConfig *castaiOidcConfigModel `tfsdk:"castai_oidc_config"` +} + +type castaiOidcConfigModel struct { + GcpServiceAccountEmail types.String `tfsdk:"gcp_service_account_email"` + GcpServiceAccountUniqueID types.String `tfsdk:"gcp_service_account_unique_id"` +} + +func newOmniClusterDataSource() datasource.DataSource { + return &omniClusterDataSource{} +} + +func (d *omniClusterDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_omni_cluster" +} + +func (d *omniClusterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Retrieve information about a CAST AI Omni cluster", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "Cluster ID (same as cluster_id)", + }, + "organization_id": schema.StringAttribute{ + Required: true, + Description: "CAST AI organization ID", + }, + "cluster_id": schema.StringAttribute{ + Required: true, + Description: "CAST AI cluster ID", + }, + "name": schema.StringAttribute{ + Computed: true, + Description: "Name of the cluster", + }, + "state": schema.StringAttribute{ + Computed: true, + Description: "State of the cluster on API level", + }, + "provider_type": schema.StringAttribute{ + Computed: true, + Description: "Provider type of the cluster (e.g. GKE, EKS)", + }, + "service_account_id": schema.StringAttribute{ + Computed: true, + Description: "CAST AI service account ID associated with OMNI operations", + }, + "castai_oidc_config": schema.SingleNestedAttribute{ + Computed: true, + Description: "CAST AI OIDC configuration for service account impersonation", + Attributes: map[string]schema.Attribute{ + "gcp_service_account_email": schema.StringAttribute{ + Computed: true, + Description: "CAST AI GCP service account email for impersonation", + }, + "gcp_service_account_unique_id": schema.StringAttribute{ + Computed: true, + Description: "CAST AI GCP service account unique ID for impersonation", + }, + }, + }, + }, + } +} + +func (d *omniClusterDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*ProviderConfig) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *ProviderConfig, got: %T", req.ProviderData), + ) + return + } + + d.client = client +} + +func (d *omniClusterDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data omniClusterDataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + client := d.client.omniAPI + organizationID := data.OrganizationID.ValueString() + clusterID := data.ClusterID.ValueString() + + apiResp, err := client.ClustersAPIGetClusterWithResponse(ctx, organizationID, clusterID, nil) + if err != nil { + resp.Diagnostics.AddError("Failed to read omni cluster", err.Error()) + return + } + + if apiResp.StatusCode() != http.StatusOK { + resp.Diagnostics.AddError( + "Failed to read omni cluster", + fmt.Sprintf("unexpected status code: %d, body: %s", apiResp.StatusCode(), string(apiResp.Body)), + ) + return + } + + cluster := apiResp.JSON200 + + data.ID = types.StringValue(clusterID) + data.Name = types.StringPointerValue(cluster.Name) + data.ServiceAccountID = types.StringPointerValue(cluster.ServiceAccountId) + + if cluster.State != nil { + data.State = types.StringValue(string(*cluster.State)) + } + if cluster.ProviderType != nil { + data.ProviderType = types.StringValue(string(*cluster.ProviderType)) + } + + if cluster.CastaiOidcConfig != nil { + data.CastaiOidcConfig = &castaiOidcConfigModel{ + GcpServiceAccountEmail: types.StringPointerValue(cluster.CastaiOidcConfig.GcpServiceAccountEmail), + GcpServiceAccountUniqueID: types.StringPointerValue(cluster.CastaiOidcConfig.GcpServiceAccountUniqueId), + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/castai/data_source_omni_cluster_test.go b/castai/data_source_omni_cluster_test.go new file mode 100644 index 000000000..5c1adb58d --- /dev/null +++ b/castai/data_source_omni_cluster_test.go @@ -0,0 +1,50 @@ +package castai + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccCloudAgnostic_DataSourceOmniCluster(t *testing.T) { + clusterName := "omni-tf-acc-gcp" + dataSourceName := "data.castai_omni_cluster.test" + resourceName := "castai_omni_cluster.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccOmniClusterDataSourceConfig(clusterName), + Check: resource.ComposeTestCheckFunc( + // Verify data source ID matches the resource ID + resource.TestCheckResourceAttrPair(dataSourceName, "id", resourceName, "id"), + // Verify organization_id is correctly passed through + resource.TestCheckResourceAttr(dataSourceName, "organization_id", testAccGetOrganizationID()), + // Verify cluster metadata fields are populated + resource.TestCheckResourceAttrSet(dataSourceName, "name"), + resource.TestCheckResourceAttrSet(dataSourceName, "state"), + // Verify OIDC config fields have expected formats + resource.TestMatchResourceAttr(dataSourceName, "castai_oidc_config.gcp_service_account_email", + regexp.MustCompile(`^.+@.+\.iam\.gserviceaccount\.com$`)), + resource.TestMatchResourceAttr(dataSourceName, "castai_oidc_config.gcp_service_account_unique_id", + regexp.MustCompile(`^\d+$`)), + ), + }, + }, + }) +} + +func testAccOmniClusterDataSourceConfig(clusterName string) string { + organizationID := testAccGetOrganizationID() + + return ConfigCompose(testOmniClusterConfig(clusterName), fmt.Sprintf(` +data "castai_omni_cluster" "test" { + organization_id = %[1]q + cluster_id = castai_omni_cluster.test.id +} +`, organizationID)) +} diff --git a/castai/provider_framework.go b/castai/provider_framework.go index a962aed3b..a3900b9a9 100644 --- a/castai/provider_framework.go +++ b/castai/provider_framework.go @@ -132,5 +132,7 @@ func (p *frameworkProvider) Resources(_ context.Context) []func() resource.Resou } func (p *frameworkProvider) DataSources(_ context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{} + return []func() datasource.DataSource{ + newOmniClusterDataSource, + } } diff --git a/castai/resource_edge_location.go b/castai/resource_edge_location.go index a2e2a901a..da2c18d28 100644 --- a/castai/resource_edge_location.go +++ b/castai/resource_edge_location.go @@ -57,9 +57,11 @@ type zoneModel struct { type awsModel struct { AccountID types.String `tfsdk:"account_id"` InstanceProfile types.String `tfsdk:"instance_profile"` + RoleArn types.String `tfsdk:"role_arn"` AccessKeyIDWO types.String `tfsdk:"access_key_id_wo"` SecretAccessKeyWO types.String `tfsdk:"secret_access_key_wo"` VpcID types.String `tfsdk:"vpc_id"` + VpcCidr types.String `tfsdk:"vpc_cidr"` VpcPeered types.Bool `tfsdk:"vpc_peered"` SecurityGroupID types.String `tfsdk:"security_group_id"` SubnetIDs types.Map `tfsdk:"subnet_ids"` @@ -70,9 +72,11 @@ type awsModel struct { type gcpModel struct { ProjectID types.String `tfsdk:"project_id"` InstanceServiceAccount types.String `tfsdk:"instance_service_account"` + TargetServiceAccountEmail types.String `tfsdk:"target_service_account_email"` ClientServiceAccountJSONBase64WO types.String `tfsdk:"client_service_account_json_base64_wo"` NetworkName types.String `tfsdk:"network_name"` SubnetName types.String `tfsdk:"subnet_name"` + SubnetCidr types.String `tfsdk:"subnet_cidr"` NetworkTags types.Set `tfsdk:"network_tags"` } @@ -82,11 +86,19 @@ type ociModel struct { UserIDWO types.String `tfsdk:"user_id_wo"` FingerprintWO types.String `tfsdk:"fingerprint_wo"` PrivateKeyBase64WO types.String `tfsdk:"private_key_base64_wo"` + ClientID types.String `tfsdk:"client_id"` + ClientSecretWO types.String `tfsdk:"client_secret_wo"` + IdentityDomainUri types.String `tfsdk:"identity_domain_uri"` VcnID types.String `tfsdk:"vcn_id"` + VcnCidr types.String `tfsdk:"vcn_cidr"` SubnetID types.String `tfsdk:"subnet_id"` + SecurityGroupID types.String `tfsdk:"security_group_id"` } func (m awsModel) credentials() types.String { + if m.AccessKeyIDWO.IsNull() && m.SecretAccessKeyWO.IsNull() { + return types.StringNull() + } return types.StringValue(m.SecretAccessKeyWO.String() + m.AccessKeyIDWO.String()) } @@ -95,7 +107,9 @@ func (m awsModel) Equal(other *awsModel) bool { return false } return m.AccountID.Equal(other.AccountID) && + m.RoleArn.Equal(other.RoleArn) && m.VpcID.Equal(other.VpcID) && + m.VpcCidr.Equal(other.VpcCidr) && m.VpcPeered.Equal(other.VpcPeered) && m.SecurityGroupID.Equal(other.SecurityGroupID) && m.SubnetIDs.Equal(other.SubnetIDs) && @@ -103,6 +117,9 @@ func (m awsModel) Equal(other *awsModel) bool { } func (m gcpModel) credentials() types.String { + if m.ClientServiceAccountJSONBase64WO.IsNull() { + return types.StringNull() + } return m.ClientServiceAccountJSONBase64WO } @@ -111,14 +128,24 @@ func (m gcpModel) Equal(other *gcpModel) bool { return false } return m.ProjectID.Equal(other.ProjectID) && + m.TargetServiceAccountEmail.Equal(other.TargetServiceAccountEmail) && m.NetworkName.Equal(other.NetworkName) && m.SubnetName.Equal(other.SubnetName) && + m.SubnetCidr.Equal(other.SubnetCidr) && m.NetworkTags.Equal(other.NetworkTags) && m.InstanceServiceAccount.Equal(other.InstanceServiceAccount) } func (m ociModel) credentials() types.String { - return types.StringValue(m.UserIDWO.String() + m.PrivateKeyBase64WO.String() + m.FingerprintWO.String()) + // WIF flow: track client secret + if !m.ClientSecretWO.IsNull() { + return m.ClientSecretWO + } + // Legacy API key flow: track user+key+fingerprint + if !m.UserIDWO.IsNull() || !m.FingerprintWO.IsNull() || !m.PrivateKeyBase64WO.IsNull() { + return types.StringValue(m.UserIDWO.String() + m.PrivateKeyBase64WO.String() + m.FingerprintWO.String()) + } + return types.StringNull() } func (m ociModel) Equal(other *ociModel) bool { @@ -127,8 +154,12 @@ func (m ociModel) Equal(other *ociModel) bool { } return m.TenancyID.Equal(other.TenancyID) && m.CompartmentID.Equal(other.CompartmentID) && + m.ClientID.Equal(other.ClientID) && + m.IdentityDomainUri.Equal(other.IdentityDomainUri) && + m.VcnID.Equal(other.VcnID) && + m.VcnCidr.Equal(other.VcnCidr) && m.SubnetID.Equal(other.SubnetID) && - m.VcnID.Equal(other.VcnID) + m.SecurityGroupID.Equal(other.SecurityGroupID) } type ModelWithCredentials interface { @@ -231,14 +262,18 @@ func (r *edgeLocationResource) Schema(_ context.Context, _ resource.SchemaReques Optional: true, Description: "AWS IAM instance profile ARN to be attached to edge instances. It can be used to grant permissions to access other AWS resources such as ECR.", }, + "role_arn": schema.StringAttribute{ + Optional: true, + Description: "AWS IAM role ARN used for Google OIDC federation impersonation", + }, "access_key_id_wo": schema.StringAttribute{ - Required: true, + Optional: true, WriteOnly: true, Sensitive: true, Description: "AWS access key ID", }, "secret_access_key_wo": schema.StringAttribute{ - Required: true, + Optional: true, Sensitive: true, WriteOnly: true, Description: "AWS secret access key", @@ -247,6 +282,10 @@ func (r *edgeLocationResource) Schema(_ context.Context, _ resource.SchemaReques Required: true, Description: "VPC ID to be used in the selected region", }, + "vpc_cidr": schema.StringAttribute{ + Optional: true, + Description: "VPC IPv4 CIDR block", + }, "vpc_peered": schema.BoolAttribute{ Optional: true, Description: "Whether existing VPC is peered with main cluster's VPC. Field is ignored if vpc_id is not provided or main cluster is not EKS", @@ -279,8 +318,12 @@ func (r *edgeLocationResource) Schema(_ context.Context, _ resource.SchemaReques Optional: true, Description: "GCP service account email to be attached to edge instances. It can be used to grant permissions to access other GCP resources.", }, + "target_service_account_email": schema.StringAttribute{ + Optional: true, + Description: "Target service account email to be used for impersonation", + }, "client_service_account_json_base64_wo": schema.StringAttribute{ - Required: true, + Optional: true, Sensitive: true, WriteOnly: true, Description: "Base64 encoded service account JSON for provisioning edge resources", @@ -296,6 +339,10 @@ func (r *edgeLocationResource) Schema(_ context.Context, _ resource.SchemaReques Required: true, Description: "The name of the subnetwork to be used in the selected region", }, + "subnet_cidr": schema.StringAttribute{ + Optional: true, + Description: "VPC Subnet IPv4 CIDR block", + }, "network_tags": schema.SetAttribute{ Required: true, ElementType: types.StringType, @@ -315,20 +362,34 @@ func (r *edgeLocationResource) Schema(_ context.Context, _ resource.SchemaReques Required: true, Description: "OCI compartment ID of edge location", }, + "client_id": schema.StringAttribute{ + Optional: true, + Description: "ID of the OCI confidential application used for WIF token exchange", + }, + "client_secret_wo": schema.StringAttribute{ + Optional: true, + Sensitive: true, + WriteOnly: true, + Description: "Secret of the OCI confidential application used for WIF token exchange", + }, + "identity_domain_uri": schema.StringAttribute{ + Optional: true, + Description: "OCI Identity Domain URI (e.g., idcs-xxxx.identity.oraclecloud.com)", + }, "user_id_wo": schema.StringAttribute{ - Required: true, + Optional: true, Description: "User ID used to authenticate OCI", WriteOnly: true, }, "fingerprint_wo": schema.StringAttribute{ - Required: true, + Optional: true, Sensitive: true, WriteOnly: true, Description: "API key fingerprint", }, "private_key_base64_wo": schema.StringAttribute{ WriteOnly: true, - Required: true, + Optional: true, Sensitive: true, Description: "Base64 encoded API private key", Validators: []validator.String{ @@ -339,10 +400,18 @@ func (r *edgeLocationResource) Schema(_ context.Context, _ resource.SchemaReques Required: true, Description: "OCI virtual cloud network ID", }, + "vcn_cidr": schema.StringAttribute{ + Optional: true, + Description: "OCI VCN IPv4 CIDR block", + }, "subnet_id": schema.StringAttribute{ Required: true, Description: "OCI subnet ID of edge location", }, + "security_group_id": schema.StringAttribute{ + Optional: true, + Description: "OCI network security group ID", + }, }, }, }, @@ -426,8 +495,10 @@ func (r *edgeLocationResource) Create(ctx context.Context, req resource.CreateRe plan.ID = types.StringValue(*apiResp.JSON200.Id) plan.CredentialsRevision = types.Int64Value(1) - // Store credential hash in private state - resp.Diagnostics.Append(r.woCredentialsStore(resp.Private).Set(ctx, mc.credentials())...) + // Store credential hash in private state (skip when credentials are missing e.g. OIDC flow with AWS and GCP) + if mc != nil && !mc.credentials().IsNull() { + resp.Diagnostics.Append(r.woCredentialsStore(resp.Private).Set(ctx, mc.credentials())...) + } resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) } @@ -561,8 +632,8 @@ func (r *edgeLocationResource) Update(ctx context.Context, req resource.UpdateRe return } - // Update stored credentials hash if credentials changed - if credentialsChanged { + // Update stored credentials hash if credentials changed (skip when using impersonation flow) + if credentialsChanged && mc != nil && !mc.credentials().IsNull() { resp.Diagnostics.Append(r.woCredentialsStore(resp.Private).Set(ctx, mc.credentials())...) } @@ -613,27 +684,29 @@ func (r *edgeLocationResource) ModifyPlan(ctx context.Context, req resource.Modi return } - var ( - credentialsEqual bool - diags diag.Diagnostics - ) + var mc ModelWithCredentials switch { case config.AWS != nil: - credentialsEqual, diags = r.woCredentialsStore(req.Private).Equal(ctx, config.AWS.credentials()) + mc = config.AWS case config.GCP != nil: - credentialsEqual, diags = r.woCredentialsStore(req.Private).Equal(ctx, config.GCP.credentials()) + mc = config.GCP case config.OCI != nil: - credentialsEqual, diags = r.woCredentialsStore(req.Private).Equal(ctx, config.OCI.credentials()) - } - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return + mc = config.OCI } - // If credentials changed, update the planned credentials_revision - if !credentialsEqual { - plan.CredentialsRevision = types.Int64Value(state.CredentialsRevision.ValueInt64() + 1) - resp.Diagnostics.Append(resp.Plan.Set(ctx, plan)...) + // Skip credentials comparison when no credentials are provided (e.g. impersonation flow) + if mc != nil && !mc.credentials().IsNull() { + credentialsEqual, diags := r.woCredentialsStore(req.Private).Equal(ctx, mc.credentials()) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // If credentials changed, update the planned credentials_revision + if !credentialsEqual { + plan.CredentialsRevision = types.Int64Value(state.CredentialsRevision.ValueInt64() + 1) + resp.Diagnostics.Append(resp.Plan.Set(ctx, plan)...) + } } } @@ -704,17 +777,21 @@ func (r *edgeLocationResource) toAWS(ctx context.Context, plan, config *awsModel out := &omni.AWSParam{ AccountId: toPtr(plan.AccountID.ValueString()), InstanceProfile: instanceProfile, - Credentials: &omni.AWSParamCredentials{ - AccessKeyId: config.AccessKeyIDWO.ValueStringPointer(), - SecretAccessKey: config.SecretAccessKeyWO.ValueStringPointer(), - }, + RoleArn: plan.RoleArn.ValueStringPointer(), Networking: &omni.AWSParamAWSNetworking{ VpcId: plan.VpcID.ValueString(), VpcPeered: plan.VpcPeered.ValueBoolPointer(), + VpcCidr: plan.VpcCidr.ValueStringPointer(), SecurityGroupId: plan.SecurityGroupID.ValueString(), SubnetIds: subnetMap, }, } + if !config.AccessKeyIDWO.IsNull() || !config.SecretAccessKeyWO.IsNull() { + out.Credentials = &omni.AWSParamCredentials{ + AccessKeyId: config.AccessKeyIDWO.ValueStringPointer(), + SecretAccessKey: config.SecretAccessKeyWO.ValueStringPointer(), + } + } if !plan.NameTag.IsNull() { out.Networking.NameTag = lo.ToPtr(plan.NameTag.ValueString()) } @@ -731,7 +808,9 @@ func (r *edgeLocationResource) toAWSModel(ctx context.Context, config *omni.AWSP aws := &awsModel{ AccountID: types.StringValue(lo.FromPtr(config.AccountId)), InstanceProfile: types.StringNull(), + RoleArn: types.StringNull(), VpcID: types.StringNull(), + VpcCidr: types.StringNull(), SecurityGroupID: types.StringNull(), VpcPeered: types.BoolNull(), SubnetIDs: types.MapNull(types.StringType), @@ -744,10 +823,17 @@ func (r *edgeLocationResource) toAWSModel(ctx context.Context, config *omni.AWSP aws.InstanceProfile = types.StringValue(*config.InstanceProfile) } + if config.RoleArn != nil && *config.RoleArn != "" { + aws.RoleArn = types.StringValue(*config.RoleArn) + } + if config.Networking != nil { aws.VpcID = types.StringValue(config.Networking.VpcId) aws.VpcPeered = types.BoolPointerValue(config.Networking.VpcPeered) aws.SecurityGroupID = types.StringValue(config.Networking.SecurityGroupId) + if config.Networking.VpcCidr != nil && *config.Networking.VpcCidr != "" { + aws.VpcCidr = types.StringValue(*config.Networking.VpcCidr) + } if config.Networking.NameTag != nil && *config.Networking.NameTag != "" { aws.NameTag = types.StringValue(*config.Networking.NameTag) } @@ -776,17 +862,21 @@ func (r *edgeLocationResource) toGCP(ctx context.Context, plan, config *gcpModel } out := &omni.GCPParam{ - ProjectId: plan.ProjectID.ValueString(), - InstanceServiceAccount: plan.InstanceServiceAccount.ValueStringPointer(), - Credentials: &omni.GCPParamCredentials{ - ClientServiceAccountJsonBase64: config.ClientServiceAccountJSONBase64WO.ValueStringPointer(), - }, + ProjectId: plan.ProjectID.ValueString(), + InstanceServiceAccount: plan.InstanceServiceAccount.ValueStringPointer(), + TargetServiceAccountEmail: plan.TargetServiceAccountEmail.ValueStringPointer(), Networking: &omni.GCPParamGCPNetworking{ NetworkName: plan.NetworkName.ValueString(), SubnetName: plan.SubnetName.ValueString(), + SubnetCidr: plan.SubnetCidr.ValueStringPointer(), Tags: tags, }, } + if !config.ClientServiceAccountJSONBase64WO.IsNull() { + out.Credentials = &omni.GCPParamCredentials{ + ClientServiceAccountJsonBase64: config.ClientServiceAccountJSONBase64WO.ValueStringPointer(), + } + } return out, diags } @@ -800,9 +890,11 @@ func (r *edgeLocationResource) toGCPModel(ctx context.Context, config *omni.GCPP gcp := &gcpModel{ ProjectID: types.StringValue(config.ProjectId), InstanceServiceAccount: types.StringNull(), + TargetServiceAccountEmail: types.StringNull(), ClientServiceAccountJSONBase64WO: types.StringNull(), NetworkName: types.StringNull(), SubnetName: types.StringNull(), + SubnetCidr: types.StringNull(), NetworkTags: types.SetNull(types.StringType), } @@ -810,9 +902,16 @@ func (r *edgeLocationResource) toGCPModel(ctx context.Context, config *omni.GCPP gcp.InstanceServiceAccount = types.StringValue(*config.InstanceServiceAccount) } + if config.TargetServiceAccountEmail != nil && *config.TargetServiceAccountEmail != "" { + gcp.TargetServiceAccountEmail = types.StringValue(*config.TargetServiceAccountEmail) + } + if config.Networking != nil { gcp.NetworkName = types.StringValue(config.Networking.NetworkName) gcp.SubnetName = types.StringValue(config.Networking.SubnetName) + if config.Networking.SubnetCidr != nil && *config.Networking.SubnetCidr != "" { + gcp.SubnetCidr = types.StringValue(*config.Networking.SubnetCidr) + } if config.Networking.Tags != nil { tagsSet, d := types.SetValueFrom(ctx, types.StringType, config.Networking.Tags) diags.Append(d...) @@ -831,15 +930,30 @@ func (r *edgeLocationResource) toOCI(plan, config *ociModel) *omni.OCIParam { out := &omni.OCIParam{ TenancyId: toPtr(plan.TenancyID.ValueString()), CompartmentId: toPtr(plan.CompartmentID.ValueString()), - Credentials: &omni.OCIParamCredentials{ + Networking: &omni.OCIParamNetworking{ + VcnId: plan.VcnID.ValueString(), + SubnetId: plan.SubnetID.ValueString(), + VcnCidr: plan.VcnCidr.ValueStringPointer(), + SecurityGroupId: plan.SecurityGroupID.ValueStringPointer(), + }, + } + + // WIF flow: use confidential application client + if !plan.ClientID.IsNull() { + out.Client = &omni.OCIParamClient{ + Id: plan.ClientID.ValueString(), + Secret: config.ClientSecretWO.ValueStringPointer(), + IdentityDomainUri: plan.IdentityDomainUri.ValueString(), + } + } + + // Legacy API key flow + if !config.UserIDWO.IsNull() || !config.FingerprintWO.IsNull() || !config.PrivateKeyBase64WO.IsNull() { + out.Credentials = &omni.OCIParamCredentials{ UserId: config.UserIDWO.ValueString(), Fingerprint: config.FingerprintWO.ValueString(), PrivateKeyBase64: config.PrivateKeyBase64WO.ValueString(), - }, - Networking: &omni.OCIParamNetworking{ - VcnId: plan.VcnID.ValueString(), - SubnetId: plan.SubnetID.ValueString(), - }, + } } return out @@ -853,15 +967,32 @@ func (r *edgeLocationResource) toOCIModel(config *omni.OCIParam) *ociModel { oci := &ociModel{ TenancyID: types.StringValue(lo.FromPtr(config.TenancyId)), CompartmentID: types.StringValue(lo.FromPtr(config.CompartmentId)), + ClientID: types.StringNull(), + IdentityDomainUri: types.StringNull(), + ClientSecretWO: types.StringNull(), VcnID: types.StringNull(), + VcnCidr: types.StringNull(), SubnetID: types.StringNull(), + SecurityGroupID: types.StringNull(), UserIDWO: types.StringNull(), FingerprintWO: types.StringNull(), PrivateKeyBase64WO: types.StringNull(), } + + if config.Client != nil { + oci.ClientID = types.StringValue(config.Client.Id) + oci.IdentityDomainUri = types.StringValue(config.Client.IdentityDomainUri) + } + if config.Networking != nil { - oci.SubnetID = types.StringValue(config.Networking.SubnetId) oci.VcnID = types.StringValue(config.Networking.VcnId) + oci.SubnetID = types.StringValue(config.Networking.SubnetId) + if config.Networking.VcnCidr != nil && *config.Networking.VcnCidr != "" { + oci.VcnCidr = types.StringValue(*config.Networking.VcnCidr) + } + if config.Networking.SecurityGroupId != nil && *config.Networking.SecurityGroupId != "" { + oci.SecurityGroupID = types.StringValue(*config.Networking.SecurityGroupId) + } } return oci diff --git a/castai/resource_edge_location_test.go b/castai/resource_edge_location_test.go index 50c277532..6e2dc8e93 100644 --- a/castai/resource_edge_location_test.go +++ b/castai/resource_edge_location_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "strings" "testing" "time" @@ -191,20 +192,7 @@ func testAccEdgeLocationAWSCredentialsUpdated(rName, clusterName string) string func testAccEdgeLocationAWSConfigWithParams(rName, clusterName, description string, zones []string, awsCredentials string) string { organizationID := testAccGetOrganizationID() - zonesConfig := "zones = [" - subnetConfig := "" - for i, zone := range zones { - if i > 0 { - zonesConfig += ", " - } - zonesConfig += fmt.Sprintf(`{ - id = %q - name = %q - }`, zone, zone) - subnetConfig += fmt.Sprintf(` - %q = "subnet-%08d"`, zone, 12345678+i) - } - zonesConfig += "]" + zonesConfig, subnetConfig := formatAWSZonesAndSubnets(zones) return ConfigCompose(testOmniClusterConfig(clusterName), fmt.Sprintf(` resource "castai_edge_location" "test" { @@ -276,25 +264,8 @@ func testAccEdgeLocationGCPCredentialsUpdated(rName, clusterName string) string func testAccEdgeLocationGCPConfigWithParams(rName, clusterName, description string, zones []string, networkTags []string, gcpCredentials string) string { organizationID := testAccGetOrganizationID() - zonesConfig := "zones = [" - for i, zone := range zones { - if i > 0 { - zonesConfig += ", " - } - zonesConfig += fmt.Sprintf(`{ - id = %q - name = %q - }`, zone, zone) - } - zonesConfig += "]" - - networkTagsConfig := "" - for i, tag := range networkTags { - if i > 0 { - networkTagsConfig += ", " - } - networkTagsConfig += fmt.Sprintf("%q", tag) - } + zonesConfig := formatGCPZones(zones) + networkTagsConfig := formatGCPNetworkTags(networkTags) return ConfigCompose(testOmniClusterConfig(clusterName), fmt.Sprintf(` resource "castai_edge_location" "test" { @@ -365,6 +336,337 @@ resource "castai_edge_location" "test" { `, rName, description, organizationID, ociCredentials)) } +// New credential-less flow tests +// When the legacy credential fields are removed, the tests above can be deleted + +func TestAccCloudAgnostic_ResourceEdgeLocationAWSImpersonation(t *testing.T) { + rName := fmt.Sprintf("%v-edge-loc-%v", ResourcePrefix, acctest.RandString(8)) + resourceName := "castai_edge_location.test" + clusterName := "omni-tf-acc-aws" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckEdgeLocationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccEdgeLocationAWSImpersonationConfig(rName, clusterName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "description", "Test edge location impersonation"), + resource.TestCheckResourceAttr(resourceName, "region", "us-east-1"), + resource.TestCheckResourceAttr(resourceName, "zones.#", "2"), + resource.TestCheckResourceAttr(resourceName, "zones.0.id", "us-east-1a"), + resource.TestCheckResourceAttr(resourceName, "zones.0.name", "us-east-1a"), + resource.TestCheckResourceAttr(resourceName, "zones.1.id", "us-east-1b"), + resource.TestCheckResourceAttr(resourceName, "zones.1.name", "us-east-1b"), + resource.TestCheckResourceAttrSet(resourceName, "aws.account_id"), + resource.TestCheckResourceAttr(resourceName, "aws.role_arn", "arn:aws:iam::123456789012:role/castai-omni-edge"), + resource.TestCheckResourceAttr(resourceName, "aws.vpc_cidr", "10.0.0.0/16"), + resource.TestCheckResourceAttrSet(resourceName, "aws.vpc_peered"), + resource.TestCheckResourceAttrSet(resourceName, "aws.vpc_id"), + resource.TestCheckResourceAttrSet(resourceName, "aws.security_group_id"), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "credentials_revision", "1"), + ), + }, + { + ResourceName: resourceName, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + organizationID := testAccGetOrganizationID() + clusterID := s.RootModule().Resources["castai_omni_cluster.test"].Primary.ID + edgeLocationID := s.RootModule().Resources[resourceName].Primary.ID + return fmt.Sprintf("%v/%v/%v", organizationID, clusterID, edgeLocationID), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccEdgeLocationAWSImpersonationUpdated(rName, clusterName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "description", "Updated edge location impersonation"), + resource.TestCheckResourceAttr(resourceName, "zones.#", "3"), + resource.TestCheckResourceAttr(resourceName, "zones.0.id", "us-east-1a"), + resource.TestCheckResourceAttr(resourceName, "zones.0.name", "us-east-1a"), + resource.TestCheckResourceAttr(resourceName, "zones.1.id", "us-east-1b"), + resource.TestCheckResourceAttr(resourceName, "zones.1.name", "us-east-1b"), + resource.TestCheckResourceAttr(resourceName, "zones.2.id", "us-east-1c"), + resource.TestCheckResourceAttr(resourceName, "zones.2.name", "us-east-1c"), + resource.TestCheckResourceAttr(resourceName, "aws.role_arn", "arn:aws:iam::123456789012:role/castai-omni-edge-updated"), + ), + }, + }, + }) +} + +func TestAccCloudAgnostic_ResourceEdgeLocationGCPImpersonation(t *testing.T) { + rName := fmt.Sprintf("%v-edge-loc-%v", ResourcePrefix, acctest.RandString(8)) + resourceName := "castai_edge_location.test" + clusterName := "omni-tf-acc-gcp" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckEdgeLocationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccEdgeLocationGCPImpersonationConfig(rName, clusterName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "description", "Test GCP edge location impersonation"), + resource.TestCheckResourceAttr(resourceName, "region", "us-central1"), + resource.TestCheckResourceAttr(resourceName, "zones.#", "2"), + resource.TestCheckResourceAttr(resourceName, "zones.0.id", "us-central1-a"), + resource.TestCheckResourceAttr(resourceName, "zones.0.name", "us-central1-a"), + resource.TestCheckResourceAttr(resourceName, "zones.1.id", "us-central1-b"), + resource.TestCheckResourceAttr(resourceName, "zones.1.name", "us-central1-b"), + resource.TestCheckResourceAttrSet(resourceName, "gcp.project_id"), + resource.TestCheckResourceAttr(resourceName, "gcp.target_service_account_email", "castai-omni@test-project-123456.iam.gserviceaccount.com"), + resource.TestCheckResourceAttr(resourceName, "gcp.subnet_cidr", "10.0.0.0/20"), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "credentials_revision", "1"), + ), + }, + { + Config: testAccEdgeLocationGCPImpersonationUpdated(rName, clusterName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "description", "Updated GCP edge location impersonation"), + resource.TestCheckResourceAttr(resourceName, "gcp.target_service_account_email", "castai-omni-updated@test-project-123456.iam.gserviceaccount.com"), + resource.TestCheckResourceAttr(resourceName, "zones.#", "2"), + resource.TestCheckResourceAttr(resourceName, "zones.0.id", "us-central1-a"), + resource.TestCheckResourceAttr(resourceName, "zones.0.name", "us-central1-a"), + resource.TestCheckResourceAttr(resourceName, "zones.1.id", "us-central1-b"), + resource.TestCheckResourceAttr(resourceName, "zones.1.name", "us-central1-b"), + ), + }, + }, + }) +} + +func TestAccCloudAgnostic_ResourceEdgeLocationOCIWIF(t *testing.T) { + rName := fmt.Sprintf("%v-edge-loc-%v", ResourcePrefix, acctest.RandString(8)) + resourceName := "castai_edge_location.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckEdgeLocationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccEdgeLocationOCIWIFConfig(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "description", "Test OCI edge location WIF"), + resource.TestCheckResourceAttr(resourceName, "region", "us-phoenix-1"), + resource.TestCheckResourceAttr(resourceName, "zones.#", "1"), + resource.TestCheckResourceAttr(resourceName, "zones.0.id", "1"), + resource.TestCheckResourceAttr(resourceName, "zones.0.name", "PHX-AD-1"), + resource.TestCheckResourceAttrSet(resourceName, "oci.tenancy_id"), + resource.TestCheckResourceAttrSet(resourceName, "oci.compartment_id"), + resource.TestCheckResourceAttr(resourceName, "oci.client_id", "test-app-client-id"), + resource.TestCheckResourceAttr(resourceName, "oci.identity_domain_uri", "idcs-example.identity.oraclecloud.com"), + resource.TestCheckResourceAttr(resourceName, "oci.vcn_cidr", "10.0.0.0/16"), + resource.TestCheckResourceAttrSet(resourceName, "oci.security_group_id"), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "credentials_revision", "1"), + ), + }, + { + Config: testAccEdgeLocationOCIWIFUpdated(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "description", "Updated OCI edge location WIF"), + ), + }, + // Rotate the client secret: credentials_revision must increment + { + Config: testAccEdgeLocationOCIWIFCredentialsUpdated(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "credentials_revision", "2"), + ), + }, + }, + }) +} + +func formatAWSZonesAndSubnets(zones []string) (zonesConfig string, subnetConfig string) { + var zonesBuilder strings.Builder + var subnetBuilder strings.Builder + + zonesBuilder.WriteString("zones = [") + for i, zone := range zones { + if i > 0 { + zonesBuilder.WriteString(", ") + } + fmt.Fprintf(&zonesBuilder, `{ + id = %q + name = %q + }`, zone, zone) + fmt.Fprintf(&subnetBuilder, ` + %q = "subnet-%08d"`, zone, 12345678+i) + } + zonesBuilder.WriteString("]") + + zonesConfig = zonesBuilder.String() + subnetConfig = subnetBuilder.String() + + return +} + +func testAccEdgeLocationAWSImpersonationConfig(rName, clusterName string) string { + return testAccEdgeLocationAWSImpersonationConfigWithParams(rName, clusterName, "Test edge location impersonation", + []string{"us-east-1a", "us-east-1b"}, "arn:aws:iam::123456789012:role/castai-omni-edge") +} + +func testAccEdgeLocationAWSImpersonationUpdated(rName, clusterName string) string { + return testAccEdgeLocationAWSImpersonationConfigWithParams(rName, clusterName, "Updated edge location impersonation", + []string{"us-east-1a", "us-east-1b", "us-east-1c"}, "arn:aws:iam::123456789012:role/castai-omni-edge-updated") +} + +func testAccEdgeLocationAWSImpersonationConfigWithParams(rName, clusterName, description string, zones []string, roleArn string) string { + organizationID := testAccGetOrganizationID() + + zonesConfig, subnetConfig := formatAWSZonesAndSubnets(zones) + + return ConfigCompose(testOmniClusterConfig(clusterName), fmt.Sprintf(` +resource "castai_edge_location" "test" { + organization_id = %[5]q + cluster_id = castai_omni_cluster.test.id + name = %[1]q + description = %[2]q + region = "us-east-1" +%[3]s + + aws = { + account_id = "123456789012" + role_arn = "%[6]s" + vpc_id = "vpc-12345678" + vpc_peered = true + instance_service_account = "arn:aws:iam::123456789012:role/castai-omni-edge" + vpc_cidr = "10.0.0.0/16" + security_group_id = "sg-12345678" + subnet_ids = {%[4]s + } + } +} +`, rName, description, zonesConfig, subnetConfig, organizationID, roleArn)) +} + +func testAccEdgeLocationGCPImpersonationConfig(rName, clusterName string) string { + return testAccEdgeLocationGCPImpersonationConfigWithParams(rName, clusterName, + "Test GCP edge location impersonation", + "castai-omni@test-project-123456.iam.gserviceaccount.com", + []string{"us-central1-a", "us-central1-b"}, + []string{"edge-location", "castai"}) +} + +func testAccEdgeLocationGCPImpersonationUpdated(rName, clusterName string) string { + return testAccEdgeLocationGCPImpersonationConfigWithParams(rName, clusterName, + "Updated GCP edge location impersonation", + "castai-omni-updated@test-project-123456.iam.gserviceaccount.com", + []string{"us-central1-a", "us-central1-b"}, + []string{"edge-location", "castai"}) +} + +func formatGCPZones(zones []string) string { + var builder strings.Builder + + builder.WriteString("zones = [") + for i, zone := range zones { + if i > 0 { + builder.WriteString(", ") + } + fmt.Fprintf(&builder, `{ + id = %q + name = %q + }`, zone, zone) + } + builder.WriteString("]") + return builder.String() +} + +func formatGCPNetworkTags(networkTags []string) string { + var builder strings.Builder + + for i, tag := range networkTags { + if i > 0 { + builder.WriteString(", ") + } + fmt.Fprintf(&builder, "%q", tag) + } + return builder.String() +} + +func testAccEdgeLocationGCPImpersonationConfigWithParams(rName, clusterName, description, targetSA string, zones []string, networkTags []string) string { + organizationID := testAccGetOrganizationID() + + zonesConfig := formatGCPZones(zones) + networkTagsConfig := formatGCPNetworkTags(networkTags) + + return ConfigCompose(testOmniClusterConfig(clusterName), fmt.Sprintf(` +resource "castai_edge_location" "test" { + organization_id = %[5]q + cluster_id = castai_omni_cluster.test.id + name = %[1]q + description = %[2]q + region = "us-central1" +%[3]s + + gcp = { + project_id = "test-project-123456" + instance_service_account = "custom-sa@test-project-123456.iam.gserviceaccount.com" + target_service_account_email = "%[6]s" + network_name = "test-network" + subnet_name = "test-subnet" + subnet_cidr = "10.0.0.0/20" + network_tags = [%[4]s] + } +} +`, rName, description, zonesConfig, networkTagsConfig, organizationID, targetSA)) +} + +func testAccEdgeLocationOCIWIFConfig(rName string) string { + return testAccEdgeLocationOCIWIFConfigWithParams(rName, "Test OCI edge location WIF", "test-app-client-secret") +} + +func testAccEdgeLocationOCIWIFUpdated(rName string) string { + return testAccEdgeLocationOCIWIFConfigWithParams(rName, "Updated OCI edge location WIF", "test-app-client-secret") +} + +func testAccEdgeLocationOCIWIFCredentialsUpdated(rName string) string { + return testAccEdgeLocationOCIWIFConfigWithParams(rName, "Updated OCI edge location WIF", "test-app-client-secret-changed") +} + +func testAccEdgeLocationOCIWIFConfigWithParams(rName, description, clientSecret string) string { + organizationID := testAccGetOrganizationID() + clusterName := "test-oci-cluster" + + return ConfigCompose(testOmniClusterConfig(clusterName), fmt.Sprintf(` +resource "castai_edge_location" "test" { + organization_id = %[3]q + cluster_id = castai_omni_cluster.test.id + name = %[1]q + description = %[2]q + region = "us-phoenix-1" + zones = [{ + id = "1" + name = "PHX-AD-1" + }] + + oci = { + tenancy_id = "ocid1.tenancy.oc1..example" + compartment_id = "ocid1.compartment.oc1..example" + client_id = "test-app-client-id" + client_secret_wo = %[4]q + identity_domain_uri = "idcs-example.identity.oraclecloud.com" + vcn_id = "ocid1.vcn.oc1.phx.example" + vcn_cidr = "10.0.0.0/16" + subnet_id = "ocid1.subnet.oc1.phx.example" + security_group_id = "ocid1.networksecuritygroup.oc1.phx.example" + } +} +`, rName, description, organizationID, clientSecret)) +} + func testAccCheckEdgeLocationDestroy(s *terraform.State) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() diff --git a/docs/data-sources/omni_cluster.md b/docs/data-sources/omni_cluster.md new file mode 100644 index 000000000..3905737e2 --- /dev/null +++ b/docs/data-sources/omni_cluster.md @@ -0,0 +1,40 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "castai_omni_cluster Data Source - terraform-provider-castai" +subcategory: "" +description: |- + Retrieve information about a CAST AI Omni cluster +--- + +# castai_omni_cluster (Data Source) + +Retrieve information about a CAST AI Omni cluster + + + + +## Schema + +### Required + +- `cluster_id` (String) CAST AI cluster ID +- `organization_id` (String) CAST AI organization ID + +### Read-Only + +- `castai_oidc_config` (Attributes) CAST AI OIDC configuration for service account impersonation (see [below for nested schema](#nestedatt--castai_oidc_config)) +- `id` (String) Cluster ID (same as cluster_id) +- `name` (String) Name of the cluster +- `provider_type` (String) Provider type of the cluster (e.g. GKE, EKS) +- `service_account_id` (String) CAST AI service account ID associated with OMNI operations +- `state` (String) State of the cluster on API level + + +### Nested Schema for `castai_oidc_config` + +Read-Only: + +- `gcp_service_account_email` (String) CAST AI GCP service account email for impersonation +- `gcp_service_account_unique_id` (String) CAST AI GCP service account unique ID for impersonation + + diff --git a/docs/resources/edge_location.md b/docs/resources/edge_location.md index 4c2467c72..94f0b7f7b 100644 --- a/docs/resources/edge_location.md +++ b/docs/resources/edge_location.md @@ -75,17 +75,19 @@ resource "castai_edge_location" "aws_example" { Required: -- `access_key_id_wo` (String, Sensitive) AWS access key ID - `account_id` (String) AWS account ID -- `secret_access_key_wo` (String, Sensitive) AWS secret access key - `security_group_id` (String) Security group ID to be used in the selected region - `subnet_ids` (Map of String) Map of zone names to subnet IDs to be used in the selected region - `vpc_id` (String) VPC ID to be used in the selected region Optional: +- `access_key_id_wo` (String, Sensitive) AWS access key ID - `instance_profile` (String) AWS IAM instance profile ARN to be attached to edge instances. It can be used to grant permissions to access other AWS resources such as ECR. - `name_tag` (String, Deprecated) The value of a 'Name' tag applied to VPC resources +- `role_arn` (String) AWS IAM role ARN used for Google OIDC federation impersonation +- `secret_access_key_wo` (String, Sensitive) AWS secret access key +- `vpc_cidr` (String) VPC IPv4 CIDR block - `vpc_peered` (Boolean) Whether existing VPC is peered with main cluster's VPC. Field is ignored if vpc_id is not provided or main cluster is not EKS @@ -94,7 +96,6 @@ Optional: Required: -- `client_service_account_json_base64_wo` (String, Sensitive) Base64 encoded service account JSON for provisioning edge resources - `network_name` (String) The name of the network to be used in the selected region - `network_tags` (Set of String) Tags applied on the provisioned cloud resources and the firewall rule - `project_id` (String) GCP project ID where edges run @@ -102,7 +103,10 @@ Required: Optional: +- `client_service_account_json_base64_wo` (String, Sensitive) Base64 encoded service account JSON for provisioning edge resources - `instance_service_account` (String) GCP service account email to be attached to edge instances. It can be used to grant permissions to access other GCP resources. +- `subnet_cidr` (String) VPC Subnet IPv4 CIDR block +- `target_service_account_email` (String) Target service account email to be used for impersonation @@ -111,13 +115,21 @@ Optional: Required: - `compartment_id` (String) OCI compartment ID of edge location -- `fingerprint_wo` (String, Sensitive) API key fingerprint -- `private_key_base64_wo` (String, Sensitive) Base64 encoded API private key - `subnet_id` (String) OCI subnet ID of edge location - `tenancy_id` (String) OCI tenancy ID of the account -- `user_id_wo` (String) User ID used to authenticate OCI - `vcn_id` (String) OCI virtual cloud network ID +Optional: + +- `client_id` (String) ID of the OCI confidential application used for WIF token exchange +- `client_secret_wo` (String, Sensitive) Secret of the OCI confidential application used for WIF token exchange +- `fingerprint_wo` (String, Sensitive) API key fingerprint +- `identity_domain_uri` (String) OCI Identity Domain URI (e.g., idcs-xxxx.identity.oraclecloud.com) +- `private_key_base64_wo` (String, Sensitive) Base64 encoded API private key +- `security_group_id` (String) OCI network security group ID +- `user_id_wo` (String) User ID used to authenticate OCI +- `vcn_cidr` (String) OCI VCN IPv4 CIDR block + ### Nested Schema for `zones`