Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/resources/service_wait.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: "plural_service_wait Resource - terraform-provider-plural"
subcategory: ""
description: |-

---

# plural_service_wait (Resource)





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

### Required

- `cluster` (String) Handle of the cluster where the service is deployed.
- `service` (String) Name the service deployment that should be checked.

### Optional

- `duration` (String) Maximum duration to wait for the service deployment to become healthy. Minimum 1 minute. Defaults to 10 minutes.
- `warmup` (String) Initial delay before checking the service deployment health. Defaults to 5 minutes.
20 changes: 20 additions & 0 deletions example/servicewait/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
terraform {
required_providers {
plural = {
source = "pluralsh/plural"
version = "0.2.28"
}
}
}

provider "plural" {
use_cli = true
}

resource "plural_service_wait" "test" {
cluster = "mgmt"
service = "console"
warmup = "5s"
duration = "1m"
}

1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ func (p *PluralProvider) Resources(_ context.Context) []func() resource.Resource
r.NewObservabilityWebhookResource,
r.NewCloudConnectionResource,
r.NewServiceAccountResource,
r.NewServiceWaitResource,
}
}

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

import (
"context"
"fmt"
"time"

"terraform-provider-plural/internal/client"
"terraform-provider-plural/internal/common"
customvalidator "terraform-provider-plural/internal/validator"

"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/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
console "github.com/pluralsh/console/go/client"
"k8s.io/apimachinery/pkg/util/wait"
)

type serviceWait struct {
Cluster types.String `tfsdk:"cluster"`
Service types.String `tfsdk:"service"`
Warmup types.String `tfsdk:"warmup"`
Duration types.String `tfsdk:"duration"`
}

func (in *serviceWait) ParseWarmup() (time.Duration, error) {
return time.ParseDuration(in.Warmup.ValueString())
}

func (in *serviceWait) ParseDuration() (time.Duration, error) {
return time.ParseDuration(in.Duration.ValueString())
}

var _ resource.ResourceWithConfigure = &serviceWaitResource{}

func NewServiceWaitResource() resource.Resource {
return &serviceWaitResource{}
}

type serviceWaitResource struct {
client *client.Client
}

func (in *serviceWaitResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) {
response.TypeName = request.ProviderTypeName + "_service_wait"
}

func (in *serviceWaitResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) {
response.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"cluster": schema.StringAttribute{
Description: "Handle of the cluster where the service is deployed.",
MarkdownDescription: "Handle of the cluster where the service is deployed.",
Required: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
},
"service": schema.StringAttribute{
Description: "Name the service deployment that should be checked.",
MarkdownDescription: "Name the service deployment that should be checked.",
Required: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
},
"warmup": schema.StringAttribute{
Description: "Initial delay before checking the service deployment health. Defaults to 5 minutes.",
MarkdownDescription: "Initial delay before checking the service deployment health. Defaults to 5 minutes.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString("5m"),
Validators: []validator.String{customvalidator.Duration()},
},
"duration": schema.StringAttribute{
Description: "Maximum duration to wait for the service deployment to become healthy. Minimum 1 minute. Defaults to 10 minutes.",
MarkdownDescription: "Maximum duration to wait for the service deployment to become healthy. Minimum 1 minute. Defaults to 10 minutes.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString("10m"),
Validators: []validator.String{customvalidator.MinDuration(time.Minute)},
},
},
}
}

func (in *serviceWaitResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) {
if request.ProviderData == nil {
return
}

data, ok := request.ProviderData.(*common.ProviderData)
if !ok {
response.Diagnostics.AddError(
"Unexpected Project Resource Configure Type",
fmt.Sprintf("Expected *common.ProviderData, got: %T. Please report this issue to the provider developers.", request.ProviderData),
)
return
}

in.client = data.Client
}

func (in *serviceWaitResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
data := new(serviceWait)
response.Diagnostics.Append(request.Plan.Get(ctx, data)...)
if response.Diagnostics.HasError() {
return
}

if err := in.Wait(ctx, data); err != nil {
response.Diagnostics.AddError("Client Error", fmt.Sprintf("Got error while waiting for service: %s", err))
return
}

response.Diagnostics.Append(response.State.Set(ctx, &data)...)
}

func (in *serviceWaitResource) Read(_ context.Context, _ resource.ReadRequest, _ *resource.ReadResponse) {
// Ignore.
}

func (in *serviceWaitResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data serviceWait
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (in *serviceWaitResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
// Ignore.
}

func (in *serviceWaitResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("service_id"), req, resp)
}

