diff --git a/betatemplates/resources/davinci_application.md.tmpl b/betatemplates/resources/davinci_application.md.tmpl new file mode 100644 index 000000000..bc3a0e48c --- /dev/null +++ b/betatemplates/resources/davinci_application.md.tmpl @@ -0,0 +1,26 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.RenderedProviderName}}" +subcategory: "DaVinci" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "%s%s%s" "examples/resources/" .Name "/resource.tf") }} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} + +{{ if .HasImport -}} +## Import + +Import is supported using the following syntax, where attributes in `<>` brackets are replaced with the relevant ID. For example, `` should be replaced with the ID of the environment to import from. + +{{ codefile "shell" (printf "%s%s%s" "examples/resources/" .Name "/import.sh") }} +{{- end }} \ No newline at end of file diff --git a/examples/resources/pingone_davinci_application/import.sh b/examples/resources/pingone_davinci_application/import.sh new file mode 100644 index 000000000..728fac71b --- /dev/null +++ b/examples/resources/pingone_davinci_application/import.sh @@ -0,0 +1 @@ +terraform import pingone_davinci_application.example / \ No newline at end of file diff --git a/examples/resources/pingone_davinci_application/resource.tf b/examples/resources/pingone_davinci_application/resource.tf new file mode 100644 index 000000000..8882d8d98 --- /dev/null +++ b/examples/resources/pingone_davinci_application/resource.tf @@ -0,0 +1,44 @@ +resource "pingone_davinci_application" "my_awesome_application" { + environment_id = var.pingone_environment_id + + name = "My Awesome Application" + + oauth = { + grant_types = ["authorizationCode"] + scopes = ["openid", "profile"] + enforce_signed_request_openid = false + redirect_uris = ["https://auth.pingone.com/0000-0000-000/rp/callback/openid_connect"] + } +} + +resource "pingone_davinci_application_flow_policy" "authentication_flow_policy" { + environment_id = var.pingone_environment_id + application_id = pingone_davinci_application.my_awesome_application.id + + name = "PingOne - Authentication" + status = "enabled" + + flow_distributions = [ + { + flow_id = pingone_davinci_flow.authentication.id + version = -1 + weight = 100 + } + ] +} + +resource "pingone_davinci_application_flow_policy" "registration_flow_policy" { + environment_id = var.pingone_environment_id + application_id = pingone_davinci_application.my_awesome_application.id + + name = "PingOne - Registration" + status = "enabled" + + flow_distributions = [ + { + flow_id = pingone_davinci_flow.registration.id + version = -1 + weight = 100 + } + ] +} diff --git a/examples/resources/pingone_davinci_connector_instance/resource.tf b/examples/resources/pingone_davinci_connector_instance/resource.tf index 08deb84ec..cb7a16455 100644 --- a/examples/resources/pingone_davinci_connector_instance/resource.tf +++ b/examples/resources/pingone_davinci_connector_instance/resource.tf @@ -67,4 +67,4 @@ resource "pingone_davinci_connector_instance" "pingfederate_connector_example" { } } }) -} \ No newline at end of file +} diff --git a/internal/framework/resource.go b/internal/framework/resource.go index 6a8b001d7..4a4ee8735 100644 --- a/internal/framework/resource.go +++ b/internal/framework/resource.go @@ -478,6 +478,9 @@ func ParseImportID(id string, components ...ImportComponent) (map[string]string, i := 0 for _, v := range components { + if v.Regexp == nil { + return nil, fmt.Errorf("cannot parse import ID as component %d has no Regexp", i) + } keys[i] = v.Label regexpList[i] = v.Regexp.String() i++ diff --git a/internal/service/davinci/resource_davinci_application.go b/internal/service/davinci/resource_davinci_application.go new file mode 100644 index 000000000..4772f9b61 --- /dev/null +++ b/internal/service/davinci/resource_davinci_application.go @@ -0,0 +1,201 @@ +// Copyright © 2025 Ping Identity Corporation + +//go:build beta + +package davinci + +import ( + "context" + "fmt" + "net/http" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/pingidentity/pingone-go-client/pingone" + "github.com/pingidentity/terraform-provider-pingone/internal/framework" +) + +// Build the PUT client struct to be used after initial creation +func (model *davinciApplicationResourceModel) buildClientStructPutAfterCreate(createResponse *pingone.DaVinciApplicationResponse) (*pingone.DaVinciApplicationReplaceRequest, diag.Diagnostics) { + result := &pingone.DaVinciApplicationReplaceRequest{} + var respDiags diag.Diagnostics + // First copy over values from the create response + result.ApiKeyEnabled = &createResponse.ApiKey.Enabled + var grantTypes []pingone.DaVinciApplicationReplaceRequestOAuthGrantType + for _, grantType := range createResponse.Oauth.GrantTypes { + grantTypes = append(grantTypes, pingone.DaVinciApplicationReplaceRequestOAuthGrantType(grantType)) + } + var scopes []pingone.DaVinciApplicationReplaceRequestOAuthScope + for _, scope := range createResponse.Oauth.Scopes { + scopes = append(scopes, pingone.DaVinciApplicationReplaceRequestOAuthScope(scope)) + } + result.Oauth = &pingone.DaVinciApplicationReplaceRequestOAuth{ + EnforceSignedRequestOpenid: createResponse.Oauth.EnforceSignedRequestOpenid, + GrantTypes: grantTypes, + LogoutUris: createResponse.Oauth.LogoutUris, + RedirectUris: createResponse.Oauth.RedirectUris, + Scopes: scopes, + SpJwksOpenid: createResponse.Oauth.SpJwksOpenid, + SpjwksUrl: createResponse.Oauth.SpjwksUrl, + } + + // Then overwrite with anything specified in the plan model + result.Name = model.Name.ValueString() + if !model.ApiKey.IsNull() && !model.ApiKey.IsUnknown() { + if !model.ApiKey.Attributes()["enabled"].IsNull() && !model.ApiKey.Attributes()["enabled"].IsUnknown() { + result.ApiKeyEnabled = model.ApiKey.Attributes()["enabled"].(types.Bool).ValueBoolPointer() + } + } + if !model.Oauth.IsNull() && !model.Oauth.IsUnknown() { + oauthAttrs := model.Oauth.Attributes() + if !oauthAttrs["enforce_signed_request_openid"].IsNull() && !oauthAttrs["enforce_signed_request_openid"].IsUnknown() { + result.Oauth.EnforceSignedRequestOpenid = oauthAttrs["enforce_signed_request_openid"].(types.Bool).ValueBoolPointer() + } + if !oauthAttrs["grant_types"].IsNull() && !oauthAttrs["grant_types"].IsUnknown() { + result.Oauth.GrantTypes = []pingone.DaVinciApplicationReplaceRequestOAuthGrantType{} + for _, grantTypesElement := range oauthAttrs["grant_types"].(types.Set).Elements() { + var grantTypesValue pingone.DaVinciApplicationReplaceRequestOAuthGrantType + grantTypesEnumValue, err := pingone.NewDaVinciApplicationReplaceRequestOAuthGrantTypeFromValue(grantTypesElement.(types.String).ValueString()) + if err != nil { + respDiags.AddAttributeError( + path.Root("grant_types"), + "Provided value is not valid", + fmt.Sprintf("The value provided for grant_types is not valid: %s", err.Error()), + ) + } else { + grantTypesValue = *grantTypesEnumValue + } + result.Oauth.GrantTypes = append(result.Oauth.GrantTypes, grantTypesValue) + } + } + if !oauthAttrs["logout_uris"].IsNull() && !oauthAttrs["logout_uris"].IsUnknown() { + result.Oauth.LogoutUris = []string{} + for _, logoutUrisElement := range oauthAttrs["logout_uris"].(types.Set).Elements() { + result.Oauth.LogoutUris = append(result.Oauth.LogoutUris, logoutUrisElement.(types.String).ValueString()) + } + } + if !oauthAttrs["redirect_uris"].IsNull() && !oauthAttrs["redirect_uris"].IsUnknown() { + result.Oauth.RedirectUris = []string{} + for _, redirectUrisElement := range oauthAttrs["redirect_uris"].(types.Set).Elements() { + result.Oauth.RedirectUris = append(result.Oauth.RedirectUris, redirectUrisElement.(types.String).ValueString()) + } + } + if !oauthAttrs["scopes"].IsNull() && !oauthAttrs["scopes"].IsUnknown() { + result.Oauth.Scopes = []pingone.DaVinciApplicationReplaceRequestOAuthScope{} + for _, scopesElement := range oauthAttrs["scopes"].(types.Set).Elements() { + var scopesValue pingone.DaVinciApplicationReplaceRequestOAuthScope + scopesEnumValue, err := pingone.NewDaVinciApplicationReplaceRequestOAuthScopeFromValue(scopesElement.(types.String).ValueString()) + if err != nil { + respDiags.AddAttributeError( + path.Root("scopes"), + "Provided value is not valid", + fmt.Sprintf("The value provided for scopes is not valid: %s", err.Error()), + ) + } else { + scopesValue = *scopesEnumValue + } + result.Oauth.Scopes = append(result.Oauth.Scopes, scopesValue) + } + } + if !oauthAttrs["sp_jwks_openid"].IsNull() && !oauthAttrs["sp_jwks_openid"].IsUnknown() { + result.Oauth.SpJwksOpenid = oauthAttrs["sp_jwks_openid"].(types.String).ValueStringPointer() + } + if !oauthAttrs["sp_jwks_url"].IsNull() && !oauthAttrs["sp_jwks_url"].IsUnknown() { + result.Oauth.SpjwksUrl = oauthAttrs["sp_jwks_url"].(types.String).ValueStringPointer() + } + } + + return result, respDiags +} + +// Application Creates have to run a POST followed by a PUT to set fields other than name. +func (r *davinciApplicationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data davinciApplicationResourceModel + + if r.Client == nil { + resp.Diagnostics.AddError( + "Client not initialized", + "Expected the PingOne client, got nil. Please report this issue to the provider maintainers.") + return + } + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Create API call logic + clientData, diags := data.buildClientStructPost() + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + environmentIdUuid, err := uuid.Parse(data.EnvironmentId.ValueString()) + if err != nil { + resp.Diagnostics.AddAttributeError( + path.Root("environment_id"), + "Attribute Validation Error", + fmt.Sprintf("The value '%s' for attribute '%s' is not a valid UUID: %s", data.EnvironmentId.ValueString(), "EnvironmentId", err.Error()), + ) + return + } + var createResponseData *pingone.DaVinciApplicationResponse + resp.Diagnostics.Append(framework.ParseResponse( + ctx, + + func() (any, *http.Response, error) { + fO, fR, fErr := r.Client.DaVinciApplicationsApi.CreateDavinciApplication(ctx, environmentIdUuid).DaVinciApplicationCreateRequest(*clientData).Execute() + return framework.CheckEnvironmentExistsOnPermissionsError(ctx, r.Client, data.EnvironmentId.ValueString(), fO, fR, fErr) + }, + "CreateDavinciApplication", + framework.DefaultCustomError, + framework.DefaultRetryable, + &createResponseData, + )...) + + if resp.Diagnostics.HasError() { + return + } + + // In order to update fields, a second call to the PUT endpoint has to be made, because only name can be set on creation. + // Update API call logic + updateClientData, diags := data.buildClientStructPutAfterCreate(createResponseData) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var responseData *pingone.DaVinciApplicationResponse + resp.Diagnostics.Append(framework.ParseResponse( + ctx, + + func() (any, *http.Response, error) { + fO, fR, fErr := r.Client.DaVinciApplicationsApi.ReplaceDavinciApplicationById(ctx, environmentIdUuid, createResponseData.Id).DaVinciApplicationReplaceRequest(*updateClientData).Execute() + return framework.CheckEnvironmentExistsOnPermissionsError(ctx, r.Client, data.EnvironmentId.ValueString(), fO, fR, fErr) + }, + "ReplaceDavinciApplicationById-Create", + framework.DefaultCustomError, + framework.DefaultRetryable, + &responseData, + )...) + + if resp.Diagnostics.HasError() { + return + } + + // Read update response into the model + resp.Diagnostics.Append(data.readClientResponse(responseData)...) + + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/service/davinci/resource_davinci_application_gen.go b/internal/service/davinci/resource_davinci_application_gen.go new file mode 100644 index 000000000..0e50244a1 --- /dev/null +++ b/internal/service/davinci/resource_davinci_application_gen.go @@ -0,0 +1,602 @@ +// Copyright © 2025 Ping Identity Corporation +// Code generated by ping-terraform-plugin-framework-generator + +//go:build beta + +package davinci + +import ( + "context" + "fmt" + "net/http" + "regexp" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/pingidentity/pingone-go-client/pingone" + "github.com/pingidentity/terraform-provider-pingone/internal/framework" + "github.com/pingidentity/terraform-provider-pingone/internal/verify" +) + +var ( + _ resource.Resource = &davinciApplicationResource{} + _ resource.ResourceWithConfigure = &davinciApplicationResource{} + _ resource.ResourceWithImportState = &davinciApplicationResource{} +) + +func NewDavinciApplicationResource() resource.Resource { + return &davinciApplicationResource{} +} + +type davinciApplicationResource serviceClientType + +func (r *davinciApplicationResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_davinci_application" +} + +func (r *davinciApplicationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + resourceConfig, ok := req.ProviderData.(framework.ResourceType) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected the provider client, got: %T. Please report this issue to the provider maintainers.", req.ProviderData), + ) + + return + } + + r.Client = resourceConfig.Client + if r.Client == nil { + resp.Diagnostics.AddError( + "Client not initialised", + "Expected the PingOne client, got nil. Please report this issue to the provider maintainers.", + ) + return + } +} + +type davinciApplicationResourceModel struct { + ApiKey types.Object `tfsdk:"api_key"` + EnvironmentId types.String `tfsdk:"environment_id"` + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Oauth types.Object `tfsdk:"oauth"` +} + +func (r *davinciApplicationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + apiKeyDefault, diags := types.ObjectValue(map[string]attr.Type{ + "enabled": types.BoolType, + "value": types.StringType, + }, map[string]attr.Value{ + "enabled": types.BoolValue(true), + "value": types.StringUnknown(), + }) + resp.Diagnostics.Append(diags...) + oauthGrantTypesDefault, diags := types.SetValue(types.StringType, []attr.Value{ + types.StringValue("authorizationCode"), + }) + resp.Diagnostics.Append(diags...) + oauthScopesDefault, diags := types.SetValue(types.StringType, []attr.Value{ + types.StringValue("openid"), + types.StringValue("profile"), + }) + resp.Diagnostics.Append(diags...) + emptySetDefault, diags := types.SetValue(types.StringType, nil) + resp.Diagnostics.Append(diags...) + oauthDefault, diags := types.ObjectValue(map[string]attr.Type{ + "client_secret": types.StringType, + "enforce_signed_request_openid": types.BoolType, + "grant_types": types.SetType{ElemType: types.StringType}, + "logout_uris": types.SetType{ElemType: types.StringType}, + "redirect_uris": types.SetType{ElemType: types.StringType}, + "scopes": types.SetType{ElemType: types.StringType}, + "sp_jwks_openid": types.StringType, + "sp_jwks_url": types.StringType, + }, map[string]attr.Value{ + "client_secret": types.StringUnknown(), + "enforce_signed_request_openid": types.BoolValue(false), + "grant_types": oauthGrantTypesDefault, + "logout_uris": emptySetDefault, + "redirect_uris": emptySetDefault, + "scopes": oauthScopesDefault, + "sp_jwks_openid": types.StringNull(), + "sp_jwks_url": types.StringNull(), + }) + resp.Diagnostics.Append(diags...) + resp.Schema = schema.Schema{ + Description: "Resource to create and manage a DaVinci application.", + Attributes: map[string]schema.Attribute{ + "api_key": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{ + Computed: true, + Optional: true, + Default: booldefault.StaticBool(true), + }, + "value": schema.StringAttribute{ + Computed: true, + Sensitive: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + Computed: true, + Optional: true, + Default: objectdefault.StaticValue(apiKeyDefault), + }, + "environment_id": schema.StringAttribute{ + Required: true, + Description: "The ID of the environment to create and manage the davinci_application in.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"), "Must be a valid UUID"), + }, + }, + "id": schema.StringAttribute{ + Computed: true, + Description: "The ID of this resource.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"), "Must be a valid UUID"), + }, + }, + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(256), + }, + }, + "oauth": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "client_secret": schema.StringAttribute{ + Computed: true, + Sensitive: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "enforce_signed_request_openid": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "grant_types": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Computed: true, + Description: "Options are \"authorizationCode\", \"clientCredentials\", \"implicit\".", + MarkdownDescription: "Options are `authorizationCode`, `clientCredentials`, `implicit`.", + Validators: []validator.Set{ + setvalidator.ValueStringsAre(stringvalidator.OneOf( + "authorizationCode", + "clientCredentials", + "implicit", + )), + }, + Default: setdefault.StaticValue(oauthGrantTypesDefault), + }, + "logout_uris": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Computed: true, + Default: setdefault.StaticValue(emptySetDefault), + }, + "redirect_uris": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Computed: true, + Default: setdefault.StaticValue(emptySetDefault), + }, + "scopes": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Computed: true, + Description: "Options are \"flow_analytics\", \"offline_access\", \"openid\", \"profile\".", + MarkdownDescription: "Options are `flow_analytics`, `offline_access`, `openid`, `profile`.", + Validators: []validator.Set{ + setvalidator.ValueStringsAre(stringvalidator.OneOf( + "flow_analytics", + "offline_access", + "openid", + "profile", + )), + }, + Default: setdefault.StaticValue(oauthScopesDefault), + }, + "sp_jwks_openid": schema.StringAttribute{ + Optional: true, + }, + "sp_jwks_url": schema.StringAttribute{ + Optional: true, + }, + }, + Optional: true, + Computed: true, + Default: objectdefault.StaticValue(oauthDefault), + }, + }, + } +} + +func (model *davinciApplicationResourceModel) buildClientStructPost() (*pingone.DaVinciApplicationCreateRequest, diag.Diagnostics) { + result := &pingone.DaVinciApplicationCreateRequest{} + var respDiags diag.Diagnostics + // name + result.Name = model.Name.ValueString() + return result, respDiags +} + +func (model *davinciApplicationResourceModel) buildClientStructPut() (*pingone.DaVinciApplicationReplaceRequest, diag.Diagnostics) { + result := &pingone.DaVinciApplicationReplaceRequest{} + var respDiags diag.Diagnostics + // api_key + if !model.ApiKey.IsNull() && !model.ApiKey.IsUnknown() { + if !model.ApiKey.Attributes()["enabled"].IsNull() && !model.ApiKey.Attributes()["enabled"].IsUnknown() { + result.ApiKeyEnabled = model.ApiKey.Attributes()["enabled"].(types.Bool).ValueBoolPointer() + } + } + // name + result.Name = model.Name.ValueString() + // oauth + if !model.Oauth.IsNull() && !model.Oauth.IsUnknown() { + oauthValue := &pingone.DaVinciApplicationReplaceRequestOAuth{} + oauthAttrs := model.Oauth.Attributes() + if !oauthAttrs["enforce_signed_request_openid"].IsNull() && !oauthAttrs["enforce_signed_request_openid"].IsUnknown() { + oauthValue.EnforceSignedRequestOpenid = oauthAttrs["enforce_signed_request_openid"].(types.Bool).ValueBoolPointer() + } + if !oauthAttrs["grant_types"].IsNull() && !oauthAttrs["grant_types"].IsUnknown() { + oauthValue.GrantTypes = []pingone.DaVinciApplicationReplaceRequestOAuthGrantType{} + for _, grantTypesElement := range oauthAttrs["grant_types"].(types.Set).Elements() { + var grantTypesValue pingone.DaVinciApplicationReplaceRequestOAuthGrantType + grantTypesEnumValue, err := pingone.NewDaVinciApplicationReplaceRequestOAuthGrantTypeFromValue(grantTypesElement.(types.String).ValueString()) + if err != nil { + respDiags.AddAttributeError( + path.Root("grant_types"), + "Provided value is not valid", + fmt.Sprintf("The value provided for grant_types is not valid: %s", err.Error()), + ) + } else { + grantTypesValue = *grantTypesEnumValue + } + oauthValue.GrantTypes = append(oauthValue.GrantTypes, grantTypesValue) + } + } + if !oauthAttrs["logout_uris"].IsNull() && !oauthAttrs["logout_uris"].IsUnknown() { + oauthValue.LogoutUris = []string{} + for _, logoutUrisElement := range oauthAttrs["logout_uris"].(types.Set).Elements() { + oauthValue.LogoutUris = append(oauthValue.LogoutUris, logoutUrisElement.(types.String).ValueString()) + } + } + if !oauthAttrs["redirect_uris"].IsNull() && !oauthAttrs["redirect_uris"].IsUnknown() { + oauthValue.RedirectUris = []string{} + for _, redirectUrisElement := range oauthAttrs["redirect_uris"].(types.Set).Elements() { + oauthValue.RedirectUris = append(oauthValue.RedirectUris, redirectUrisElement.(types.String).ValueString()) + } + } + if !oauthAttrs["scopes"].IsNull() && !oauthAttrs["scopes"].IsUnknown() { + oauthValue.Scopes = []pingone.DaVinciApplicationReplaceRequestOAuthScope{} + for _, scopesElement := range oauthAttrs["scopes"].(types.Set).Elements() { + var scopesValue pingone.DaVinciApplicationReplaceRequestOAuthScope + scopesEnumValue, err := pingone.NewDaVinciApplicationReplaceRequestOAuthScopeFromValue(scopesElement.(types.String).ValueString()) + if err != nil { + respDiags.AddAttributeError( + path.Root("scopes"), + "Provided value is not valid", + fmt.Sprintf("The value provided for scopes is not valid: %s", err.Error()), + ) + } else { + scopesValue = *scopesEnumValue + } + oauthValue.Scopes = append(oauthValue.Scopes, scopesValue) + } + } + oauthValue.SpJwksOpenid = oauthAttrs["sp_jwks_openid"].(types.String).ValueStringPointer() + oauthValue.SpjwksUrl = oauthAttrs["sp_jwks_url"].(types.String).ValueStringPointer() + result.Oauth = oauthValue + } + + return result, respDiags +} + +func (state *davinciApplicationResourceModel) readClientResponse(response *pingone.DaVinciApplicationResponse) diag.Diagnostics { + var respDiags, diags diag.Diagnostics + // api_key + apiKeyAttrTypes := map[string]attr.Type{ + "enabled": types.BoolType, + "value": types.StringType, + } + apiKeyValue, diags := types.ObjectValue(apiKeyAttrTypes, map[string]attr.Value{ + "enabled": types.BoolValue(response.ApiKey.Enabled), + "value": types.StringValue(response.ApiKey.Value), + }) + respDiags.Append(diags...) + state.ApiKey = apiKeyValue + // id + state.Id = types.StringValue(response.Id) + // name + state.Name = types.StringValue(response.Name) + // oauth + oauthAttrTypes := map[string]attr.Type{ + "client_secret": types.StringType, + "enforce_signed_request_openid": types.BoolType, + "grant_types": types.SetType{ElemType: types.StringType}, + "logout_uris": types.SetType{ElemType: types.StringType}, + "redirect_uris": types.SetType{ElemType: types.StringType}, + "scopes": types.SetType{ElemType: types.StringType}, + "sp_jwks_openid": types.StringType, + "sp_jwks_url": types.StringType, + } + // enforce_signed_request_openid is not returned by the API when false, so assume it is false if nil + var oauthEnforceSignedRequestOpenidValue types.Bool + if response.Oauth.EnforceSignedRequestOpenid == nil { + oauthEnforceSignedRequestOpenidValue = types.BoolValue(false) + } else { + oauthEnforceSignedRequestOpenidValue = types.BoolValue(*response.Oauth.EnforceSignedRequestOpenid) + } + var oauthGrantTypesValue types.Set + if response.Oauth.GrantTypes == nil { + oauthGrantTypesValue, diags = types.SetValue(types.StringType, []attr.Value{}) + respDiags.Append(diags...) + } else { + oauthGrantTypesValue, diags = types.SetValueFrom(context.Background(), types.StringType, response.Oauth.GrantTypes) + respDiags.Append(diags...) + } + var oauthLogoutUrisValue types.Set + if response.Oauth.LogoutUris == nil { + oauthLogoutUrisValue, diags = types.SetValue(types.StringType, []attr.Value{}) + respDiags.Append(diags...) + } else { + oauthLogoutUrisValue, diags = types.SetValueFrom(context.Background(), types.StringType, response.Oauth.LogoutUris) + respDiags.Append(diags...) + } + var oauthRedirectUrisValue types.Set + if response.Oauth.RedirectUris == nil { + oauthRedirectUrisValue, diags = types.SetValue(types.StringType, []attr.Value{}) + respDiags.Append(diags...) + } else { + oauthRedirectUrisValue, diags = types.SetValueFrom(context.Background(), types.StringType, response.Oauth.RedirectUris) + respDiags.Append(diags...) + } + var oauthScopesValue types.Set + if response.Oauth.Scopes == nil { + oauthScopesValue, diags = types.SetValue(types.StringType, []attr.Value{}) + respDiags.Append(diags...) + } else { + oauthScopesValue, diags = types.SetValueFrom(context.Background(), types.StringType, response.Oauth.Scopes) + respDiags.Append(diags...) + } + oauthValue, diags := types.ObjectValue(oauthAttrTypes, map[string]attr.Value{ + "client_secret": types.StringValue(response.Oauth.ClientSecret), + "enforce_signed_request_openid": oauthEnforceSignedRequestOpenidValue, + "grant_types": oauthGrantTypesValue, + "logout_uris": oauthLogoutUrisValue, + "redirect_uris": oauthRedirectUrisValue, + "scopes": oauthScopesValue, + "sp_jwks_openid": types.StringPointerValue(response.Oauth.SpJwksOpenid), + "sp_jwks_url": types.StringPointerValue(response.Oauth.SpjwksUrl), + }) + respDiags.Append(diags...) + state.Oauth = oauthValue + return respDiags +} + +func (r *davinciApplicationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data davinciApplicationResourceModel + + if r.Client == nil { + resp.Diagnostics.AddError( + "Client not initialized", + "Expected the PingOne client, got nil. Please report this issue to the provider maintainers.") + return + } + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Read API call logic + environmentIdUuid, err := uuid.Parse(data.EnvironmentId.ValueString()) + if err != nil { + resp.Diagnostics.AddAttributeError( + path.Root("environment_id"), + "Attribute Validation Error", + fmt.Sprintf("The value '%s' for attribute '%s' is not a valid UUID: %s", data.EnvironmentId.ValueString(), "EnvironmentId", err.Error()), + ) + return + } + var responseData *pingone.DaVinciApplicationResponse + resp.Diagnostics.Append(framework.ParseResponse( + ctx, + + func() (any, *http.Response, error) { + fO, fR, fErr := r.Client.DaVinciApplicationsApi.GetDavinciApplicationById(ctx, environmentIdUuid, data.Id.ValueString()).Execute() + return framework.CheckEnvironmentExistsOnPermissionsError(ctx, r.Client, data.EnvironmentId.ValueString(), fO, fR, fErr) + }, + "GetDavinciApplicationById", + framework.CustomErrorResourceNotFoundWarning, + framework.DefaultRetryable, + &responseData, + )...) + + if resp.Diagnostics.HasError() { + return + } + + // Remove from state if resource is not found + if responseData == nil { + resp.State.RemoveResource(ctx) + return + } + + // Read response into the model + resp.Diagnostics.Append(data.readClientResponse(responseData)...) + + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *davinciApplicationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data davinciApplicationResourceModel + + if r.Client == nil { + resp.Diagnostics.AddError( + "Client not initialized", + "Expected the PingOne client, got nil. Please report this issue to the provider maintainers.") + return + } + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Update API call logic + clientData, diags := data.buildClientStructPut() + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + environmentIdUuid, err := uuid.Parse(data.EnvironmentId.ValueString()) + if err != nil { + resp.Diagnostics.AddAttributeError( + path.Root("environment_id"), + "Attribute Validation Error", + fmt.Sprintf("The value '%s' for attribute '%s' is not a valid UUID: %s", data.EnvironmentId.ValueString(), "EnvironmentId", err.Error()), + ) + return + } + var responseData *pingone.DaVinciApplicationResponse + resp.Diagnostics.Append(framework.ParseResponse( + ctx, + + func() (any, *http.Response, error) { + fO, fR, fErr := r.Client.DaVinciApplicationsApi.ReplaceDavinciApplicationById(ctx, environmentIdUuid, data.Id.ValueString()).DaVinciApplicationReplaceRequest(*clientData).Execute() + return framework.CheckEnvironmentExistsOnPermissionsError(ctx, r.Client, data.EnvironmentId.ValueString(), fO, fR, fErr) + }, + "ReplaceDavinciApplicationById", + framework.DefaultCustomError, + framework.DefaultRetryable, + &responseData, + )...) + + if resp.Diagnostics.HasError() { + return + } + + // Read response into the model + resp.Diagnostics.Append(data.readClientResponse(responseData)...) + + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *davinciApplicationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data davinciApplicationResourceModel + + if r.Client == nil { + resp.Diagnostics.AddError( + "Client not initialized", + "Expected the PingOne client, got nil. Please report this issue to the provider maintainers.") + return + } + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Delete API call logic + environmentIdUuid, err := uuid.Parse(data.EnvironmentId.ValueString()) + if err != nil { + resp.Diagnostics.AddAttributeError( + path.Root("environment_id"), + "Attribute Validation Error", + fmt.Sprintf("The value '%s' for attribute '%s' is not a valid UUID: %s", data.EnvironmentId.ValueString(), "EnvironmentId", err.Error()), + ) + return + } + resp.Diagnostics.Append(framework.ParseResponse( + ctx, + + func() (any, *http.Response, error) { + fR, fErr := r.Client.DaVinciApplicationsApi.DeleteDavinciApplicationById(ctx, environmentIdUuid, data.Id.ValueString()).Execute() + return framework.CheckEnvironmentExistsOnPermissionsError(ctx, r.Client, data.EnvironmentId.ValueString(), nil, fR, fErr) + }, + "DeleteDavinciApplicationById", + framework.CustomErrorResourceNotFoundWarning, + framework.DefaultRetryable, + nil, + )...) +} + +func (r *davinciApplicationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + + idComponents := []framework.ImportComponent{ + { + Label: "environment_id", + Regexp: verify.P1ResourceIDRegexp, + }, + { + Label: "application_id", + PrimaryID: true, + Regexp: verify.P1DVResourceIDRegexp, + }, + } + + attributes, err := framework.ParseImportID(req.ID, idComponents...) + if err != nil { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + err.Error(), + ) + return + } + + for _, idComponent := range idComponents { + pathKey := idComponent.Label + + if idComponent.PrimaryID { + pathKey = "id" + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(pathKey), attributes[idComponent.Label])...) + } +} diff --git a/internal/service/davinci/resource_davinci_application_gen_test.go b/internal/service/davinci/resource_davinci_application_gen_test.go new file mode 100644 index 000000000..0896b689c --- /dev/null +++ b/internal/service/davinci/resource_davinci_application_gen_test.go @@ -0,0 +1,533 @@ +// Copyright © 2025 Ping Identity Corporation +// Code generated by ping-terraform-plugin-framework-generator + +//go:build beta + +package davinci_test + +import ( + "context" + "fmt" + "os" + "regexp" + "testing" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/pingidentity/pingone-go-client/pingone" + "github.com/pingidentity/terraform-provider-pingone/internal/acctest" + acctestlegacysdk "github.com/pingidentity/terraform-provider-pingone/internal/acctest/legacysdk" + "github.com/pingidentity/terraform-provider-pingone/internal/acctest/service/base" +) + +func TestAccDavinciApplication_RemovalDrift(t *testing.T) { + t.Parallel() + + resourceName := acctest.ResourceNameGen() + resourceFullName := fmt.Sprintf("pingone_davinci_application.%s", resourceName) + + environmentName := acctest.ResourceNameGenEnvironment() + + licenseID := os.Getenv("PINGONE_LICENSE_ID") + var environmentId string + var id string + + var p1Client *pingone.APIClient + var ctx = context.Background() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheckClient(t) + acctest.PreCheckNewEnvironment(t) + acctest.PreCheckBeta(t) + + p1Client = acctest.PreCheckTestClient(ctx, t) + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: davinciApplication_CheckDestroy, + ErrorCheck: acctest.ErrorCheck(t), + Steps: []resource.TestStep{ + // Configure + { + Config: davinciApplication_MinimalHCL(resourceName, false), + Check: davinciApplication_GetIDs(resourceFullName, &environmentId, &id), + }, + { + PreConfig: func() { + davinciApplication_Delete(ctx, p1Client, t, environmentId, id) + }, + RefreshState: true, + ExpectNonEmptyPlan: true, + }, + // Test removal of the environment + { + Config: davinciApplication_NewEnvHCL(environmentName, licenseID, resourceName), + Check: davinciApplication_GetIDs(resourceFullName, &environmentId, &id), + }, + { + PreConfig: func() { + base.Environment_RemovalDrift_PreConfig(ctx, p1Client, t, environmentId) + }, + RefreshState: true, + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccDavinciApplication_MinimalMaximalClean(t *testing.T) { + testAccDavinciApplication_MinimalMaximal(t, false) +} + +func TestAccDavinciApplication_MinimalMaximalWithBootstrap(t *testing.T) { + testAccDavinciApplication_MinimalMaximal(t, true) +} + +func testAccDavinciApplication_MinimalMaximal(t *testing.T, withBootstrapConfig bool) { + t.Parallel() + + resourceName := acctest.ResourceNameGen() + resourceFullName := fmt.Sprintf("pingone_davinci_application.%s", resourceName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheckClient(t) + acctest.PreCheckBeta(t) + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: davinciApplication_CheckDestroy, + ErrorCheck: acctest.ErrorCheck(t), + Steps: []resource.TestStep{ + { + // Create the resource with a minimal model + Config: davinciApplication_MinimalHCL(resourceName, withBootstrapConfig), + Check: davinciApplication_CheckComputedValuesMinimal(resourceName), + }, + { + // Delete the minimal model + Config: davinciApplication_MinimalHCL(resourceName, withBootstrapConfig), + Destroy: true, + }, + { + // Re-create with a complete model + Config: davinciApplication_CompleteHCL(resourceName, withBootstrapConfig), + Check: davinciApplication_CheckComputedValuesComplete(resourceName), + }, + { + // Back to minimal model + Config: davinciApplication_MinimalHCL(resourceName, withBootstrapConfig), + Check: davinciApplication_CheckComputedValuesMinimal(resourceName), + }, + { + // Back to complete model + Config: davinciApplication_CompleteHCL(resourceName, withBootstrapConfig), + Check: davinciApplication_CheckComputedValuesComplete(resourceName), + }, + { + // Complete model with reordering of lists and sets + Config: davinciApplication_CompleteReorderedHCL(resourceName, withBootstrapConfig), + Check: davinciApplication_CheckComputedValuesComplete(resourceName), + }, + { + // Test importing the resource + Config: davinciApplication_CompleteHCL(resourceName, withBootstrapConfig), + ResourceName: fmt.Sprintf("pingone_davinci_application.%s", resourceName), + ImportStateIdFunc: func() resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceFullName] + if !ok { + return "", fmt.Errorf("Resource Not found: %s", resourceFullName) + } + + return fmt.Sprintf("%s/%s", rs.Primary.Attributes["environment_id"], rs.Primary.Attributes["id"]), nil + } + }(), + ImportStateVerifyIdentifierAttribute: "id", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccDavinciApplication_NewEnv(t *testing.T) { + t.Parallel() + + resourceName := acctest.ResourceNameGen() + + environmentName := acctest.ResourceNameGenEnvironment() + + licenseID := os.Getenv("PINGONE_LICENSE_ID") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheckClient(t) + acctest.PreCheckNewEnvironment(t) + acctest.PreCheckBeta(t) + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: davinciApplication_CheckDestroy, + ErrorCheck: acctest.ErrorCheck(t), + Steps: []resource.TestStep{ + { + Config: davinciApplication_NewEnvHCL(environmentName, licenseID, resourceName), + Check: davinciApplication_CheckComputedValuesMinimal(resourceName), + }, + }, + }) +} + +func TestAccDavinciApplication_BadParameters(t *testing.T) { + t.Parallel() + + resourceName := acctest.ResourceNameGen() + resourceFullName := fmt.Sprintf("pingone_davinci_application.%s", resourceName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheckClient(t) + acctest.PreCheckBeta(t) + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: davinciApplication_CheckDestroy, + ErrorCheck: acctest.ErrorCheck(t), + Steps: []resource.TestStep{ + // Configure + { + Config: davinciApplication_MinimalHCL(resourceName, false), + }, + // Errors + { + ResourceName: resourceFullName, + ImportState: true, + ExpectError: regexp.MustCompile(`Unexpected Import Identifier`), + }, + { + ResourceName: resourceFullName, + ImportStateId: "/", + ImportState: true, + ExpectError: regexp.MustCompile(`Unexpected Import Identifier`), + }, + { + ResourceName: resourceFullName, + ImportStateId: "badformat/badformat", + ImportState: true, + ExpectError: regexp.MustCompile(`Unexpected Import Identifier`), + }, + }, + }) +} + +// Test detailed OAuth configurations +func TestAccDavinciApplication_WithOAuth(t *testing.T) { + t.Parallel() + + resourceName := acctest.ResourceNameGen() + resourceFullName := fmt.Sprintf("pingone_davinci_application.%s", resourceName) + + name := resourceName + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheckClient(t) + acctest.PreCheckBeta(t) + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: davinciApplication_CheckDestroy, + ErrorCheck: acctest.ErrorCheck(t), + Steps: []resource.TestStep{ + // Create with full OAuth configuration + { + Config: davinciApplication_CompleteHCL(resourceName, false), + Check: davinciApplication_CheckComputedValuesComplete(resourceName), + }, + { + Config: davinciApplication_CompleteHCL(resourceName, false), + Destroy: true, + }, + // Create with minimal OAuth configuration 1 + { + Config: davinciApplication_WithOAuth_Minimal1_HCL(resourceName, name), + Check: davinciApplication_CheckComputedValuesMinimal(resourceName), + }, + { + Config: davinciApplication_WithOAuth_Minimal1_HCL(resourceName, name), + Destroy: true, + }, + // Create with minimal OAuth configuration 2 + { + Config: davinciApplication_WithOAuth_Minimal2_HCL(resourceName, name), + Check: davinciApplication_CheckComputedValuesComplete(resourceName), + }, + { + Config: davinciApplication_WithOAuth_Minimal2_HCL(resourceName, name), + Destroy: true, + }, + // Test transitions between configurations + { + Config: davinciApplication_CompleteHCL(resourceName, false), + Check: davinciApplication_CheckComputedValuesComplete(resourceName), + }, + { + Config: davinciApplication_WithOAuth_Minimal1_HCL(resourceName, name), + Check: davinciApplication_CheckComputedValuesMinimal(resourceName), + }, + { + Config: davinciApplication_CompleteHCL(resourceName, false), + Check: davinciApplication_CheckComputedValuesComplete(resourceName), + }, + // Test importing the resource + { + ResourceName: resourceFullName, + ImportStateIdFunc: func() resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceFullName] + if !ok { + return "", fmt.Errorf("Resource Not found: %s", resourceFullName) + } + + return fmt.Sprintf("%s/%s", rs.Primary.Attributes["environment_id"], rs.Primary.Attributes["id"]), nil + } + }(), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +// Minimal HCL with only required values set +func davinciApplication_MinimalHCL(resourceName string, withBootstrapConfig bool) string { + return fmt.Sprintf(` + %[1]s + +resource "pingone_davinci_application" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + name = "%[2]s" +} +`, acctest.DaVinciSandboxEnvironment(withBootstrapConfig), resourceName) +} + +// Maximal HCL with all values set where possible +func davinciApplication_CompleteHCL(resourceName string, withBootstrapConfig bool) string { + return fmt.Sprintf(` + %[1]s + +resource "pingone_davinci_application" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + name = "%[2]s" + api_key = { + enabled = false + } + oauth = { + enforce_signed_request_openid = true + grant_types = [ + "clientCredentials", + "authorizationCode", + "implicit", + ] + logout_uris = [ + "https://pingidentity.com/logout", + "https://pingidentity.com/logout3", + ] + redirect_uris = [ + "https://pingidentity.com/callback", + "https://pingidentity.com/redirect", + ] + scopes = [ + "profile", + "flow_analytics", + "openid", + ] + sp_jwks_url = "https://pingidentity.com/jwks" + } +} +`, acctest.DaVinciSandboxEnvironment(withBootstrapConfig), resourceName) +} + +// Maximal HCL with all values set, with ordering changes in lists and sets from the default CompleteHCL, +// as well as coverage for the sp_jwks_openid attribute. +func davinciApplication_CompleteReorderedHCL(resourceName string, withBootstrapConfig bool) string { + return fmt.Sprintf(` + %[1]s + +resource "pingone_davinci_application" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + name = "%[2]s" + api_key = { + # Test with enabled false after TRIAGE-28630 fixed + enabled = true + } + oauth = { + enforce_signed_request_openid = false + grant_types = [ + "authorizationCode", + "implicit", + "clientCredentials", + ] + logout_uris = [ + "https://pingidentity.com/v2/logout", + "https://pingidentity.com/logout", + ] + redirect_uris = [ + "https://pingidentity.com/redirect", + "https://pingidentity.com/callback", + "https://pingidentity.com/someotherthing", + ] + scopes = [ + "openid", + "profile", + "flow_analytics", + ] + sp_jwks_openid = jsonencode({ + "keys" : [ + { + "kty" : "RSA", + "kid" : "mykeyid", + "n" : "example", + "e" : "example", + "alg" : "RS256", + "use" : "sig" + } + ] + }) + } +} +`, acctest.DaVinciSandboxEnvironment(withBootstrapConfig), resourceName) +} + +func davinciApplication_WithOAuth_Minimal1_HCL(resourceName string, name string) string { + return fmt.Sprintf(` + %[1]s + +resource "pingone_davinci_application" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + name = "%[3]s" + oauth = { + } +} +`, acctest.GenericSandboxEnvironment(), resourceName, name) +} + +func davinciApplication_WithOAuth_Minimal2_HCL(resourceName string, name string) string { + return fmt.Sprintf(` + %[1]s + +resource "pingone_davinci_application" "%[2]s" { + environment_id = data.pingone_environment.general_test.id + name = "%[3]s" + oauth = { + grant_types = [] + scopes = [] + redirect_uris = [] + logout_uris = [] + } +} +`, acctest.GenericSandboxEnvironment(), resourceName, name) +} + +func davinciApplication_NewEnvHCL(environmentName, licenseID, resourceName string) string { + return fmt.Sprintf(` + %[1]s + +resource "pingone_davinci_application" "%[3]s" { + environment_id = pingone_environment.%[2]s.id + name = "%[3]s" +} +`, acctestlegacysdk.MinimalSandboxEnvironment(environmentName, licenseID), environmentName, resourceName) +} + +// Validate any computed values when applying minimal HCL +func davinciApplication_CheckComputedValuesMinimal(resourceName string) resource.TestCheckFunc { + return resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fmt.Sprintf("pingone_davinci_application.%s", resourceName), "api_key.enabled", "true"), + resource.TestCheckResourceAttrSet(fmt.Sprintf("pingone_davinci_application.%s", resourceName), "api_key.value"), + resource.TestCheckResourceAttrSet(fmt.Sprintf("pingone_davinci_application.%s", resourceName), "oauth.client_secret"), + resource.TestCheckResourceAttr(fmt.Sprintf("pingone_davinci_application.%s", resourceName), "oauth.grant_types.#", "1"), + resource.TestCheckTypeSetElemAttr(fmt.Sprintf("pingone_davinci_application.%s", resourceName), "oauth.grant_types.*", "authorizationCode"), + resource.TestCheckResourceAttr(fmt.Sprintf("pingone_davinci_application.%s", resourceName), "oauth.logout_uris.#", "0"), + resource.TestCheckResourceAttr(fmt.Sprintf("pingone_davinci_application.%s", resourceName), "oauth.redirect_uris.#", "0"), + resource.TestCheckResourceAttr(fmt.Sprintf("pingone_davinci_application.%s", resourceName), "oauth.scopes.#", "2"), + resource.TestCheckTypeSetElemAttr(fmt.Sprintf("pingone_davinci_application.%s", resourceName), "oauth.scopes.*", "openid"), + resource.TestCheckTypeSetElemAttr(fmt.Sprintf("pingone_davinci_application.%s", resourceName), "oauth.scopes.*", "profile"), + ) +} + +// Validate any computed values when applying complete HCL +func davinciApplication_CheckComputedValuesComplete(resourceName string) resource.TestCheckFunc { + return resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(fmt.Sprintf("pingone_davinci_application.%s", resourceName), "api_key.value"), + resource.TestCheckResourceAttrSet(fmt.Sprintf("pingone_davinci_application.%s", resourceName), "oauth.client_secret"), + ) +} + +func davinciApplication_GetIDs(resourceName string, environmentId, id *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Resource Not found: %s", resourceName) + } + if environmentId != nil { + *environmentId = rs.Primary.Attributes["environment_id"] + } + if id != nil { + *id = rs.Primary.Attributes["id"] + } + + return nil + } +} + +// Delete the resource +func davinciApplication_Delete(ctx context.Context, apiClient *pingone.APIClient, t *testing.T, environmentId, id string) { + if environmentId == "" || id == "" { + t.Fatalf("One of the identifier attributes can't be determined. environmentId: '%s' id: '%s'", environmentId, id) + } + + _, err := apiClient.DaVinciApplicationsApi.DeleteDavinciApplicationById(ctx, uuid.MustParse(environmentId), id).Execute() + if err != nil { + t.Fatalf("Failed to delete davinci_application: %v", err) + } +} + +// Test that any objects created by the test are destroyed +func davinciApplication_CheckDestroy(s *terraform.State) error { + var ctx = context.Background() + + p1Client, err := acctest.TestClient(ctx) + + if err != nil { + return err + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "pingone_davinci_application" { + continue + } + shouldContinue, err := acctest.CheckParentEnvironmentDestroy(ctx, p1Client, rs.Primary.Attributes["environment_id"]) + if err != nil { + return err + } + + if shouldContinue { + continue + } + + _, r, err := p1Client.DaVinciApplicationsApi.GetDavinciApplicationById(ctx, uuid.MustParse(rs.Primary.Attributes["environment_id"]), rs.Primary.Attributes["id"]).Execute() + + shouldContinue, err = acctest.CheckForResourceDestroy(r, err) + if err != nil { + return err + } + + if shouldContinue { + continue + } + + return fmt.Errorf("PingOne davinci_application Instance %s still exists", rs.Primary.ID) + } + + return nil +} diff --git a/internal/service/davinci/resource_davinci_connector_instance_gen_test.go b/internal/service/davinci/resource_davinci_connector_instance_gen_test.go index 2488cc753..0d6eac5fa 100644 --- a/internal/service/davinci/resource_davinci_connector_instance_gen_test.go +++ b/internal/service/davinci/resource_davinci_connector_instance_gen_test.go @@ -458,9 +458,9 @@ resource "pingone_davinci_connector_instance" "%[2]s" { "value" : [ { "name" : "example", - "url" : "https://example.com", + "url" : "https://pingidentity.com", "token" : "mytoken", - "value" : "https://example.com" + "value" : "https://pingidentity.com" } ] } @@ -492,9 +492,9 @@ resource "pingone_davinci_connector_instance" "%[2]s" { "value" : [ { "name" : "example", - "url" : "https://example.com", + "url" : "https://pingidentity.com", "token" : "mytoken", - "value" : "https://example.com" + "value" : "https://pingidentity.com" } ] } diff --git a/internal/service/davinci/service_beta.go b/internal/service/davinci/service_beta.go index caf8138a4..67d06a1fb 100644 --- a/internal/service/davinci/service_beta.go +++ b/internal/service/davinci/service_beta.go @@ -11,8 +11,9 @@ import ( func BetaResources() []func() resource.Resource { return []func() resource.Resource{ - NewDavinciVariableResource, + NewDavinciApplicationResource, NewDavinciConnectorInstanceResource, + NewDavinciVariableResource, } }