internal/provider/resource_gitlab_application.go (219 lines of code) (raw):

package provider import ( "context" "fmt" "strconv" "strings" conv "github.com/dcarbone/terraform-plugin-framework-utils/v3/conv" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "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/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" "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/hashicorp/terraform-plugin-log/tflog" gitlab "gitlab.com/gitlab-org/api/client-go" "gitlab.com/gitlab-org/terraform-provider-gitlab/internal/provider/utils" ) // Ensure provider defined types fully satisfy framework interfaces var ( _ resource.Resource = &gitlabApplicationResource{} _ resource.ResourceWithConfigure = &gitlabApplicationResource{} _ resource.ResourceWithImportState = &gitlabApplicationResource{} ) func init() { registerResource(NewGitLabApplicationResource) } // NewGitLabApplicationResource is a helper function to simplify the provider implementation. func NewGitLabApplicationResource() resource.Resource { return &gitlabApplicationResource{} } func (r *gitlabApplicationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_application" } // gitlabApplicationResource defines the resource implementation. type gitlabApplicationResource struct { client *gitlab.Client } // gitlabProjectProtectedEnvironmentResourceModel describes the resource data model. type gitlabApplicationResourceModel struct { Name types.String `tfsdk:"name"` RedirectURL types.String `tfsdk:"redirect_url"` Scopes types.Set `tfsdk:"scopes"` Confidential types.Bool `tfsdk:"confidential"` Id types.String `tfsdk:"id"` Secret types.String `tfsdk:"secret"` ApplicationId types.String `tfsdk:"application_id"` } func (r *gitlabApplicationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { allowedScopes := []string{"api", "read_api", "read_user", "read_repository", "write_repository", "read_registry", "write_registry", "sudo", "admin_mode", "openid", "profile", "email"} resp.Schema = schema.Schema{ MarkdownDescription: fmt.Sprintf(`The ` + "`gitlab_application`" + ` resource allows to manage the lifecycle of applications in gitlab. ~> In order to use a user for a user to create an application, they must have admin privileges at the instance level. To create an OIDC application, a scope of "openid". **Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/api/applications/)`), Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ MarkdownDescription: "The ID of this Terraform resource. In the format of `<application_id>`.", Computed: true, }, "name": schema.StringAttribute{ MarkdownDescription: "Name of the application.", Required: true, Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "redirect_url": schema.StringAttribute{ MarkdownDescription: "The URL gitlab should send the user to after authentication.", Required: true, Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "scopes": schema.SetAttribute{ MarkdownDescription: fmt.Sprintf(` Scopes of the application. Use "openid" if you plan to use this as an oidc authentication application. Valid options are: %s. This is only populated when creating a new application. This attribute is not available for imported resources `, utils.RenderValueListForDocs(allowedScopes), ), ElementType: types.StringType, Required: true, PlanModifiers: []planmodifier.Set{setplanmodifier.RequiresReplace(), setplanmodifier.UseStateForUnknown()}, Validators: []validator.Set{setvalidator.ValueStringsAre(stringvalidator.OneOf(allowedScopes...))}, }, "confidential": schema.BoolAttribute{ MarkdownDescription: "The application is used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential. Defaults to true if not supplied", Optional: true, Computed: true, PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplace(), boolplanmodifier.UseStateForUnknown()}, }, "secret": schema.StringAttribute{ MarkdownDescription: "Application secret. Sensitive and must be kept secret. This is only populated when creating a new application. This attribute is not available for imported resources.", Computed: true, Sensitive: true, }, "application_id": schema.StringAttribute{ MarkdownDescription: "Internal name of the application.", Computed: true, }, }, } } // Configure adds the provider configured client to the resource. func (r *gitlabApplicationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return } resourceData := req.ProviderData.(*GitLabResourceData) r.client = resourceData.Client } // Create creates a new upstream resources and adds it into the Terraform state. func (r *gitlabApplicationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data *gitlabApplicationResourceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } tflog.Debug(ctx, "creating application", map[string]any{ "scopes": data.Scopes.String(), }) scopes := conv.StringSetToStrings(data.Scopes) if resp.Diagnostics.HasError() { return } formatted_scopes := strings.Join(scopes, " ") // configure GitLab API call options := &gitlab.CreateApplicationOptions{ Name: gitlab.Ptr(data.Name.ValueString()), RedirectURI: gitlab.Ptr(data.RedirectURL.ValueString()), Scopes: gitlab.Ptr(formatted_scopes), } if !data.Confidential.IsNull() { options.Confidential = gitlab.Ptr(data.Confidential.ValueBool()) } // Create application application, _, err := r.client.Applications.CreateApplication(options) if err != nil { resp.Diagnostics.AddError("GitLab API error occurred", fmt.Sprintf("Unable to create application: %s", err.Error())) return } r.applicationModelToState(application, data) // Log the creation of the resource tflog.Debug(ctx, "created an application", map[string]any{ "name": data.Name.ValueString(), "id": data.Id.ValueString(), }) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } // Read refreshes the Terraform state with the latest data. func (r *gitlabApplicationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data *gitlabApplicationResourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } application, err := findGitlabApplication(r.client, data.Id.ValueString()) if err != nil { resp.Diagnostics.AddError("GitLab API error occurred", fmt.Sprintf("Unable to create application: %s", err.Error())) return } tflog.Trace(ctx, "found application", map[string]any{ "application": gitlab.Stringify(application), }) r.applicationModelToState(application, data) // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } // Updates updates the resource in-place. func (r *gitlabApplicationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { resp.Diagnostics.AddError( "Provider Error, report upstream", "Somehow the resource was requested to perform an in-place upgrade which is not possible.", ) } // Deletes removes the resource. func (r *gitlabApplicationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var data *gitlabApplicationResourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } id, err := strconv.Atoi(data.Id.ValueString()) if err != nil { resp.Diagnostics.AddError( "Internal provider error", fmt.Sprintf("Unable to convert application id to int: %s", err.Error()), ) return } if _, err = r.client.Applications.DeleteApplication(id); err != nil { resp.Diagnostics.AddError( "GitLab API Error occurred", fmt.Sprintf("Unable to delete application: %s", err.Error()), ) } } // ImportState imports the resource into the Terraform state. func (r *gitlabApplicationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } func (r *gitlabApplicationResource) applicationModelToState(application *gitlab.Application, data *gitlabApplicationResourceModel) { // need to check this // For reads, the secret will be empty, in which case we shouldn't set the state if application.Secret != "" { data.Secret = types.StringValue(application.Secret) } data.Id = types.StringValue(strconv.Itoa(application.ID)) data.Confidential = types.BoolValue(application.Confidential) data.Name = types.StringValue(application.ApplicationName) data.RedirectURL = types.StringValue(application.CallbackURL) data.ApplicationId = types.StringValue(application.ApplicationID) } func findGitlabApplication(client *gitlab.Client, desiredId string) (*gitlab.Application, error) { options := gitlab.ListApplicationsOptions{ PerPage: 20, Page: 1, } for options.Page != 0 { paginatedApplications, resp, err := client.Applications.ListApplications(&options) if err != nil { return nil, fmt.Errorf("unable to list applications. %s", err) } for i := range paginatedApplications { if strconv.Itoa(paginatedApplications[i].ID) == desiredId { return paginatedApplications[i], nil } } options.Page = resp.NextPage } // if we loop through the pages and haven't found it, we should error return nil, fmt.Errorf("unable to find application with id: %s", desiredId) }