Skip to content

Commit

Permalink
feat: Support for workspace schedules (#76)
Browse files Browse the repository at this point in the history
feat: Support for workspace schedules
  • Loading branch information
alfespa17 authored Oct 18, 2024
1 parent 9a1ee37 commit b4669fe
Show file tree
Hide file tree
Showing 4 changed files with 374 additions and 0 deletions.
26 changes: 26 additions & 0 deletions docs/resources/workspace_schedule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "terrakube_workspace_schedule Resource - terrakube"
subcategory: ""
description: |-
---

# terrakube_workspace_schedule (Resource)





<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `schedule` (String) Schedule expression using java quartz notation
- `template_id` (String) Template Id to be used when triggering a job
- `workspace_id` (String) Workspace Id

### Read-Only

- `id` (String) Schedule Id
6 changes: 6 additions & 0 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,9 @@ type WorkspaceWebhookEntity struct {
RemoteHookId string `jsonapi:"attr,remoteHookId"`
Event string `jsonapi:"attr,event"`
}

type WorkspaceScheduleEntity struct {
ID string `jsonapi:"primary,schedule"`
Schedule string `jsonapi:"attr,cron"`
TemplateId string `jsonapi:"attr,tcl"`
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ func (p *TerrakubeProvider) Resources(ctx context.Context) []func() resource.Res
NewWorkspaceVcsResource,
NewWorkspaceWebhookResource,
NewVcsResource,
NewWorkspaceScheduleResource,
}
}

Expand Down
341 changes: 341 additions & 0 deletions internal/provider/workspace_schedule_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
package provider

import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io"
"net/http"
"strings"
"terraform-provider-terrakube/internal/client"

"github.com/google/jsonapi"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"

"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 = &WorkspaceScheduleResource{}
var _ resource.ResourceWithImportState = &WorkspaceScheduleResource{}

type WorkspaceScheduleResource struct {
client *http.Client
endpoint string
token string
}

type WorkspaceScheduleResourceModel struct {
ID types.String `tfsdk:"id"`
WorkspaceId types.String `tfsdk:"workspace_id"`
TemplateId types.String `tfsdk:"template_id"`
Schedule types.String `tfsdk:"schedule"`
}

func NewWorkspaceScheduleResource() resource.Resource {
return &WorkspaceScheduleResource{}
}

func (r *WorkspaceScheduleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_workspace_schedule"
}

func (r *WorkspaceScheduleResource) 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: "Schedule Id",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"schedule": schema.StringAttribute{
Required: true,
Description: "Schedule expression using java quartz notation",
},
"template_id": schema.StringAttribute{
Required: true,
Description: "Template Id to be used when triggering a job",
},
"workspace_id": schema.StringAttribute{
Required: true,
Description: "Workspace Id",
},
},
}
}

func (r *WorkspaceScheduleResource) 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 Schedule 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 Schedule resource", map[string]any{"success": true})
}

func (r *WorkspaceScheduleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan WorkspaceScheduleResourceModel

resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)

if resp.Diagnostics.HasError() {
return
}

bodyRequest := &client.WorkspaceScheduleEntity{
Schedule: plan.Schedule.ValueString(),
TemplateId: plan.TemplateId.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
}

workspaceScheduleRequest, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/workspace/%s/schedule", r.endpoint, plan.WorkspaceId.ValueString()), strings.NewReader(out.String()))
workspaceScheduleRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.token))
workspaceScheduleRequest.Header.Add("Content-Type", "application/vnd.api+json")
if err != nil {
resp.Diagnostics.AddError("Error creating workspace schedule resource request", fmt.Sprintf("Error creating workspace schedule resource request: %s", err))
return
}

workspaceScheduleResponse, err := r.client.Do(workspaceScheduleRequest)
if err != nil {
resp.Diagnostics.AddError("Error executing workspace schedule resource request", fmt.Sprintf("Error executing workspace schedule resource request: %s", err))
return
}

bodyResponse, err := io.ReadAll(workspaceScheduleResponse.Body)
if err != nil {
tflog.Error(ctx, "Error reading workspace schedule resource response")
}
workspaceSchedule := &client.WorkspaceScheduleEntity{}

tflog.Info(ctx, "Body Response", map[string]any{"success": string(bodyResponse)})

err = jsonapi.UnmarshalPayload(strings.NewReader(string(bodyResponse)), workspaceSchedule)

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.Schedule = types.StringValue(workspaceSchedule.Schedule)
plan.TemplateId = types.StringValue(workspaceSchedule.TemplateId)
plan.ID = types.StringValue(workspaceSchedule.ID)

tflog.Info(ctx, "workspace schedule Resource Created", map[string]any{"success": true})

resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

func (r *WorkspaceScheduleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state WorkspaceScheduleResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

workspaceScheduleRequest, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/workspace/%s/schedule/%s", r.endpoint, state.WorkspaceId.ValueString(), state.ID.ValueString()), nil)
workspaceScheduleRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.token))
workspaceScheduleRequest.Header.Add("Content-Type", "application/vnd.api+json")
if err != nil {
resp.Diagnostics.AddError("Error creating workspace schedule resource request", fmt.Sprintf("Error creating workspace schedule resource request: %s", err))
return
}

