diff --git a/docs/resources/workspace_variable.md b/docs/resources/workspace_variable.md new file mode 100644 index 0000000..e4c04e9 --- /dev/null +++ b/docs/resources/workspace_variable.md @@ -0,0 +1,31 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "terrakube_workspace_variable Resource - terraform-provider-terrakube" +subcategory: "" +description: |- + +--- + +# terrakube_workspace_variable (Resource) + + + + + + +## Schema + +### Required + +- `category` (String) Variable category (ENV or TERRAFORM) +- `description` (String) Variable description +- `hcl` (Boolean) is hcl? +- `key` (String) Variable key +- `organization_id` (String) Terrakube organization id +- `sensitive` (Boolean) is sensitive? +- `value` (String) Variable value +- `workspace_id` (String) Terrakube workspace id + +### Read-Only + +- `id` (String) Variable Id diff --git a/examples/resources/workspace_variable/resource.tf b/examples/resources/workspace_variable/resource.tf new file mode 100644 index 0000000..bb6b135 --- /dev/null +++ b/examples/resources/workspace_variable/resource.tf @@ -0,0 +1,21 @@ +resource "terrakube_workspace_variable" "sample1" { + organization_id = data.terrakube_organization.org.id + workspace_id = terrakube_workspace_cli.sample1.id + key = "sample-env-var" + value = "sample-value2222" + description = "sample env var" + category = "ENV" + sensitive = false + hcl = false +} + +resource "terrakube_workspace_variable" "sample2" { + organization_id = data.terrakube_organization.org.id + workspace_id = terrakube_workspace_cli.sample1.id + key = "sample-terra-var" + value = "sample-TERRAFORM" + description = "sample env var" + category = "TERRAFORM" + sensitive = false + hcl = false +} \ No newline at end of file diff --git a/internal/client/client.go b/internal/client/client.go index 5e04e2d..a78cdfa 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -35,6 +35,16 @@ type WorkspaceEntity struct { Deleted bool `jsonapi:"attr,deleted"` } +type WorkspaceVariableEntity struct { + ID string `jsonapi:"primary,variable"` + Key string `jsonapi:"attr,key"` + Value string `jsonapi:"attr,value"` + Description string `jsonapi:"attr,description"` + Category string `jsonapi:"attr,category"` + Sensitive bool `jsonapi:"attr,sensitive"` + Hcl bool `jsonapi:"attr,hcl"` +} + type VcsEntity struct { ID string `jsonapi:"primary,vcs"` Name string `jsonapi:"attr,name"` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index acfba7b..02322a7 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -172,6 +172,7 @@ func (p *TerrakubeProvider) Resources(ctx context.Context) []func() resource.Res NewModuleResource, NewOrganizationResource, NewWorkspaceCliResource, + NewWorkspaceVariableResource, } } diff --git a/internal/provider/workspace_variable_resource.go b/internal/provider/workspace_variable_resource.go new file mode 100644 index 0000000..59b50c2 --- /dev/null +++ b/internal/provider/workspace_variable_resource.go @@ -0,0 +1,365 @@ +package provider + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "github.com/google/jsonapi" + "io" + "net/http" + "strings" + "terraform-provider-terrakube/internal/client" + + "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/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &WorkspaceVariableResource{} +var _ resource.ResourceWithImportState = &WorkspaceVariableResource{} + +type WorkspaceVariableResource struct { + client *http.Client + endpoint string + token string +} + +type WorkspaceVariableResourceModel struct { + ID types.String `tfsdk:"id"` + OrganizationId types.String `tfsdk:"organization_id"` + WorkspaceId types.String `tfsdk:"workspace_id"` + Key types.String `tfsdk:"key"` + Value types.String `tfsdk:"value"` + Description types.String `tfsdk:"description"` + Category types.String `tfsdk:"category"` + Sensitive types.Bool `tfsdk:"sensitive"` + Hcl types.Bool `tfsdk:"hcl"` +} + +func NewWorkspaceVariableResource() resource.Resource { + return &WorkspaceVariableResource{} +} + +func (r *WorkspaceVariableResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_workspace_variable" +} + +func (r *WorkspaceVariableResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "Variable Id", + }, + "organization_id": schema.StringAttribute{ + Required: true, + Description: "Terrakube organization id", + }, + "workspace_id": schema.StringAttribute{ + Required: true, + Description: "Terrakube workspace id", + }, + "key": schema.StringAttribute{ + Required: true, + Description: "Variable key", + }, + "value": schema.StringAttribute{ + Required: true, + Description: "Variable value", + }, + "description": schema.StringAttribute{ + Required: true, + Description: "Variable description", + }, + "category": schema.StringAttribute{ + Required: true, + Description: "Variable category (ENV or TERRAFORM)", + }, + "sensitive": schema.BoolAttribute{ + Required: true, + Description: "is sensitive?", + }, + "hcl": schema.BoolAttribute{ + Required: true, + Description: "is hcl?", + }, + }, + } +} + +func (r *WorkspaceVariableResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + providerData, ok := req.ProviderData.(*TerrakubeConnectionData) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Workspace Variable Resource Configure Type", + fmt.Sprintf("Expected *TerrakubeConnectionData, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + if providerData.InsecureHttpClient { + if custom, ok := http.DefaultTransport.(*http.Transport); ok { + customTransport := custom.Clone() + customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + r.client = &http.Client{Transport: customTransport} + } else { + r.client = &http.Client{} + } + } else { + r.client = &http.Client{} + } + + r.endpoint = providerData.Endpoint + r.token = providerData.Token + + tflog.Debug(ctx, "Configuring Workspace Variable resource", map[string]any{"success": true}) +} + +func (r *WorkspaceVariableResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan WorkspaceVariableResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if resp.Diagnostics.HasError() { + return + } + + bodyRequest := &client.WorkspaceVariableEntity{ + Key: plan.Key.ValueString(), + Value: plan.Value.ValueString(), + Description: plan.Description.ValueString(), + Sensitive: plan.Sensitive.ValueBool(), + Category: plan.Category.ValueString(), + Hcl: plan.Hcl.ValueBool(), + } + + var out = new(bytes.Buffer) + err := jsonapi.MarshalPayload(out, bodyRequest) + + if err != nil { + resp.Diagnostics.AddError("Unable to marshal payload", fmt.Sprintf("Unable to marshal payload: %s", err)) + return + } + + workspaceVarRequest, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/organization/%s/workspace/%s/variable", r.endpoint, plan.OrganizationId.ValueString(), plan.WorkspaceId.ValueString()), strings.NewReader(out.String())) + workspaceVarRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.token)) + workspaceVarRequest.Header.Add("Content-Type", "application/vnd.api+json") + if err != nil { + resp.Diagnostics.AddError("Error creating workspace variable resource request", fmt.Sprintf("Error creating workspace variable resource request: %s", err)) + return + } + + workspaceVarResponse, err := r.client.Do(workspaceVarRequest) + if err != nil { + resp.Diagnostics.AddError("Error executing workspace variable resource request", fmt.Sprintf("Error executing workspace variable resource request: %s", err)) + return + } + + bodyResponse, err := io.ReadAll(workspaceVarResponse.Body) + if err != nil { + tflog.Error(ctx, "Error reading workspace variable resource response") + } + workspaceVariable := &client.WorkspaceVariableEntity{} + + err = jsonapi.UnmarshalPayload(strings.NewReader(string(bodyResponse)), workspaceVariable) + + if err != nil { + resp.Diagnostics.AddError("Error unmarshal payload response", fmt.Sprintf("Error unmarshal payload response: %s", err)) + return + } + + tflog.Info(ctx, "Body Response", map[string]any{"bodyResponse": string(bodyResponse)}) + + plan.ID = types.StringValue(workspaceVariable.ID) + plan.Key = types.StringValue(workspaceVariable.Key) + plan.Value = types.StringValue(workspaceVariable.Value) + plan.Description = types.StringValue(workspaceVariable.Description) + plan.Category = types.StringValue(workspaceVariable.Category) + plan.Sensitive = types.BoolValue(workspaceVariable.Sensitive) + plan.Hcl = types.BoolValue(workspaceVariable.Hcl) + + tflog.Info(ctx, "workspace variable Resource Created", map[string]any{"success": true}) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *WorkspaceVariableResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state WorkspaceVariableResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + workspaceVariableRequest, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/organization/%s/workspace/%s/variable/%s", r.endpoint, state.OrganizationId.ValueString(), state.WorkspaceId.ValueString(), state.ID.ValueString()), nil) + workspaceVariableRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.token)) + workspaceVariableRequest.Header.Add("Content-Type", "application/vnd.api+json") + if err != nil { + resp.Diagnostics.AddError("Error creating workspace variable resource request", fmt.Sprintf("Error creating workspace variable resource request: %s", err)) + return + } + + workspaceVariableResponse, err := r.client.Do(workspaceVariableRequest) + if err != nil { + resp.Diagnostics.AddError("Error executing workspace variable resource request", fmt.Sprintf("Error executing workspace variable resource request: %s", err)) + return + } + + bodyResponse, err := io.ReadAll(workspaceVariableResponse.Body) + if err != nil { + tflog.Error(ctx, "Error reading workspace variable resource response") + } + workspaceVariable := &client.WorkspaceVariableEntity{} + + tflog.Info(ctx, "Body Response", map[string]any{"bodyResponse": string(bodyResponse)}) + err = jsonapi.UnmarshalPayload(strings.NewReader(string(bodyResponse)), workspaceVariable) + + if err != nil { + resp.Diagnostics.AddError("Error unmarshal payload response", fmt.Sprintf("Error unmarshal payload response: %s", err)) + return + } + + tflog.Info(ctx, "Body Response", map[string]any{"bodyResponse": string(bodyResponse)}) + + state.Key = types.StringValue(workspaceVariable.Key) + state.Value = types.StringValue(workspaceVariable.Value) + state.Description = types.StringValue(workspaceVariable.Description) + state.Category = types.StringValue(workspaceVariable.Category) + state.Sensitive = types.BoolValue(workspaceVariable.Sensitive) + state.Hcl = types.BoolValue(workspaceVariable.Hcl) + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Info(ctx, "Workspace variable Resource reading", map[string]any{"success": true}) +} + +func (r *WorkspaceVariableResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Retrieve values from plan + var plan WorkspaceVariableResourceModel + var state WorkspaceVariableResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + bodyRequest := &client.WorkspaceVariableEntity{ + Key: plan.Key.ValueString(), + Value: plan.Value.ValueString(), + Description: plan.Description.ValueString(), + Category: plan.Category.ValueString(), + Sensitive: plan.Sensitive.ValueBool(), + Hcl: state.Hcl.ValueBool(), + ID: state.ID.ValueString(), + } + + var out = new(bytes.Buffer) + err := jsonapi.MarshalPayload(out, bodyRequest) + + if err != nil { + resp.Diagnostics.AddError("Unable to marshal payload", fmt.Sprintf("Unable to marshal payload: %s", err)) + return + } + + workspaceVariableReq, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%s/api/v1/organization/%s/workspace/%s/variable/%s", r.endpoint, state.OrganizationId.ValueString(), state.WorkspaceId.ValueString(), state.ID.ValueString()), strings.NewReader(out.String())) + workspaceVariableReq.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.token)) + workspaceVariableReq.Header.Add("Content-Type", "application/vnd.api+json") + if err != nil { + resp.Diagnostics.AddError("Error creating Workspace variable resource request", fmt.Sprintf("Error creating Workspace variable resource request: %s", err)) + return + } + + workspaceVariableResponse, err := r.client.Do(workspaceVariableReq) + if err != nil { + resp.Diagnostics.AddError("Error executing Workspace variable resource request", fmt.Sprintf("Error executing Workspace variable resource request: %s", err)) + return + } + + bodyResponse, err := io.ReadAll(workspaceVariableResponse.Body) + if err != nil { + tflog.Error(ctx, "Error reading Workspace variable resource response") + } + + tflog.Info(ctx, "Body Response", map[string]any{"success": string(bodyResponse)}) + + workspaceVariableReq, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/organization/%s/workspace/%s/variable/%s", r.endpoint, state.OrganizationId.ValueString(), state.WorkspaceId.ValueString(), state.ID.ValueString()), nil) + workspaceVariableReq.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.token)) + workspaceVariableReq.Header.Add("Content-Type", "application/vnd.api+json") + if err != nil { + resp.Diagnostics.AddError("Error creating Workspace variable resource request", fmt.Sprintf("Error creating Workspace variable resource request: %s", err)) + return + } + + workspaceVariableResponse, err = r.client.Do(workspaceVariableReq) + if err != nil { + resp.Diagnostics.AddError("Error executing Workspace variable resource request", fmt.Sprintf("Error executing Workspace variable resource request: %s", err)) + return + } + + bodyResponse, err = io.ReadAll(workspaceVariableResponse.Body) + if err != nil { + resp.Diagnostics.AddError("Error reading Workspace variable resource response body", fmt.Sprintf("Error reading Workspace variable resource response body: %s", err)) + } + + tflog.Info(ctx, "Body Response", map[string]any{"bodyResponse": string(bodyResponse)}) + + workspaceVariable := &client.WorkspaceVariableEntity{} + err = jsonapi.UnmarshalPayload(strings.NewReader(string(bodyResponse)), workspaceVariable) + + if err != nil { + resp.Diagnostics.AddError("Error unmarshal payload response", fmt.Sprintf("Error unmarshal payload response: %s", err)) + return + } + + plan.ID = types.StringValue(state.ID.ValueString()) + plan.Key = types.StringValue(workspaceVariable.Key) + plan.Value = types.StringValue(workspaceVariable.Value) + plan.Description = types.StringValue(workspaceVariable.Description) + plan.Category = types.StringValue(workspaceVariable.Category) + plan.Sensitive = types.BoolValue(workspaceVariable.Sensitive) + plan.Hcl = types.BoolValue(workspaceVariable.Hcl) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *WorkspaceVariableResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data WorkspaceVariableResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + workspaceRequest, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v1/organization/%s/workspace/%s/variable/%s", r.endpoint, data.OrganizationId.ValueString(), data.WorkspaceId.ValueString(), data.ID.ValueString()), nil) + workspaceRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.token)) + if err != nil { + resp.Diagnostics.AddError("Error creating Workspace variable resource request", fmt.Sprintf("Error creating Workspace variable resource request: %s", err)) + return + } + + _, err = r.client.Do(workspaceRequest) + if err != nil { + resp.Diagnostics.AddError("Error executing Workspace variable resource request", fmt.Sprintf("Error executing Workspace variable resource request: %s", err)) + return + } +} + +func (r *WorkspaceVariableResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +}