* add deprecation message * edit acceptance test * Enable project before creating first cluster // fix typo in function name * add cluster existence check before deleting ske project // add deprecation message to project resource * add region to acceptance test * Update resource error message Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> * Update resource error message Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> * Update deprecation message Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> * Update error message Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> * remove model id assignment * update error handling for cluster existence check while deleting project * add nil check for clusters during project deletion * add deprecation notice to documentation * Update datasource deprecation message Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> * improve condition handling --------- Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com>
224 lines
8.9 KiB
Go
224 lines
8.9 KiB
Go
package ske
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"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"
|
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
|
"github.com/stackitcloud/stackit-sdk-go/core/config"
|
|
"github.com/stackitcloud/stackit-sdk-go/services/ske"
|
|
"github.com/stackitcloud/stackit-sdk-go/services/ske/wait"
|
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
|
)
|
|
|
|
// Ensure the implementation satisfies the expected interfaces.
|
|
var (
|
|
_ resource.Resource = &projectResource{}
|
|
_ resource.ResourceWithConfigure = &projectResource{}
|
|
_ resource.ResourceWithImportState = &projectResource{}
|
|
)
|
|
|
|
type Model struct {
|
|
Id types.String `tfsdk:"id"`
|
|
ProjectId types.String `tfsdk:"project_id"`
|
|
}
|
|
|
|
// NewProjectResource is a helper function to simplify the provider implementation.
|
|
func NewProjectResource() resource.Resource {
|
|
return &projectResource{}
|
|
}
|
|
|
|
// projectResource is the resource implementation.
|
|
type projectResource struct {
|
|
client *ske.APIClient
|
|
}
|
|
|
|
// Metadata returns the resource type name.
|
|
func (r *projectResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
|
resp.TypeName = req.ProviderTypeName + "_ske_project"
|
|
}
|
|
|
|
// Configure adds the provider configured client to the resource.
|
|
func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
|
// Prevent panic if the provider has not been configured.
|
|
if req.ProviderData == nil {
|
|
return
|
|
}
|
|
|
|
providerData, ok := req.ProviderData.(core.ProviderData)
|
|
if !ok {
|
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
|
return
|
|
}
|
|
|
|
var apiClient *ske.APIClient
|
|
var err error
|
|
if providerData.SKECustomEndpoint != "" {
|
|
apiClient, err = ske.NewAPIClient(
|
|
config.WithCustomAuth(providerData.RoundTripper),
|
|
config.WithEndpoint(providerData.SKECustomEndpoint),
|
|
)
|
|
} else {
|
|
apiClient, err = ske.NewAPIClient(
|
|
config.WithCustomAuth(providerData.RoundTripper),
|
|
config.WithRegion(providerData.Region),
|
|
)
|
|
}
|
|
|
|
if err != nil {
|
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err))
|
|
return
|
|
}
|
|
|
|
r.client = apiClient
|
|
tflog.Info(ctx, "SKE project client configured")
|
|
}
|
|
|
|
// Schema returns the Terraform schema structure
|
|
func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
|
resp.Schema = schema.Schema{
|
|
Description: "SKE project resource schema. Must have a `region` specified in the provider configuration. This resource allows you to enable the SKE service and you can only have one per project. Before deleting this resource, all SKE clusters associated to the project must be deleted. Warning: SKE project resource is no longer in use and will be removed with the next minor release. SKE service enablement is done automatically when a new cluster is created.",
|
|
DeprecationMessage: "SKE project resource is no longer in use and will be removed with the next minor release. SKE service enablement is done automatically when a new cluster is created.",
|
|
Attributes: map[string]schema.Attribute{
|
|
"id": schema.StringAttribute{
|
|
Description: "Terraform's internal resource ID. It is structured as \"`project_id`\".",
|
|
Computed: true,
|
|
PlanModifiers: []planmodifier.String{
|
|
stringplanmodifier.UseStateForUnknown(),
|
|
},
|
|
},
|
|
"project_id": schema.StringAttribute{
|
|
Description: "STACKIT Project ID in which the kubernetes project is enabled.",
|
|
Required: true,
|
|
Validators: []validator.String{
|
|
validate.UUID(),
|
|
validate.NoSeparator(),
|
|
},
|
|
PlanModifiers: []planmodifier.String{
|
|
stringplanmodifier.RequiresReplace(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Create creates the resource and sets the initial Terraform state.
|
|
func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
|
|
var model Model
|
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
projectId := model.ProjectId.ValueString()
|
|
_, err := r.client.EnableService(ctx, projectId).Execute()
|
|
if err != nil {
|
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", fmt.Sprintf("Calling API: %v", err))
|
|
return
|
|
}
|
|
|
|
model.Id = types.StringValue(projectId)
|
|
_, err = wait.EnableServiceWaitHandler(ctx, r.client, projectId).WaitWithContext(ctx)
|
|
if err != nil {
|
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating cluster", fmt.Sprintf("Project creation waiting: %v", err))
|
|
return
|
|
}
|
|
|
|
diags := resp.State.Set(ctx, model)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
tflog.Info(ctx, "SKE project created")
|
|
}
|
|
|
|
// Read refreshes the Terraform state with the latest data.
|
|
func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
|
|
var model Model
|
|
diags := req.State.Get(ctx, &model)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
projectId := model.ProjectId.ValueString()
|
|
_, err := r.client.GetServiceStatus(ctx, projectId).Execute()
|
|
if err != nil {
|
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Calling API: %v", err))
|
|
return
|
|
}
|
|
model.Id = types.StringValue(projectId)
|
|
model.ProjectId = types.StringValue(projectId)
|
|
diags = resp.State.Set(ctx, model)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
tflog.Info(ctx, "SKE project read")
|
|
}
|
|
|
|
// Update updates the resource and sets the updated Terraform state on success.
|
|
func (r *projectResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
|
|
// Update shouldn't be called
|
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", "Project can't be updated")
|
|
}
|
|
|
|
// Delete deletes the resource and removes the Terraform state on success.
|
|
func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
|
|
core.LogAndAddWarning(ctx, &resp.Diagnostics, "Deleting project", "Deleting this resource will destroy any existing clusters under the project")
|
|
|
|
var model Model
|
|
resp.Diagnostics.Append(req.State.Get(ctx, &model)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
projectId := model.ProjectId.ValueString()
|
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
|
|
c := r.client
|
|
|
|
clusters, err := c.ListClusters(ctx, projectId).Execute()
|
|
if err != nil {
|
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting project", fmt.Sprintf("Calling API to get the list of clusters: %v", err))
|
|
return
|
|
}
|
|
|
|
if clusters != nil && len(*clusters.Items) > 0 {
|
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting project", fmt.Sprintln("You still have clusters in the project. Please delete them before deleting the project."))
|
|
return
|
|
}
|
|
|
|
_, err = c.DisableService(ctx, projectId).Execute()
|
|
if err != nil {
|
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting project", fmt.Sprintf("Calling API: %v", err))
|
|
return
|
|
}
|
|
_, err = wait.DisableServiceWaitHandler(ctx, r.client, projectId).WaitWithContext(ctx)
|
|
if err != nil {
|
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting project", fmt.Sprintf("Project deletion waiting: %v", err))
|
|
return
|
|
}
|
|
tflog.Info(ctx, "SKE project deleted")
|
|
}
|
|
|
|
// ImportState imports a resource into the Terraform state on success.
|
|
// The expected format of the resource import identifier is: project_id
|
|
func (r *projectResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { // nolint:gocritic // function signature required by Terraform
|
|
idParts := strings.Split(req.ID, core.Separator)
|
|
if len(idParts) != 1 || idParts[0] == "" {
|
|
core.LogAndAddError(ctx, &resp.Diagnostics,
|
|
"Error importing project",
|
|
fmt.Sprintf("Expected import identifier with format: [project_id] Got: %q", req.ID),
|
|
)
|
|
return
|
|
}
|
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
|
|
tflog.Info(ctx, "SKE project state imported")
|
|
}
|