workspaceScheduleResponse, err := r.client.Do(workspaceScheduleRequest)
if err != nil {
resp.Diagnostics.AddError("Error executing workspace schedule resource request", fmt.Sprintf("Error executing workspace schedule resource request: %s", err))
return
}

bodyResponse, err := io.ReadAll(workspaceScheduleResponse.Body)
if err != nil {
tflog.Error(ctx, "Error reading workspace schedule resource response")
}
workspaceSchedule := &client.WorkspaceScheduleEntity{}

tflog.Info(ctx, "Body Response", map[string]any{"bodyResponse": string(bodyResponse)})
err = jsonapi.UnmarshalPayload(strings.NewReader(string(bodyResponse)), workspaceSchedule)

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.Schedule = types.StringValue(workspaceSchedule.Schedule)
state.TemplateId = types.StringValue(workspaceSchedule.TemplateId)
state.ID = types.StringValue(workspaceSchedule.ID)

// Set refreshed state
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

tflog.Info(ctx, "Workspace schedule Resource reading", map[string]any{"success": true})
}

func (r *WorkspaceScheduleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// Retrieve values from plan
var plan WorkspaceScheduleResourceModel
var state WorkspaceScheduleResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}

bodyRequest := &client.WorkspaceScheduleEntity{
Schedule: plan.Schedule.ValueString(),
TemplateId: plan.TemplateId.ValueString(),
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
}

workspaceScheduleReq, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%s/api/v1/workspace/%s/schedule/%s", r.endpoint, state.WorkspaceId.ValueString(), state.ID.ValueString()), strings.NewReader(out.String()))
workspaceScheduleReq.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.token))
workspaceScheduleReq.Header.Add("Content-Type", "application/vnd.api+json")
if err != nil {
resp.Diagnostics.AddError("Error creating Workspace schedule resource request", fmt.Sprintf("Error creating schedule schedule resource request: %s", err))
return
}

workspaceScheduleResponse, err := r.client.Do(workspaceScheduleReq)
if err != nil {
resp.Diagnostics.AddError("Error executing Workspace schedule resource request", fmt.Sprintf("Error executing Workspace schedule resource request: %s", err))
return
}

bodyResponse, err := io.ReadAll(workspaceScheduleResponse.Body)
if err != nil {
tflog.Error(ctx, "Error reading Workspace schedule resource response")
}

tflog.Info(ctx, "Body Response", map[string]any{"success": string(bodyResponse)})

workspaceScheduleReq, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/workspace/%s/schedule/%s", r.endpoint, state.WorkspaceId.ValueString(), state.ID.ValueString()), nil)
workspaceScheduleReq.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.token))
workspaceScheduleReq.Header.Add("Content-Type", "application/vnd.api+json")
if err != nil {
resp.Diagnostics.AddError("Error creating Workspace schedule resource request", fmt.Sprintf("Error creating Workspace schedule resource request: %s", err))
return
}

workspaceScheduleResponse, err = r.client.Do(workspaceScheduleReq)
if err != nil {
resp.Diagnostics.AddError("Error executing Workspace schedule resource request", fmt.Sprintf("Error executing Workspace schedule resource request: %s", err))
return
}

bodyResponse, err = io.ReadAll(workspaceScheduleResponse.Body)
if err != nil {
resp.Diagnostics.AddError("Error reading Workspace schedule resource response body", fmt.Sprintf("Error reading Workspace schedule resource response body: %s", err))
}

tflog.Info(ctx, "Body Response", map[string]any{"bodyResponse": string(bodyResponse)})

workspaceSchedule := &client.WorkspaceScheduleEntity{}
err = jsonapi.UnmarshalPayload(strings.NewReader(string(bodyResponse)), workspaceSchedule)

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.Schedule = types.StringValue(workspaceSchedule.Schedule)
plan.TemplateId = types.StringValue(workspaceSchedule.TemplateId)

resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

func (r *WorkspaceScheduleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data WorkspaceScheduleResourceModel

// 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/workspace/%s/schedule/%s", r.endpoint, 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 schedule resource request", fmt.Sprintf("Error creating schedule schedule resource request: %s", err))
return
}

_, err = r.client.Do(workspaceRequest)
if err != nil {
resp.Diagnostics.AddError("Error executing Workspace schedule resource request", fmt.Sprintf("Error executing Workspace schedule resource request: %s", err))
return
}
}

func (r *WorkspaceScheduleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, ",")

if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
resp.Diagnostics.AddError(
"Unexpected Import Identifier",
fmt.Sprintf("Expected import identifier with format: 'organization_ID,workspace_ID, ID', Got: %q", req.ID),
)
return
}

resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("workspace_id"), idParts[1])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), idParts[2])...)
}

0 comments on commit b4669fe

Please sign in to comment.