diff --git a/docs/resources/chickhouse.md b/docs/resources/chickhouse.md new file mode 100644 index 0000000..f05bd33 --- /dev/null +++ b/docs/resources/chickhouse.md @@ -0,0 +1,22 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_chickhouse Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_chickhouse (Resource) + + + + + + +## Schema + +### Required + +- `service_name` (String) Service name to create + + diff --git a/docs/resources/clickhouse_link.md b/docs/resources/clickhouse_link.md new file mode 100644 index 0000000..2b20f30 --- /dev/null +++ b/docs/resources/clickhouse_link.md @@ -0,0 +1,23 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_clickhouse_link Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_clickhouse_link (Resource) + + + + + + +## Schema + +### Required + +- `app_name` (String) App name to apply link service to +- `service_name` (String) Service name to link + + diff --git a/docs/resources/couchdb.md b/docs/resources/couchdb.md new file mode 100644 index 0000000..387c360 --- /dev/null +++ b/docs/resources/couchdb.md @@ -0,0 +1,22 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_couchdb Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_couchdb (Resource) + + + + + + +## Schema + +### Required + +- `service_name` (String) Service name to create + + diff --git a/docs/resources/couchdb_link.md b/docs/resources/couchdb_link.md new file mode 100644 index 0000000..df43afd --- /dev/null +++ b/docs/resources/couchdb_link.md @@ -0,0 +1,23 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_couchdb_link Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_couchdb_link (Resource) + + + + + + +## Schema + +### Required + +- `app_name` (String) App name to apply link service to +- `service_name` (String) Service name to link + + diff --git a/docs/resources/elasticsearch.md b/docs/resources/elasticsearch.md new file mode 100644 index 0000000..265d31b --- /dev/null +++ b/docs/resources/elasticsearch.md @@ -0,0 +1,22 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_elasticsearch Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_elasticsearch (Resource) + + + + + + +## Schema + +### Required + +- `service_name` (String) Service name to create + + diff --git a/docs/resources/elasticsearch_link.md b/docs/resources/elasticsearch_link.md new file mode 100644 index 0000000..e9a8af8 --- /dev/null +++ b/docs/resources/elasticsearch_link.md @@ -0,0 +1,23 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_elasticsearch_link Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_elasticsearch_link (Resource) + + + + + + +## Schema + +### Required + +- `app_name` (String) App name to apply link service to +- `service_name` (String) Service name to link + + diff --git a/docs/resources/mariadb.md b/docs/resources/mariadb.md new file mode 100644 index 0000000..19605fe --- /dev/null +++ b/docs/resources/mariadb.md @@ -0,0 +1,22 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_mariadb Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_mariadb (Resource) + + + + + + +## Schema + +### Required + +- `service_name` (String) Service name to create + + diff --git a/docs/resources/mariadb_link.md b/docs/resources/mariadb_link.md new file mode 100644 index 0000000..595bf95 --- /dev/null +++ b/docs/resources/mariadb_link.md @@ -0,0 +1,23 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_mariadb_link Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_mariadb_link (Resource) + + + + + + +## Schema + +### Required + +- `app_name` (String) App name to apply link service to +- `service_name` (String) Service name to link + + diff --git a/docs/resources/mongo.md b/docs/resources/mongo.md new file mode 100644 index 0000000..27a8a7f --- /dev/null +++ b/docs/resources/mongo.md @@ -0,0 +1,22 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_mongo Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_mongo (Resource) + + + + + + +## Schema + +### Required + +- `service_name` (String) Service name to create + + diff --git a/docs/resources/mongo_link.md b/docs/resources/mongo_link.md new file mode 100644 index 0000000..d525f69 --- /dev/null +++ b/docs/resources/mongo_link.md @@ -0,0 +1,23 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_mongo_link Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_mongo_link (Resource) + + + + + + +## Schema + +### Required + +- `app_name` (String) App name to apply link service to +- `service_name` (String) Service name to link + + diff --git a/docs/resources/mysql.md b/docs/resources/mysql.md new file mode 100644 index 0000000..5e1a546 --- /dev/null +++ b/docs/resources/mysql.md @@ -0,0 +1,22 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_mysql Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_mysql (Resource) + + + + + + +## Schema + +### Required + +- `service_name` (String) Service name to create + + diff --git a/docs/resources/mysql_link.md b/docs/resources/mysql_link.md new file mode 100644 index 0000000..8b566fd --- /dev/null +++ b/docs/resources/mysql_link.md @@ -0,0 +1,23 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_mysql_link Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_mysql_link (Resource) + + + + + + +## Schema + +### Required + +- `app_name` (String) App name to apply link service to +- `service_name` (String) Service name to link + + diff --git a/docs/resources/nats.md b/docs/resources/nats.md new file mode 100644 index 0000000..5b6f113 --- /dev/null +++ b/docs/resources/nats.md @@ -0,0 +1,22 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_nats Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_nats (Resource) + + + + + + +## Schema + +### Required + +- `service_name` (String) Service name to create + + diff --git a/docs/resources/nats_link.md b/docs/resources/nats_link.md new file mode 100644 index 0000000..34135c2 --- /dev/null +++ b/docs/resources/nats_link.md @@ -0,0 +1,23 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_nats_link Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_nats_link (Resource) + + + + + + +## Schema + +### Required + +- `app_name` (String) App name to apply link service to +- `service_name` (String) Service name to link + + diff --git a/docs/resources/rabbitmq.md b/docs/resources/rabbitmq.md new file mode 100644 index 0000000..56b58fd --- /dev/null +++ b/docs/resources/rabbitmq.md @@ -0,0 +1,22 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_rabbitmq Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_rabbitmq (Resource) + + + + + + +## Schema + +### Required + +- `service_name` (String) Service name to create + + diff --git a/docs/resources/rabbitmq_link.md b/docs/resources/rabbitmq_link.md new file mode 100644 index 0000000..8280fee --- /dev/null +++ b/docs/resources/rabbitmq_link.md @@ -0,0 +1,23 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_rabbitmq_link Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_rabbitmq_link (Resource) + + + + + + +## Schema + +### Required + +- `app_name` (String) App name to apply link service to +- `service_name` (String) Service name to link + + diff --git a/docs/resources/redis.md b/docs/resources/redis.md new file mode 100644 index 0000000..67650a3 --- /dev/null +++ b/docs/resources/redis.md @@ -0,0 +1,22 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_redis Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_redis (Resource) + + + + + + +## Schema + +### Required + +- `service_name` (String) Service name to create + + diff --git a/docs/resources/redis_link.md b/docs/resources/redis_link.md new file mode 100644 index 0000000..80e07b0 --- /dev/null +++ b/docs/resources/redis_link.md @@ -0,0 +1,23 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_redis_link Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_redis_link (Resource) + + + + + + +## Schema + +### Required + +- `app_name` (String) App name to apply link service to +- `service_name` (String) Service name to link + + diff --git a/docs/resources/rethinkdb.md b/docs/resources/rethinkdb.md new file mode 100644 index 0000000..e30b4df --- /dev/null +++ b/docs/resources/rethinkdb.md @@ -0,0 +1,22 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_rethinkdb Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_rethinkdb (Resource) + + + + + + +## Schema + +### Required + +- `service_name` (String) Service name to create + + diff --git a/docs/resources/rethinkdb_link.md b/docs/resources/rethinkdb_link.md new file mode 100644 index 0000000..2addd3d --- /dev/null +++ b/docs/resources/rethinkdb_link.md @@ -0,0 +1,23 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokku_rethinkdb_link Resource - terraform-provider-dokku" +subcategory: "" +description: |- + +--- + +# dokku_rethinkdb_link (Resource) + + + + + + +## Schema + +### Required + +- `app_name` (String) App name to apply link service to +- `service_name` (String) Service name to link + + diff --git a/internal/provider/dokku_client/postgres_link.go b/internal/provider/dokku_client/postgres_link.go deleted file mode 100644 index 72b9b30..0000000 --- a/internal/provider/dokku_client/postgres_link.go +++ /dev/null @@ -1,31 +0,0 @@ -package dokkuclient - -import ( - "context" - "fmt" - "strings" -) - -func (c *Client) PostgresLinkExists(ctx context.Context, serviceName string, appName string) (bool, error) { - stdout, _, err := c.RunQuiet(ctx, fmt.Sprintf("postgres:linked %s %s", serviceName, appName)) - if err != nil { - if strings.Contains(stdout, fmt.Sprintf("Service %s is not linked to %s", serviceName, appName)) { - return false, nil - } - if strings.Contains(stdout, fmt.Sprintf("App %s does not exist", appName)) { - return false, nil - } - return false, err - } - return true, nil -} - -func (c *Client) PostgresLinkCreate(ctx context.Context, serviceName string, appName string) error { - _, _, err := c.RunQuiet(ctx, fmt.Sprintf("postgres:link %s %s", serviceName, appName)) - return err -} - -func (c *Client) PostgresLinkRemove(ctx context.Context, serviceName string, appName string) error { - _, _, err := c.RunQuiet(ctx, fmt.Sprintf("postgres:unlink %s %s", serviceName, appName)) - return err -} diff --git a/internal/provider/dokku_client/postgres_service.go b/internal/provider/dokku_client/postgres_service.go deleted file mode 100644 index 260d7a1..0000000 --- a/internal/provider/dokku_client/postgres_service.go +++ /dev/null @@ -1,29 +0,0 @@ -package dokkuclient - -import ( - "context" - "fmt" - "strings" -) - -func (c *Client) PostgresServiceExists(ctx context.Context, serviceName string) (bool, error) { - stdout, _, err := c.RunQuiet(ctx, fmt.Sprintf("postgres:exists %s", serviceName)) - if err != nil { - if strings.Contains(stdout, fmt.Sprintf("Postgres service %s does not exist", serviceName)) { - return false, nil - } - - return false, err - } - return true, nil -} - -func (c *Client) PostgresServiceDestroy(ctx context.Context, serviceName string) error { - _, _, err := c.RunQuiet(ctx, fmt.Sprintf("postgres:destroy %s --force", serviceName)) - return err -} - -func (c *Client) PostgresServiceCreate(ctx context.Context, serviceName string) error { - _, _, err := c.RunQuiet(ctx, fmt.Sprintf("postgres:create %s", serviceName)) - return err -} diff --git a/internal/provider/dokku_client/simple_service.go b/internal/provider/dokku_client/simple_service.go new file mode 100644 index 0000000..722d656 --- /dev/null +++ b/internal/provider/dokku_client/simple_service.go @@ -0,0 +1,29 @@ +package dokkuclient + +import ( + "context" + "fmt" + "strings" +) + +func (c *Client) SimpleServiceExists(ctx context.Context, servicePluginName string, serviceName string) (bool, error) { + stdout, _, err := c.RunQuiet(ctx, fmt.Sprintf("%s:exists %s", servicePluginName, serviceName)) + if err != nil { + if strings.Contains(stdout, fmt.Sprintf("%s service %s does not exist", servicePluginName, serviceName)) { + return false, nil + } + + return false, err + } + return true, nil +} + +func (c *Client) SimpleServiceDestroy(ctx context.Context, servicePluginName string, serviceName string) error { + _, _, err := c.RunQuiet(ctx, fmt.Sprintf("%s:destroy %s --force", servicePluginName, serviceName)) + return err +} + +func (c *Client) SimpleServiceCreate(ctx context.Context, servicePluginName string, serviceName string) error { + _, _, err := c.RunQuiet(ctx, fmt.Sprintf("%s:create %s", servicePluginName, serviceName)) + return err +} diff --git a/internal/provider/dokku_client/simple_service_link.go b/internal/provider/dokku_client/simple_service_link.go new file mode 100644 index 0000000..342892e --- /dev/null +++ b/internal/provider/dokku_client/simple_service_link.go @@ -0,0 +1,31 @@ +package dokkuclient + +import ( + "context" + "fmt" + "strings" +) + +func (c *Client) SimpleServiceLinkExists(ctx context.Context, servicePluginName string, serviceName string, appName string) (bool, error) { + stdout, _, err := c.RunQuiet(ctx, fmt.Sprintf("%s:linked %s %s", servicePluginName, serviceName, appName)) + if err != nil { + if strings.Contains(stdout, fmt.Sprintf("Service %s (%s) is not linked to %s", serviceName, servicePluginName, appName)) { + return false, nil + } + if strings.Contains(stdout, fmt.Sprintf("App %s does not exist", appName)) { + return false, nil + } + return false, err + } + return true, nil +} + +func (c *Client) SimpleServiceLinkCreate(ctx context.Context, servicePluginName string, serviceName string, appName string) error { + _, _, err := c.RunQuiet(ctx, fmt.Sprintf("%s:link %s %s", servicePluginName, serviceName, appName)) + return err +} + +func (c *Client) SimpleServiceLinkRemove(ctx context.Context, servicePluginName string, serviceName string, appName string) error { + _, _, err := c.RunQuiet(ctx, fmt.Sprintf("%s:unlink %s %s", servicePluginName, serviceName, appName)) + return err +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 5b9a643..b29390b 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -11,6 +11,7 @@ import ( "strings" dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + "terraform-provider-dokku/internal/provider/services" "github.com/blang/semver" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" @@ -300,8 +301,29 @@ func (p *dokkuProvider) Resources(ctx context.Context) []func() resource.Resourc NewHttpAuthResource, NewLetsencryptResource, NewPluginResource, - NewPostgresLinkResource, - NewPostgresResource, + + services.NewClickhouseLinkResource, + services.NewClickhouseResource, + services.NewCouchDBLinkResource, + services.NewCouchDBResource, + services.NewElasticsearchLinkResource, + services.NewElasticsearchResource, + services.NewMariaDBLinkResource, + services.NewMariaDBResource, + services.NewMongoLinkResource, + services.NewMongoResource, + services.NewMysqlLinkResource, + services.NewMysqlResource, + services.NewNatsLinkResource, + services.NewNatsResource, + services.NewPostgresLinkResource, + services.NewPostgresResource, + services.NewRabbitMQLinkResource, + services.NewRabbitMQResource, + services.NewRedisLinkResource, + services.NewRedisResource, + services.NewRethinkDBLinkResource, + services.NewRethinkDBResource, } } diff --git a/internal/provider/services/clickhouse_link_resource.go b/internal/provider/services/clickhouse_link_resource.go new file mode 100644 index 0000000..b12a8a1 --- /dev/null +++ b/internal/provider/services/clickhouse_link_resource.go @@ -0,0 +1,216 @@ +package services + +import ( + "context" + "regexp" + "strings" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &clickhouseLinkResource{} + _ resource.ResourceWithConfigure = &clickhouseLinkResource{} + _ resource.ResourceWithImportState = &clickhouseLinkResource{} +) + +func NewClickhouseLinkResource() resource.Resource { + return &clickhouseLinkResource{} +} + +type clickhouseLinkResource struct { + client *dokkuclient.Client +} + +type clickhouseLinkResourceModel struct { + AppName types.String `tfsdk:"app_name"` + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *clickhouseLinkResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_clickhouse_link" +} + +// Configure adds the provider configured client to the resource. +func (r *clickhouseLinkResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *clickhouseLinkResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "app_name": schema.StringAttribute{ + Required: true, + Description: "App name to apply link service to", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid app_name"), + }, + }, + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to link", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *clickhouseLinkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state clickhouseLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "clickhouse", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check clickhouse service existence", "Unable to check clickhouse service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddError("Unable to find clickhouse service", "Unable to find clickhouse service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "clickhouse", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check clickhouse link existence", "Unable to check clickhouse link existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *clickhouseLinkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan clickhouseLinkResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "clickhouse", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check clickhouse service existence", "Unable to check clickhouse service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "Unable to find clickhouse service", "Unable to find clickhouse service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "clickhouse", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check clickhouse link existence", "Unable to check clickhouse link existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddError("Service already linked to app", "Service already linked to app") + return + } + + // Create link + err = r.client.SimpleServiceLinkCreate(ctx, "clickhouse", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create clickhouse link", "Unable to create clickhouse link. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *clickhouseLinkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *clickhouseLinkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state clickhouseLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "clickhouse", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check clickhouse service existence", "Unable to check clickhouse service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "clickhouse", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check clickhouse link existence", "Unable to check clickhouse link existence. "+err.Error()) + return + } + if !exists { + return + } + + // Unlink service + err = r.client.SimpleServiceLinkRemove(ctx, "clickhouse", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to unlink service from app", "Unable to unlink service from app. "+err.Error()) + return + } +} + +func (r *clickhouseLinkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.Split(req.ID, " ") + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("app_name"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), parts[1])...) +} diff --git a/internal/provider/services/clickhouse_resource.go b/internal/provider/services/clickhouse_resource.go new file mode 100644 index 0000000..0230e7c --- /dev/null +++ b/internal/provider/services/clickhouse_resource.go @@ -0,0 +1,170 @@ +package services + +import ( + "context" + "regexp" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &chickhouseResource{} + _ resource.ResourceWithConfigure = &chickhouseResource{} + _ resource.ResourceWithImportState = &chickhouseResource{} +) + +func NewClickhouseResource() resource.Resource { + return &chickhouseResource{} +} + +type chickhouseResource struct { + client *dokkuclient.Client +} + +type chickhouseResourceModel struct { + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *chickhouseResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_chickhouse" +} + +// Configure adds the provider configured client to the resource. +func (r *chickhouseResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *chickhouseResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to create", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *chickhouseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state chickhouseResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "clickhouse", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check chickhouse service existence", "Unable to check chickhouse service existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *chickhouseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan chickhouseResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Create service is not exists + exists, err := r.client.SimpleServiceExists(ctx, "clickhouse", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check chickhouse service existence", "Unable to check chickhouse service existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "Clickhouse service already exists", "Clickhouse service already exists") + return + } + + err = r.client.SimpleServiceCreate(ctx, "clickhouse", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create chickhouse service", "Unable to create chickhouse service. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *chickhouseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *chickhouseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state chickhouseResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "clickhouse", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check chickhouse service existence", "Unable to check chickhouse service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Destroy instance + err = r.client.SimpleServiceDestroy(ctx, "clickhouse", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to destroy service", "Unable to destroy service. "+err.Error()) + return + } +} + +func (r *chickhouseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Retrieve import ID and save to service_name attribute + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), req.ID)...) +} diff --git a/internal/provider/services/couchdb_link_resource.go b/internal/provider/services/couchdb_link_resource.go new file mode 100644 index 0000000..efbbd33 --- /dev/null +++ b/internal/provider/services/couchdb_link_resource.go @@ -0,0 +1,216 @@ +package services + +import ( + "context" + "regexp" + "strings" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &couchDBLinkResource{} + _ resource.ResourceWithConfigure = &couchDBLinkResource{} + _ resource.ResourceWithImportState = &couchDBLinkResource{} +) + +func NewCouchDBLinkResource() resource.Resource { + return &couchDBLinkResource{} +} + +type couchDBLinkResource struct { + client *dokkuclient.Client +} + +type couchDBLinkResourceModel struct { + AppName types.String `tfsdk:"app_name"` + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *couchDBLinkResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_couchdb_link" +} + +// Configure adds the provider configured client to the resource. +func (r *couchDBLinkResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *couchDBLinkResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "app_name": schema.StringAttribute{ + Required: true, + Description: "App name to apply link service to", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid app_name"), + }, + }, + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to link", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *couchDBLinkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state couchDBLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "couchdb", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check couchDB service existence", "Unable to check couchDB service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddError("Unable to find couchDB service", "Unable to find couchDB service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "couchdb", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check couchDB link existence", "Unable to check couchDB link existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *couchDBLinkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan couchDBLinkResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "couchdb", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check couchDB service existence", "Unable to check couchDB service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "Unable to find couchDB service", "Unable to find couchDB service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "couchdb", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check couchDB link existence", "Unable to check couchDB link existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddError("Service already linked to app", "Service already linked to app") + return + } + + // Create link + err = r.client.SimpleServiceLinkCreate(ctx, "couchdb", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create couchDB link", "Unable to create couchDB link. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *couchDBLinkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *couchDBLinkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state couchDBLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "couchdb", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check couchDB service existence", "Unable to check couchDB service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "couchdb", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check couchDB link existence", "Unable to check couchDB link existence. "+err.Error()) + return + } + if !exists { + return + } + + // Unlink service + err = r.client.SimpleServiceLinkRemove(ctx, "couchdb", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to unlink service from app", "Unable to unlink service from app. "+err.Error()) + return + } +} + +func (r *couchDBLinkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.Split(req.ID, " ") + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("app_name"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), parts[1])...) +} diff --git a/internal/provider/services/couchdb_resource.go b/internal/provider/services/couchdb_resource.go new file mode 100644 index 0000000..c98ee24 --- /dev/null +++ b/internal/provider/services/couchdb_resource.go @@ -0,0 +1,170 @@ +package services + +import ( + "context" + "regexp" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &couchDBResource{} + _ resource.ResourceWithConfigure = &couchDBResource{} + _ resource.ResourceWithImportState = &couchDBResource{} +) + +func NewCouchDBResource() resource.Resource { + return &couchDBResource{} +} + +type couchDBResource struct { + client *dokkuclient.Client +} + +type couchDBResourceModel struct { + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *couchDBResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_couchdb" +} + +// Configure adds the provider configured client to the resource. +func (r *couchDBResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *couchDBResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to create", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *couchDBResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state couchDBResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "couchdb", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check couchDB service existence", "Unable to check couchDB service existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *couchDBResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan couchDBResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Create service is not exists + exists, err := r.client.SimpleServiceExists(ctx, "couchdb", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check couchDB service existence", "Unable to check couchDB service existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "CouchDB service already exists", "CouchDB service already exists") + return + } + + err = r.client.SimpleServiceCreate(ctx, "couchdb", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create couchDB service", "Unable to create couchDB service. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *couchDBResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *couchDBResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state couchDBResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "couchdb", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check couchDB service existence", "Unable to check couchDB service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Destroy instance + err = r.client.SimpleServiceDestroy(ctx, "couchdb", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to destroy service", "Unable to destroy service. "+err.Error()) + return + } +} + +func (r *couchDBResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Retrieve import ID and save to service_name attribute + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), req.ID)...) +} diff --git a/internal/provider/services/elasticsearch_link_resource.go b/internal/provider/services/elasticsearch_link_resource.go new file mode 100644 index 0000000..398df1d --- /dev/null +++ b/internal/provider/services/elasticsearch_link_resource.go @@ -0,0 +1,216 @@ +package services + +import ( + "context" + "regexp" + "strings" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &elasticsearchLinkResource{} + _ resource.ResourceWithConfigure = &elasticsearchLinkResource{} + _ resource.ResourceWithImportState = &elasticsearchLinkResource{} +) + +func NewElasticsearchLinkResource() resource.Resource { + return &elasticsearchLinkResource{} +} + +type elasticsearchLinkResource struct { + client *dokkuclient.Client +} + +type elasticsearchLinkResourceModel struct { + AppName types.String `tfsdk:"app_name"` + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *elasticsearchLinkResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_elasticsearch_link" +} + +// Configure adds the provider configured client to the resource. +func (r *elasticsearchLinkResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *elasticsearchLinkResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "app_name": schema.StringAttribute{ + Required: true, + Description: "App name to apply link service to", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid app_name"), + }, + }, + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to link", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *elasticsearchLinkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state elasticsearchLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "elasticsearch", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check elasticsearch service existence", "Unable to check elasticsearch service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddError("Unable to find elasticsearch service", "Unable to find elasticsearch service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "elasticsearch", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check elasticsearch link existence", "Unable to check elasticsearch link existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *elasticsearchLinkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan elasticsearchLinkResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "elasticsearch", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check elasticsearch service existence", "Unable to check elasticsearch service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "Unable to find elasticsearch service", "Unable to find elasticsearch service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "elasticsearch", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check elasticsearch link existence", "Unable to check elasticsearch link existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddError("Service already linked to app", "Service already linked to app") + return + } + + // Create link + err = r.client.SimpleServiceLinkCreate(ctx, "elasticsearch", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create elasticsearch link", "Unable to create elasticsearch link. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *elasticsearchLinkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *elasticsearchLinkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state elasticsearchLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "elasticsearch", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check elasticsearch service existence", "Unable to check elasticsearch service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "elasticsearch", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check elasticsearch link existence", "Unable to check elasticsearch link existence. "+err.Error()) + return + } + if !exists { + return + } + + // Unlink service + err = r.client.SimpleServiceLinkRemove(ctx, "elasticsearch", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to unlink service from app", "Unable to unlink service from app. "+err.Error()) + return + } +} + +func (r *elasticsearchLinkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.Split(req.ID, " ") + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("app_name"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), parts[1])...) +} diff --git a/internal/provider/services/elasticsearch_resource.go b/internal/provider/services/elasticsearch_resource.go new file mode 100644 index 0000000..805ec24 --- /dev/null +++ b/internal/provider/services/elasticsearch_resource.go @@ -0,0 +1,170 @@ +package services + +import ( + "context" + "regexp" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &elasticsearchResource{} + _ resource.ResourceWithConfigure = &elasticsearchResource{} + _ resource.ResourceWithImportState = &elasticsearchResource{} +) + +func NewElasticsearchResource() resource.Resource { + return &elasticsearchResource{} +} + +type elasticsearchResource struct { + client *dokkuclient.Client +} + +type elasticsearchResourceModel struct { + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *elasticsearchResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_elasticsearch" +} + +// Configure adds the provider configured client to the resource. +func (r *elasticsearchResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *elasticsearchResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to create", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *elasticsearchResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state elasticsearchResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "elasticsearch", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check elasticsearch service existence", "Unable to check elasticsearch service existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *elasticsearchResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan elasticsearchResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Create service is not exists + exists, err := r.client.SimpleServiceExists(ctx, "elasticsearch", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check elasticsearch service existence", "Unable to check elasticsearch service existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "Elasticsearch service already exists", "Elasticsearch service already exists") + return + } + + err = r.client.SimpleServiceCreate(ctx, "elasticsearch", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create elasticsearch service", "Unable to create elasticsearch service. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *elasticsearchResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *elasticsearchResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state elasticsearchResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "elasticsearch", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check elasticsearch service existence", "Unable to check elasticsearch service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Destroy instance + err = r.client.SimpleServiceDestroy(ctx, "elasticsearch", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to destroy service", "Unable to destroy service. "+err.Error()) + return + } +} + +func (r *elasticsearchResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Retrieve import ID and save to service_name attribute + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), req.ID)...) +} diff --git a/internal/provider/services/mariadb_link_resource.go b/internal/provider/services/mariadb_link_resource.go new file mode 100644 index 0000000..08e6d16 --- /dev/null +++ b/internal/provider/services/mariadb_link_resource.go @@ -0,0 +1,216 @@ +package services + +import ( + "context" + "regexp" + "strings" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &mariaDBLinkResource{} + _ resource.ResourceWithConfigure = &mariaDBLinkResource{} + _ resource.ResourceWithImportState = &mariaDBLinkResource{} +) + +func NewMariaDBLinkResource() resource.Resource { + return &mariaDBLinkResource{} +} + +type mariaDBLinkResource struct { + client *dokkuclient.Client +} + +type mariaDBLinkResourceModel struct { + AppName types.String `tfsdk:"app_name"` + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *mariaDBLinkResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mariadb_link" +} + +// Configure adds the provider configured client to the resource. +func (r *mariaDBLinkResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *mariaDBLinkResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "app_name": schema.StringAttribute{ + Required: true, + Description: "App name to apply link service to", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid app_name"), + }, + }, + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to link", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *mariaDBLinkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state mariaDBLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "mariadb", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mariaDB service existence", "Unable to check mariaDB service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddError("Unable to find mariaDB service", "Unable to find mariaDB service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "mariadb", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mariaDB link existence", "Unable to check mariaDB link existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *mariaDBLinkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan mariaDBLinkResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "mariadb", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mariaDB service existence", "Unable to check mariaDB service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "Unable to find mariaDB service", "Unable to find mariaDB service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "mariadb", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mariaDB link existence", "Unable to check mariaDB link existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddError("Service already linked to app", "Service already linked to app") + return + } + + // Create link + err = r.client.SimpleServiceLinkCreate(ctx, "mariadb", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create mariaDB link", "Unable to create mariaDB link. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *mariaDBLinkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *mariaDBLinkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state mariaDBLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "mariadb", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mariaDB service existence", "Unable to check mariaDB service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "mariadb", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mariaDB link existence", "Unable to check mariaDB link existence. "+err.Error()) + return + } + if !exists { + return + } + + // Unlink service + err = r.client.SimpleServiceLinkRemove(ctx, "mariadb", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to unlink service from app", "Unable to unlink service from app. "+err.Error()) + return + } +} + +func (r *mariaDBLinkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.Split(req.ID, " ") + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("app_name"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), parts[1])...) +} diff --git a/internal/provider/services/mariadb_resource.go b/internal/provider/services/mariadb_resource.go new file mode 100644 index 0000000..1d0dc79 --- /dev/null +++ b/internal/provider/services/mariadb_resource.go @@ -0,0 +1,170 @@ +package services + +import ( + "context" + "regexp" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &mariaDBResource{} + _ resource.ResourceWithConfigure = &mariaDBResource{} + _ resource.ResourceWithImportState = &mariaDBResource{} +) + +func NewMariaDBResource() resource.Resource { + return &mariaDBResource{} +} + +type mariaDBResource struct { + client *dokkuclient.Client +} + +type mariaDBResourceModel struct { + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *mariaDBResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mariadb" +} + +// Configure adds the provider configured client to the resource. +func (r *mariaDBResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *mariaDBResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to create", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *mariaDBResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state mariaDBResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "mariadb", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mariaDB service existence", "Unable to check mariaDB service existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *mariaDBResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan mariaDBResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Create service is not exists + exists, err := r.client.SimpleServiceExists(ctx, "mariadb", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mariaDB service existence", "Unable to check mariaDB service existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "MariaDB service already exists", "MariaDB service already exists") + return + } + + err = r.client.SimpleServiceCreate(ctx, "mariadb", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create mariaDB service", "Unable to create mariaDB service. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *mariaDBResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *mariaDBResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state mariaDBResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "mariadb", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mariaDB service existence", "Unable to check mariaDB service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Destroy instance + err = r.client.SimpleServiceDestroy(ctx, "mariadb", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to destroy service", "Unable to destroy service. "+err.Error()) + return + } +} + +func (r *mariaDBResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Retrieve import ID and save to service_name attribute + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), req.ID)...) +} diff --git a/internal/provider/services/mongo_link_resource.go b/internal/provider/services/mongo_link_resource.go new file mode 100644 index 0000000..2867c45 --- /dev/null +++ b/internal/provider/services/mongo_link_resource.go @@ -0,0 +1,216 @@ +package services + +import ( + "context" + "regexp" + "strings" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &mongoLinkResource{} + _ resource.ResourceWithConfigure = &mongoLinkResource{} + _ resource.ResourceWithImportState = &mongoLinkResource{} +) + +func NewMongoLinkResource() resource.Resource { + return &mongoLinkResource{} +} + +type mongoLinkResource struct { + client *dokkuclient.Client +} + +type mongoLinkResourceModel struct { + AppName types.String `tfsdk:"app_name"` + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *mongoLinkResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mongo_link" +} + +// Configure adds the provider configured client to the resource. +func (r *mongoLinkResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *mongoLinkResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "app_name": schema.StringAttribute{ + Required: true, + Description: "App name to apply link service to", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid app_name"), + }, + }, + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to link", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *mongoLinkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state mongoLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "mongo", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mongo service existence", "Unable to check mongo service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddError("Unable to find mongo service", "Unable to find mongo service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "mongo", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mongo link existence", "Unable to check mongo link existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *mongoLinkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan mongoLinkResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "mongo", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mongo service existence", "Unable to check mongo service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "Unable to find mongo service", "Unable to find mongo service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "mongo", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mongo link existence", "Unable to check mongo link existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddError("Service already linked to app", "Service already linked to app") + return + } + + // Create link + err = r.client.SimpleServiceLinkCreate(ctx, "mongo", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create mongo link", "Unable to create mongo link. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *mongoLinkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *mongoLinkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state mongoLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "mongo", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mongo service existence", "Unable to check mongo service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "mongo", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mongo link existence", "Unable to check mongo link existence. "+err.Error()) + return + } + if !exists { + return + } + + // Unlink service + err = r.client.SimpleServiceLinkRemove(ctx, "mongo", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to unlink service from app", "Unable to unlink service from app. "+err.Error()) + return + } +} + +func (r *mongoLinkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.Split(req.ID, " ") + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("app_name"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), parts[1])...) +} diff --git a/internal/provider/services/mongo_resource.go b/internal/provider/services/mongo_resource.go new file mode 100644 index 0000000..54cb508 --- /dev/null +++ b/internal/provider/services/mongo_resource.go @@ -0,0 +1,170 @@ +package services + +import ( + "context" + "regexp" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &mongoResource{} + _ resource.ResourceWithConfigure = &mongoResource{} + _ resource.ResourceWithImportState = &mongoResource{} +) + +func NewMongoResource() resource.Resource { + return &mongoResource{} +} + +type mongoResource struct { + client *dokkuclient.Client +} + +type mongoResourceModel struct { + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *mongoResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mongo" +} + +// Configure adds the provider configured client to the resource. +func (r *mongoResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *mongoResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to create", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *mongoResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state mongoResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "mongo", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mongo service existence", "Unable to check mongo service existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *mongoResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan mongoResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Create service is not exists + exists, err := r.client.SimpleServiceExists(ctx, "mongo", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mongo service existence", "Unable to check mongo service existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "Mongo service already exists", "Mongo service already exists") + return + } + + err = r.client.SimpleServiceCreate(ctx, "mongo", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create mongo service", "Unable to create mongo service. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *mongoResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *mongoResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state mongoResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "mongo", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mongo service existence", "Unable to check mongo service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Destroy instance + err = r.client.SimpleServiceDestroy(ctx, "mongo", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to destroy service", "Unable to destroy service. "+err.Error()) + return + } +} + +func (r *mongoResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Retrieve import ID and save to service_name attribute + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), req.ID)...) +} diff --git a/internal/provider/services/mysql_link_resource.go b/internal/provider/services/mysql_link_resource.go new file mode 100644 index 0000000..f51f410 --- /dev/null +++ b/internal/provider/services/mysql_link_resource.go @@ -0,0 +1,216 @@ +package services + +import ( + "context" + "regexp" + "strings" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &mysqlLinkResource{} + _ resource.ResourceWithConfigure = &mysqlLinkResource{} + _ resource.ResourceWithImportState = &mysqlLinkResource{} +) + +func NewMysqlLinkResource() resource.Resource { + return &mysqlLinkResource{} +} + +type mysqlLinkResource struct { + client *dokkuclient.Client +} + +type mysqlLinkResourceModel struct { + AppName types.String `tfsdk:"app_name"` + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *mysqlLinkResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mysql_link" +} + +// Configure adds the provider configured client to the resource. +func (r *mysqlLinkResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *mysqlLinkResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "app_name": schema.StringAttribute{ + Required: true, + Description: "App name to apply link service to", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid app_name"), + }, + }, + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to link", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *mysqlLinkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state mysqlLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "mysql", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mysql service existence", "Unable to check mysql service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddError("Unable to find mysql service", "Unable to find mysql service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "mysql", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mysql link existence", "Unable to check mysql link existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *mysqlLinkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan mysqlLinkResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "mysql", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mysql service existence", "Unable to check mysql service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "Unable to find mysql service", "Unable to find mysql service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "mysql", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mysql link existence", "Unable to check mysql link existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddError("Service already linked to app", "Service already linked to app") + return + } + + // Create link + err = r.client.SimpleServiceLinkCreate(ctx, "mysql", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create mysql link", "Unable to create mysql link. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *mysqlLinkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *mysqlLinkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state mysqlLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "mysql", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mysql service existence", "Unable to check mysql service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "mysql", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mysql link existence", "Unable to check mysql link existence. "+err.Error()) + return + } + if !exists { + return + } + + // Unlink service + err = r.client.SimpleServiceLinkRemove(ctx, "mysql", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to unlink service from app", "Unable to unlink service from app. "+err.Error()) + return + } +} + +func (r *mysqlLinkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.Split(req.ID, " ") + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("app_name"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), parts[1])...) +} diff --git a/internal/provider/services/mysql_resource.go b/internal/provider/services/mysql_resource.go new file mode 100644 index 0000000..1604096 --- /dev/null +++ b/internal/provider/services/mysql_resource.go @@ -0,0 +1,170 @@ +package services + +import ( + "context" + "regexp" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &mysqlResource{} + _ resource.ResourceWithConfigure = &mysqlResource{} + _ resource.ResourceWithImportState = &mysqlResource{} +) + +func NewMysqlResource() resource.Resource { + return &mysqlResource{} +} + +type mysqlResource struct { + client *dokkuclient.Client +} + +type mysqlResourceModel struct { + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *mysqlResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mysql" +} + +// Configure adds the provider configured client to the resource. +func (r *mysqlResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *mysqlResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to create", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *mysqlResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state mysqlResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "mysql", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mysql service existence", "Unable to check mysql service existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *mysqlResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan mysqlResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Create service is not exists + exists, err := r.client.SimpleServiceExists(ctx, "mysql", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mysql service existence", "Unable to check mysql service existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "Mysql service already exists", "Mysql service already exists") + return + } + + err = r.client.SimpleServiceCreate(ctx, "mysql", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create mysql service", "Unable to create mysql service. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *mysqlResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *mysqlResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state mysqlResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "mysql", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check mysql service existence", "Unable to check mysql service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Destroy instance + err = r.client.SimpleServiceDestroy(ctx, "mysql", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to destroy service", "Unable to destroy service. "+err.Error()) + return + } +} + +func (r *mysqlResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Retrieve import ID and save to service_name attribute + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), req.ID)...) +} diff --git a/internal/provider/services/nats_link_resource.go b/internal/provider/services/nats_link_resource.go new file mode 100644 index 0000000..2be3840 --- /dev/null +++ b/internal/provider/services/nats_link_resource.go @@ -0,0 +1,216 @@ +package services + +import ( + "context" + "regexp" + "strings" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &natsLinkResource{} + _ resource.ResourceWithConfigure = &natsLinkResource{} + _ resource.ResourceWithImportState = &natsLinkResource{} +) + +func NewNatsLinkResource() resource.Resource { + return &natsLinkResource{} +} + +type natsLinkResource struct { + client *dokkuclient.Client +} + +type natsLinkResourceModel struct { + AppName types.String `tfsdk:"app_name"` + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *natsLinkResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nats_link" +} + +// Configure adds the provider configured client to the resource. +func (r *natsLinkResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *natsLinkResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "app_name": schema.StringAttribute{ + Required: true, + Description: "App name to apply link service to", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid app_name"), + }, + }, + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to link", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *natsLinkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state natsLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "nats", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check nats service existence", "Unable to check nats service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddError("Unable to find nats service", "Unable to find nats service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "nats", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check nats link existence", "Unable to check nats link existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *natsLinkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan natsLinkResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "nats", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check nats service existence", "Unable to check nats service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "Unable to find nats service", "Unable to find nats service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "nats", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check nats link existence", "Unable to check nats link existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddError("Service already linked to app", "Service already linked to app") + return + } + + // Create link + err = r.client.SimpleServiceLinkCreate(ctx, "nats", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create nats link", "Unable to create nats link. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *natsLinkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *natsLinkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state natsLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "nats", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check nats service existence", "Unable to check nats service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "nats", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check nats link existence", "Unable to check nats link existence. "+err.Error()) + return + } + if !exists { + return + } + + // Unlink service + err = r.client.SimpleServiceLinkRemove(ctx, "nats", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to unlink service from app", "Unable to unlink service from app. "+err.Error()) + return + } +} + +func (r *natsLinkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.Split(req.ID, " ") + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("app_name"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), parts[1])...) +} diff --git a/internal/provider/services/nats_resource.go b/internal/provider/services/nats_resource.go new file mode 100644 index 0000000..b428afb --- /dev/null +++ b/internal/provider/services/nats_resource.go @@ -0,0 +1,170 @@ +package services + +import ( + "context" + "regexp" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &natsResource{} + _ resource.ResourceWithConfigure = &natsResource{} + _ resource.ResourceWithImportState = &natsResource{} +) + +func NewNatsResource() resource.Resource { + return &natsResource{} +} + +type natsResource struct { + client *dokkuclient.Client +} + +type natsResourceModel struct { + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *natsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nats" +} + +// Configure adds the provider configured client to the resource. +func (r *natsResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *natsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to create", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *natsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state natsResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "nats", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check nats service existence", "Unable to check nats service existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *natsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan natsResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Create service is not exists + exists, err := r.client.SimpleServiceExists(ctx, "nats", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check nats service existence", "Unable to check nats service existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "Nats service already exists", "Nats service already exists") + return + } + + err = r.client.SimpleServiceCreate(ctx, "nats", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create nats service", "Unable to create nats service. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *natsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *natsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state natsResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "nats", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check nats service existence", "Unable to check nats service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Destroy instance + err = r.client.SimpleServiceDestroy(ctx, "nats", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to destroy service", "Unable to destroy service. "+err.Error()) + return + } +} + +func (r *natsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Retrieve import ID and save to service_name attribute + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), req.ID)...) +} diff --git a/internal/provider/postgres_link_resource.go b/internal/provider/services/postgres_link_resource.go similarity index 87% rename from internal/provider/postgres_link_resource.go rename to internal/provider/services/postgres_link_resource.go index 393a3e5..a3bc567 100644 --- a/internal/provider/postgres_link_resource.go +++ b/internal/provider/services/postgres_link_resource.go @@ -1,4 +1,4 @@ -package provider +package services import ( "context" @@ -90,7 +90,7 @@ func (r *postgresLinkResource) Read(ctx context.Context, req resource.ReadReques } // Check service existence - exists, err := r.client.PostgresServiceExists(ctx, state.ServiceName.ValueString()) + exists, err := r.client.SimpleServiceExists(ctx, "postgres", state.ServiceName.ValueString()) if err != nil { resp.Diagnostics.AddError("Unable to check postgres service existence", "Unable to check postgres service existence. "+err.Error()) return @@ -101,7 +101,7 @@ func (r *postgresLinkResource) Read(ctx context.Context, req resource.ReadReques } // Check link existence - exists, err = r.client.PostgresLinkExists(ctx, state.ServiceName.ValueString(), state.AppName.ValueString()) + exists, err = r.client.SimpleServiceLinkExists(ctx, "postgres", state.ServiceName.ValueString(), state.AppName.ValueString()) if err != nil { resp.Diagnostics.AddError("Unable to check postgres link existence", "Unable to check postgres link existence. "+err.Error()) return @@ -130,7 +130,7 @@ func (r *postgresLinkResource) Create(ctx context.Context, req resource.CreateRe } // Check service existence - exists, err := r.client.PostgresServiceExists(ctx, plan.ServiceName.ValueString()) + exists, err := r.client.SimpleServiceExists(ctx, "postgres", plan.ServiceName.ValueString()) if err != nil { resp.Diagnostics.AddError("Unable to check postgres service existence", "Unable to check postgres service existence. "+err.Error()) return @@ -141,7 +141,7 @@ func (r *postgresLinkResource) Create(ctx context.Context, req resource.CreateRe } // Check link existence - exists, err = r.client.PostgresLinkExists(ctx, plan.ServiceName.ValueString(), plan.AppName.ValueString()) + exists, err = r.client.SimpleServiceLinkExists(ctx, "postgres", plan.ServiceName.ValueString(), plan.AppName.ValueString()) if err != nil { resp.Diagnostics.AddError("Unable to check postgres link existence", "Unable to check postgres link existence. "+err.Error()) return @@ -152,7 +152,7 @@ func (r *postgresLinkResource) Create(ctx context.Context, req resource.CreateRe } // Create link - err = r.client.PostgresLinkCreate(ctx, plan.ServiceName.ValueString(), plan.AppName.ValueString()) + err = r.client.SimpleServiceLinkCreate(ctx, "postgres", plan.ServiceName.ValueString(), plan.AppName.ValueString()) if err != nil { resp.Diagnostics.AddError("Unable to create postgres link", "Unable to create postgres link. "+err.Error()) return @@ -182,7 +182,7 @@ func (r *postgresLinkResource) Delete(ctx context.Context, req resource.DeleteRe } // Check service existence - exists, err := r.client.PostgresServiceExists(ctx, state.ServiceName.ValueString()) + exists, err := r.client.SimpleServiceExists(ctx, "postgres", state.ServiceName.ValueString()) if err != nil { resp.Diagnostics.AddError("Unable to check postgres service existence", "Unable to check postgres service existence. "+err.Error()) return @@ -192,7 +192,7 @@ func (r *postgresLinkResource) Delete(ctx context.Context, req resource.DeleteRe } // Check link existence - exists, err = r.client.PostgresLinkExists(ctx, state.ServiceName.ValueString(), state.AppName.ValueString()) + exists, err = r.client.SimpleServiceLinkExists(ctx, "postgres", state.ServiceName.ValueString(), state.AppName.ValueString()) if err != nil { resp.Diagnostics.AddError("Unable to check postgres link existence", "Unable to check postgres link existence. "+err.Error()) return @@ -202,7 +202,7 @@ func (r *postgresLinkResource) Delete(ctx context.Context, req resource.DeleteRe } // Unlink service - err = r.client.PostgresLinkRemove(ctx, state.ServiceName.ValueString(), state.AppName.ValueString()) + err = r.client.SimpleServiceLinkRemove(ctx, "postgres", state.ServiceName.ValueString(), state.AppName.ValueString()) if err != nil { resp.Diagnostics.AddError("Unable to unlink service from app", "Unable to unlink service from app. "+err.Error()) return diff --git a/internal/provider/postgres_resource.go b/internal/provider/services/postgres_resource.go similarity index 92% rename from internal/provider/postgres_resource.go rename to internal/provider/services/postgres_resource.go index 5759df9..c3d7240 100644 --- a/internal/provider/postgres_resource.go +++ b/internal/provider/services/postgres_resource.go @@ -1,4 +1,4 @@ -package provider +package services import ( "context" @@ -78,7 +78,7 @@ func (r *postgresResource) Read(ctx context.Context, req resource.ReadRequest, r } // Check service existence - exists, err := r.client.PostgresServiceExists(ctx, state.ServiceName.ValueString()) + exists, err := r.client.SimpleServiceExists(ctx, "postgres", state.ServiceName.ValueString()) if err != nil { resp.Diagnostics.AddError("Unable to check postgres service existence", "Unable to check postgres service existence. "+err.Error()) return @@ -107,7 +107,7 @@ func (r *postgresResource) Create(ctx context.Context, req resource.CreateReques } // Create service is not exists - exists, err := r.client.PostgresServiceExists(ctx, plan.ServiceName.ValueString()) + exists, err := r.client.SimpleServiceExists(ctx, "postgres", plan.ServiceName.ValueString()) if err != nil { resp.Diagnostics.AddError("Unable to check postgres service existence", "Unable to check postgres service existence. "+err.Error()) return @@ -117,7 +117,7 @@ func (r *postgresResource) Create(ctx context.Context, req resource.CreateReques return } - err = r.client.PostgresServiceCreate(ctx, plan.ServiceName.ValueString()) + err = r.client.SimpleServiceCreate(ctx, "postgres", plan.ServiceName.ValueString()) if err != nil { resp.Diagnostics.AddError("Unable to create postgres service", "Unable to create postgres service. "+err.Error()) return @@ -147,7 +147,7 @@ func (r *postgresResource) Delete(ctx context.Context, req resource.DeleteReques } // Check service existence - exists, err := r.client.PostgresServiceExists(ctx, state.ServiceName.ValueString()) + exists, err := r.client.SimpleServiceExists(ctx, "postgres", state.ServiceName.ValueString()) if err != nil { resp.Diagnostics.AddError("Unable to check postgres service existence", "Unable to check postgres service existence. "+err.Error()) return @@ -157,7 +157,7 @@ func (r *postgresResource) Delete(ctx context.Context, req resource.DeleteReques } // Destroy instance - err = r.client.PostgresServiceDestroy(ctx, state.ServiceName.ValueString()) + err = r.client.SimpleServiceDestroy(ctx, "postgres", state.ServiceName.ValueString()) if err != nil { resp.Diagnostics.AddError("Unable to destroy service", "Unable to destroy service. "+err.Error()) return diff --git a/internal/provider/services/rabbitmq_link_resource.go b/internal/provider/services/rabbitmq_link_resource.go new file mode 100644 index 0000000..60ecba9 --- /dev/null +++ b/internal/provider/services/rabbitmq_link_resource.go @@ -0,0 +1,216 @@ +package services + +import ( + "context" + "regexp" + "strings" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &rabbitMQLinkResource{} + _ resource.ResourceWithConfigure = &rabbitMQLinkResource{} + _ resource.ResourceWithImportState = &rabbitMQLinkResource{} +) + +func NewRabbitMQLinkResource() resource.Resource { + return &rabbitMQLinkResource{} +} + +type rabbitMQLinkResource struct { + client *dokkuclient.Client +} + +type rabbitMQLinkResourceModel struct { + AppName types.String `tfsdk:"app_name"` + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *rabbitMQLinkResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_rabbitmq_link" +} + +// Configure adds the provider configured client to the resource. +func (r *rabbitMQLinkResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *rabbitMQLinkResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "app_name": schema.StringAttribute{ + Required: true, + Description: "App name to apply link service to", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid app_name"), + }, + }, + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to link", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *rabbitMQLinkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state rabbitMQLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "rabbitmq", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rabbitMQ service existence", "Unable to check rabbitMQ service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddError("Unable to find rabbitMQ service", "Unable to find rabbitMQ service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "rabbitmq", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rabbitMQ link existence", "Unable to check rabbitMQ link existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *rabbitMQLinkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan rabbitMQLinkResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "rabbitmq", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rabbitMQ service existence", "Unable to check rabbitMQ service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "Unable to find rabbitMQ service", "Unable to find rabbitMQ service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "rabbitmq", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rabbitMQ link existence", "Unable to check rabbitMQ link existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddError("Service already linked to app", "Service already linked to app") + return + } + + // Create link + err = r.client.SimpleServiceLinkCreate(ctx, "rabbitmq", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create rabbitMQ link", "Unable to create rabbitMQ link. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *rabbitMQLinkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *rabbitMQLinkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state rabbitMQLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "rabbitmq", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rabbitMQ service existence", "Unable to check rabbitMQ service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "rabbitmq", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rabbitMQ link existence", "Unable to check rabbitMQ link existence. "+err.Error()) + return + } + if !exists { + return + } + + // Unlink service + err = r.client.SimpleServiceLinkRemove(ctx, "rabbitmq", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to unlink service from app", "Unable to unlink service from app. "+err.Error()) + return + } +} + +func (r *rabbitMQLinkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.Split(req.ID, " ") + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("app_name"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), parts[1])...) +} diff --git a/internal/provider/services/rabbitmq_resource.go b/internal/provider/services/rabbitmq_resource.go new file mode 100644 index 0000000..dc303c7 --- /dev/null +++ b/internal/provider/services/rabbitmq_resource.go @@ -0,0 +1,170 @@ +package services + +import ( + "context" + "regexp" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &rabbitMQResource{} + _ resource.ResourceWithConfigure = &rabbitMQResource{} + _ resource.ResourceWithImportState = &rabbitMQResource{} +) + +func NewRabbitMQResource() resource.Resource { + return &rabbitMQResource{} +} + +type rabbitMQResource struct { + client *dokkuclient.Client +} + +type rabbitMQResourceModel struct { + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *rabbitMQResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_rabbitmq" +} + +// Configure adds the provider configured client to the resource. +func (r *rabbitMQResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *rabbitMQResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to create", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *rabbitMQResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state rabbitMQResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "rabbitmq", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rabbitMQ service existence", "Unable to check rabbitMQ service existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *rabbitMQResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan rabbitMQResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Create service is not exists + exists, err := r.client.SimpleServiceExists(ctx, "rabbitmq", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rabbitMQ service existence", "Unable to check rabbitMQ service existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "RabbitMQ service already exists", "RabbitMQ service already exists") + return + } + + err = r.client.SimpleServiceCreate(ctx, "rabbitmq", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create rabbitMQ service", "Unable to create rabbitMQ service. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *rabbitMQResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *rabbitMQResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state rabbitMQResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "rabbitmq", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rabbitMQ service existence", "Unable to check rabbitMQ service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Destroy instance + err = r.client.SimpleServiceDestroy(ctx, "rabbitmq", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to destroy service", "Unable to destroy service. "+err.Error()) + return + } +} + +func (r *rabbitMQResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Retrieve import ID and save to service_name attribute + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), req.ID)...) +} diff --git a/internal/provider/services/redis_link_resource.go b/internal/provider/services/redis_link_resource.go new file mode 100644 index 0000000..606976d --- /dev/null +++ b/internal/provider/services/redis_link_resource.go @@ -0,0 +1,216 @@ +package services + +import ( + "context" + "regexp" + "strings" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &redisLinkResource{} + _ resource.ResourceWithConfigure = &redisLinkResource{} + _ resource.ResourceWithImportState = &redisLinkResource{} +) + +func NewRedisLinkResource() resource.Resource { + return &redisLinkResource{} +} + +type redisLinkResource struct { + client *dokkuclient.Client +} + +type redisLinkResourceModel struct { + AppName types.String `tfsdk:"app_name"` + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *redisLinkResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_redis_link" +} + +// Configure adds the provider configured client to the resource. +func (r *redisLinkResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *redisLinkResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "app_name": schema.StringAttribute{ + Required: true, + Description: "App name to apply link service to", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid app_name"), + }, + }, + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to link", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *redisLinkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state redisLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "redis", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check redis service existence", "Unable to check redis service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddError("Unable to find redis service", "Unable to find redis service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "redis", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check redis link existence", "Unable to check redis link existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *redisLinkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan redisLinkResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "redis", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check redis service existence", "Unable to check redis service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "Unable to find redis service", "Unable to find redis service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "redis", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check redis link existence", "Unable to check redis link existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddError("Service already linked to app", "Service already linked to app") + return + } + + // Create link + err = r.client.SimpleServiceLinkCreate(ctx, "redis", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create redis link", "Unable to create redis link. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *redisLinkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *redisLinkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state redisLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "redis", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check redis service existence", "Unable to check redis service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "redis", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check redis link existence", "Unable to check redis link existence. "+err.Error()) + return + } + if !exists { + return + } + + // Unlink service + err = r.client.SimpleServiceLinkRemove(ctx, "redis", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to unlink service from app", "Unable to unlink service from app. "+err.Error()) + return + } +} + +func (r *redisLinkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.Split(req.ID, " ") + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("app_name"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), parts[1])...) +} diff --git a/internal/provider/services/redis_resource.go b/internal/provider/services/redis_resource.go new file mode 100644 index 0000000..8371f25 --- /dev/null +++ b/internal/provider/services/redis_resource.go @@ -0,0 +1,170 @@ +package services + +import ( + "context" + "regexp" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &redisResource{} + _ resource.ResourceWithConfigure = &redisResource{} + _ resource.ResourceWithImportState = &redisResource{} +) + +func NewRedisResource() resource.Resource { + return &redisResource{} +} + +type redisResource struct { + client *dokkuclient.Client +} + +type redisResourceModel struct { + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *redisResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_redis" +} + +// Configure adds the provider configured client to the resource. +func (r *redisResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *redisResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to create", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *redisResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state redisResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "redis", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check redis service existence", "Unable to check redis service existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *redisResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan redisResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Create service is not exists + exists, err := r.client.SimpleServiceExists(ctx, "redis", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check redis service existence", "Unable to check redis service existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "Redis service already exists", "Redis service already exists") + return + } + + err = r.client.SimpleServiceCreate(ctx, "redis", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create redis service", "Unable to create redis service. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *redisResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *redisResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state redisResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "redis", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check redis service existence", "Unable to check redis service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Destroy instance + err = r.client.SimpleServiceDestroy(ctx, "redis", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to destroy service", "Unable to destroy service. "+err.Error()) + return + } +} + +func (r *redisResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Retrieve import ID and save to service_name attribute + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), req.ID)...) +} diff --git a/internal/provider/services/rethinkdb_link_resource.go b/internal/provider/services/rethinkdb_link_resource.go new file mode 100644 index 0000000..cbdb73a --- /dev/null +++ b/internal/provider/services/rethinkdb_link_resource.go @@ -0,0 +1,216 @@ +package services + +import ( + "context" + "regexp" + "strings" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &rethinkDBLinkResource{} + _ resource.ResourceWithConfigure = &rethinkDBLinkResource{} + _ resource.ResourceWithImportState = &rethinkDBLinkResource{} +) + +func NewRethinkDBLinkResource() resource.Resource { + return &rethinkDBLinkResource{} +} + +type rethinkDBLinkResource struct { + client *dokkuclient.Client +} + +type rethinkDBLinkResourceModel struct { + AppName types.String `tfsdk:"app_name"` + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *rethinkDBLinkResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_rethinkdb_link" +} + +// Configure adds the provider configured client to the resource. +func (r *rethinkDBLinkResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *rethinkDBLinkResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "app_name": schema.StringAttribute{ + Required: true, + Description: "App name to apply link service to", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid app_name"), + }, + }, + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to link", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *rethinkDBLinkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state rethinkDBLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "rethinkdb", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rethinkDB service existence", "Unable to check rethinkDB service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddError("Unable to find rethinkDB service", "Unable to find rethinkDB service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "rethinkdb", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rethinkDB link existence", "Unable to check rethinkDB link existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *rethinkDBLinkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan rethinkDBLinkResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "rethinkdb", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rethinkDB service existence", "Unable to check rethinkDB service existence. "+err.Error()) + return + } + if !exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "Unable to find rethinkDB service", "Unable to find rethinkDB service") + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "rethinkdb", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rethinkDB link existence", "Unable to check rethinkDB link existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddError("Service already linked to app", "Service already linked to app") + return + } + + // Create link + err = r.client.SimpleServiceLinkCreate(ctx, "rethinkdb", plan.ServiceName.ValueString(), plan.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create rethinkDB link", "Unable to create rethinkDB link. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *rethinkDBLinkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *rethinkDBLinkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state rethinkDBLinkResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "rethinkdb", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rethinkDB service existence", "Unable to check rethinkDB service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Check link existence + exists, err = r.client.SimpleServiceLinkExists(ctx, "rethinkdb", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rethinkDB link existence", "Unable to check rethinkDB link existence. "+err.Error()) + return + } + if !exists { + return + } + + // Unlink service + err = r.client.SimpleServiceLinkRemove(ctx, "rethinkdb", state.ServiceName.ValueString(), state.AppName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to unlink service from app", "Unable to unlink service from app. "+err.Error()) + return + } +} + +func (r *rethinkDBLinkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.Split(req.ID, " ") + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("app_name"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), parts[1])...) +} diff --git a/internal/provider/services/rethinkdb_resource.go b/internal/provider/services/rethinkdb_resource.go new file mode 100644 index 0000000..b223a79 --- /dev/null +++ b/internal/provider/services/rethinkdb_resource.go @@ -0,0 +1,170 @@ +package services + +import ( + "context" + "regexp" + + dokkuclient "terraform-provider-dokku/internal/provider/dokku_client" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &rethinkDBResource{} + _ resource.ResourceWithConfigure = &rethinkDBResource{} + _ resource.ResourceWithImportState = &rethinkDBResource{} +) + +func NewRethinkDBResource() resource.Resource { + return &rethinkDBResource{} +} + +type rethinkDBResource struct { + client *dokkuclient.Client +} + +type rethinkDBResourceModel struct { + ServiceName types.String `tfsdk:"service_name"` +} + +// Metadata returns the resource type name. +func (r *rethinkDBResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_rethinkdb" +} + +// Configure adds the provider configured client to the resource. +func (r *rethinkDBResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + //nolint:forcetypeassert + r.client = req.ProviderData.(*dokkuclient.Client) +} + +// Schema defines the schema for the resource. +func (r *rethinkDBResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "service_name": schema.StringAttribute{ + Required: true, + Description: "Service name to create", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]*$`), "invalid service_name"), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *rethinkDBResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state rethinkDBResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "rethinkdb", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rethinkDB service existence", "Unable to check rethinkDB service existence. "+err.Error()) + return + } + if !exists { + resp.State.RemoveResource(ctx) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *rethinkDBResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan rethinkDBResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Create service is not exists + exists, err := r.client.SimpleServiceExists(ctx, "rethinkdb", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rethinkDB service existence", "Unable to check rethinkDB service existence. "+err.Error()) + return + } + if exists { + resp.Diagnostics.AddAttributeError(path.Root("service_name"), "RethinkDB service already exists", "RethinkDB service already exists") + return + } + + err = r.client.SimpleServiceCreate(ctx, "rethinkdb", plan.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to create rethinkDB service", "Unable to create rethinkDB service. "+err.Error()) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *rethinkDBResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Resource doesn't support Update", "Resource doesn't support Update") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *rethinkDBResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state rethinkDBResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Check service existence + exists, err := r.client.SimpleServiceExists(ctx, "rethinkdb", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to check rethinkDB service existence", "Unable to check rethinkDB service existence. "+err.Error()) + return + } + if !exists { + return + } + + // Destroy instance + err = r.client.SimpleServiceDestroy(ctx, "rethinkdb", state.ServiceName.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to destroy service", "Unable to destroy service. "+err.Error()) + return + } +} + +func (r *rethinkDBResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Retrieve import ID and save to service_name attribute + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_name"), req.ID)...) +}