func (in *serviceWaitResource) Wait(ctx context.Context, data *serviceWait) error {
warmup, err := data.ParseWarmup()
if err != nil {
return fmt.Errorf("unable to parse warmup duration, got error: %s", err.Error())
}

duration, err := data.ParseDuration()
if err != nil {
return fmt.Errorf("unable to parse duration, got error: %s", err.Error())
}

tflog.Info(ctx, fmt.Sprintf("waiting for warmup period of %s before starting health checks...", warmup))
time.Sleep(warmup)
tflog.Info(ctx, "warmup period completed, starting health checks")

if err = wait.PollUntilContextTimeout(context.Background(), 30*time.Second, duration, true, func(pollCtx context.Context) (done bool, err error) {
service, err := in.client.GetServiceDeploymentByHandle(pollCtx, data.Cluster.ValueString(), data.Service.ValueString())
if err != nil {
tflog.Warn(ctx, fmt.Sprintf("failed to get service %s, got error: %s", data.Service.ValueString(), err.Error()))
return false, nil
}

tflog.Debug(ctx, fmt.Sprintf("service %s is %s", service.ServiceDeployment.ID, service.ServiceDeployment.Status))
return service.ServiceDeployment.Status == console.ServiceDeploymentStatusHealthy, nil
}); err != nil {
tflog.Warn(ctx, fmt.Sprintf("service %s did not become healthy within %s, got error: %s", data.Service.ValueString(), duration, err.Error()))
return fmt.Errorf("service %s did not become healthy within %s, got error: %s", data.Service.ValueString(), duration, err.Error())
}

tflog.Info(ctx, fmt.Sprintf("service %s health check completed successfully", data.Service.ValueString()))
return nil
}
44 changes: 44 additions & 0 deletions internal/validator/duration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package validator

import (
"context"
"fmt"
"time"

"github.com/hashicorp/terraform-plugin-framework/schema/validator"
)

var _ validator.String = durationValidator{}

// durationValidator validates that a string is a valid time.Duration.
type durationValidator struct{}

func (v durationValidator) Description(_ context.Context) string {
return "Value must be a valid duration string (e.g., '5m', '1h30m', '500ms')."
}

func (v durationValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}

func (v durationValidator) ValidateString(_ context.Context, request validator.StringRequest, response *validator.StringResponse) {
if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() {
return
}

value := request.ConfigValue.ValueString()

if _, err := time.ParseDuration(value); err != nil {
response.Diagnostics.AddAttributeError(
request.Path,
"Invalid Duration",
fmt.Sprintf("Value %q is not a valid duration string: %s. Valid examples: '5m', '1h30m', '500ms'", value, err.Error()),
)
}
}

// Duration returns a validator which ensures that the configured string value
// is a valid time.Duration.
func Duration() validator.String {
return durationValidator{}
}
58 changes: 58 additions & 0 deletions internal/validator/min_duration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package validator

import (
"context"
"fmt"
"time"

"github.com/hashicorp/terraform-plugin-framework/schema/validator"
)

var _ validator.String = minDurationValidator{}

// minDurationValidator validates that a string is a valid time.Duration and meets a minimum value.
type minDurationValidator struct {
minDuration time.Duration
}

func (v minDurationValidator) Description(_ context.Context) string {
return fmt.Sprintf("Value must be a valid duration string and at least %s.", v.minDuration)
}

func (v minDurationValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}

func (v minDurationValidator) ValidateString(_ context.Context, request validator.StringRequest, response *validator.StringResponse) {
if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() {
return
}

value := request.ConfigValue.ValueString()

duration, err := time.ParseDuration(value)
if err != nil {
response.Diagnostics.AddAttributeError(
request.Path,
"Invalid Duration",
fmt.Sprintf("Value %q is not a valid duration string: %s. Valid examples: '5m', '1h30m', '500ms'", value, err.Error()),
)
return
}

if duration < v.minDuration {
response.Diagnostics.AddAttributeError(
request.Path,
"Duration Too Short",
fmt.Sprintf("Value %q (%s) is less than the minimum allowed duration of %s", value, duration, v.minDuration),
)
}
}

// MinDuration returns a validator which ensures that the configured string value
// is a valid time.Duration and is at least the specified minimum duration.
func MinDuration(minDuration time.Duration) validator.String {
return minDurationValidator{
minDuration: minDuration,
}
}
47 changes: 47 additions & 0 deletions internal/validator/uuid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package validator

import (
"context"
"fmt"
"regexp"

"github.com/hashicorp/terraform-plugin-framework/schema/validator"
)

var _ validator.String = uuidValidator{}

// uuidValidator validates that a string is a valid UUID (v4 format).
type uuidValidator struct{}

// UUID regex pattern that matches standard UUID format (8-4-4-4-12 hex digits)
var uuidRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)

func (v uuidValidator) Description(_ context.Context) string {
return "Value must be a valid UUID."
}

func (v uuidValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}

func (v uuidValidator) ValidateString(_ context.Context, request validator.StringRequest, response *validator.StringResponse) {
if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() {
return
}

value := request.ConfigValue.ValueString()

if !uuidRegex.MatchString(value) {
response.Diagnostics.AddAttributeError(
request.Path,
"Invalid UUID",
fmt.Sprintf("Value %q is not a valid UUID. Expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", value),
)
}
}

// UUID returns a validator which ensures that the configured string value
// is a valid UUID.
func UUID() validator.String {
return uuidValidator{}
}
Loading