terraform-provider-stackitp.../stackit/internal/services/ske/cluster/resource.go

1486 lines
53 KiB
Go

package ske
import (
"context"
"fmt"
"net/http"
"regexp"
"strings"
"time"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"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/int64default"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"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-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"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/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
"golang.org/x/mod/semver"
)
const (
DefaultOSName = "flatcar"
DefaultCRI = "containerd"
DefaultVolumeType = "storage_premium_perf1"
DefaultVolumeSizeGB int64 = 20
VersionStateSupported = "supported"
VersionStatePreview = "preview"
VersionStateDeprecated = "deprecated"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &clusterResource{}
_ resource.ResourceWithConfigure = &clusterResource{}
_ resource.ResourceWithImportState = &clusterResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
Name types.String `tfsdk:"name"`
KubernetesVersion types.String `tfsdk:"kubernetes_version"`
KubernetesVersionUsed types.String `tfsdk:"kubernetes_version_used"`
AllowPrivilegedContainers types.Bool `tfsdk:"allow_privileged_containers"`
NodePools types.List `tfsdk:"node_pools"`
Maintenance types.Object `tfsdk:"maintenance"`
Hibernations types.List `tfsdk:"hibernations"`
Extensions types.Object `tfsdk:"extensions"`
KubeConfig types.String `tfsdk:"kube_config"`
}
// Struct corresponding to Model.NodePools[i]
type nodePool struct {
Name types.String `tfsdk:"name"`
MachineType types.String `tfsdk:"machine_type"`
OSName types.String `tfsdk:"os_name"`
OSVersion types.String `tfsdk:"os_version"`
Minimum types.Int64 `tfsdk:"minimum"`
Maximum types.Int64 `tfsdk:"maximum"`
MaxSurge types.Int64 `tfsdk:"max_surge"`
MaxUnavailable types.Int64 `tfsdk:"max_unavailable"`
VolumeType types.String `tfsdk:"volume_type"`
VolumeSize types.Int64 `tfsdk:"volume_size"`
Labels types.Map `tfsdk:"labels"`
Taints types.List `tfsdk:"taints"`
CRI types.String `tfsdk:"cri"`
AvailabilityZones types.List `tfsdk:"availability_zones"`
}
// Types corresponding to nodePool
var nodePoolTypes = map[string]attr.Type{
"name": basetypes.StringType{},
"machine_type": basetypes.StringType{},
"os_name": basetypes.StringType{},
"os_version": basetypes.StringType{},
"minimum": basetypes.Int64Type{},
"maximum": basetypes.Int64Type{},
"max_surge": basetypes.Int64Type{},
"max_unavailable": basetypes.Int64Type{},
"volume_type": basetypes.StringType{},
"volume_size": basetypes.Int64Type{},
"labels": basetypes.MapType{ElemType: types.StringType},
"taints": basetypes.ListType{ElemType: types.ObjectType{AttrTypes: taintTypes}},
"cri": basetypes.StringType{},
"availability_zones": basetypes.ListType{ElemType: types.StringType},
}
// Struct corresponding to nodePool.Taints[i]
type taint struct {
Effect types.String `tfsdk:"effect"`
Key types.String `tfsdk:"key"`
Value types.String `tfsdk:"value"`
}
// Types corresponding to taint
var taintTypes = map[string]attr.Type{
"effect": basetypes.StringType{},
"key": basetypes.StringType{},
"value": basetypes.StringType{},
}
// Struct corresponding to Model.maintenance
type maintenance struct {
EnableKubernetesVersionUpdates types.Bool `tfsdk:"enable_kubernetes_version_updates"`
EnableMachineImageVersionUpdates types.Bool `tfsdk:"enable_machine_image_version_updates"`
Start types.String `tfsdk:"start"`
End types.String `tfsdk:"end"`
}
// Types corresponding to maintenance
var maintenanceTypes = map[string]attr.Type{
"enable_kubernetes_version_updates": basetypes.BoolType{},
"enable_machine_image_version_updates": basetypes.BoolType{},
"start": basetypes.StringType{},
"end": basetypes.StringType{},
}
// Struct corresponding to Model.Hibernations[i]
type hibernation struct {
Start types.String `tfsdk:"start"`
End types.String `tfsdk:"end"`
Timezone types.String `tfsdk:"timezone"`
}
// Types corresponding to hibernation
var hibernationTypes = map[string]attr.Type{
"start": basetypes.StringType{},
"end": basetypes.StringType{},
"timezone": basetypes.StringType{},
}
// Struct corresponding to Model.Extensions
type extensions struct {
Argus types.Object `tfsdk:"argus"`
ACL types.Object `tfsdk:"acl"`
}
// Types corresponding to extensions
var extensionsTypes = map[string]attr.Type{
"argus": basetypes.ObjectType{AttrTypes: argusTypes},
"acl": basetypes.ObjectType{AttrTypes: aclTypes},
}
// Struct corresponding to extensions.ACL
type acl struct {
Enabled types.Bool `tfsdk:"enabled"`
AllowedCIDRs types.List `tfsdk:"allowed_cidrs"`
}
// Types corresponding to acl
var aclTypes = map[string]attr.Type{
"enabled": basetypes.BoolType{},
"allowed_cidrs": basetypes.ListType{ElemType: types.StringType},
}
// Struct corresponding to extensions.Argus
type argus struct {
Enabled types.Bool `tfsdk:"enabled"`
ArgusInstanceId types.String `tfsdk:"argus_instance_id"`
}
// Types corresponding to argus
var argusTypes = map[string]attr.Type{
"enabled": basetypes.BoolType{},
"argus_instance_id": basetypes.StringType{},
}
// NewClusterResource is a helper function to simplify the provider implementation.
func NewClusterResource() resource.Resource {
return &clusterResource{}
}
// clusterResource is the resource implementation.
type clusterResource struct {
client *ske.APIClient
}
// Metadata returns the resource type name.
func (r *clusterResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_ske_cluster"
}
// Configure adds the provider configured client to the resource.
func (r *clusterResource) 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 cluster client configured")
}
// Schema defines the schema for the resource.
func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "SKE Cluster Resource schema. Must have a `region` specified in the provider configuration.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`name`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"project_id": schema.StringAttribute{
Description: "STACKIT project ID to which the cluster is associated.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"name": schema.StringAttribute{
Description: "The cluster name.",
Required: true,
Validators: []validator.String{
validate.NoSeparator(),
},
},
"kubernetes_version": schema.StringAttribute{
Description: "Kubernetes version. Must only contain major and minor version (e.g. 1.22)",
Required: true,
Validators: []validator.String{
validate.MinorVersionNumber(),
},
},
"kubernetes_version_used": schema.StringAttribute{
Description: "Full Kubernetes version used. For example, if 1.22 was selected, this value may result to 1.22.15",
Computed: true,
},
"allow_privileged_containers": schema.BoolAttribute{
Description: "Flag to specify if privileged mode for containers is enabled or not.\nThis should be used with care since it also disables a couple of other features like the use of some volume type (e.g. PVCs).\nDeprecated as of Kubernetes 1.25 and later",
Optional: true,
},
"node_pools": schema.ListNestedAttribute{
Description: "One or more `node_pool` block as defined below.",
Required: true,
Validators: []validator.List{
listvalidator.SizeAtLeast(1),
listvalidator.SizeAtMost(10),
},
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "Specifies the name of the node pool.",
Required: true,
},
"machine_type": schema.StringAttribute{
Description: "The machine type.",
Required: true,
},
"availability_zones": schema.ListAttribute{
Description: "Specify a list of availability zones. E.g. `eu01-m`",
Required: true,
ElementType: types.StringType,
},
"minimum": schema.Int64Attribute{
Description: "Minimum number of nodes in the pool.",
Required: true,
Validators: []validator.Int64{
int64validator.AtLeast(1),
int64validator.AtMost(100),
},
},
"maximum": schema.Int64Attribute{
Description: "Maximum number of nodes in the pool.",
Required: true,
Validators: []validator.Int64{
int64validator.AtLeast(1),
int64validator.AtMost(100),
},
},
"max_surge": schema.Int64Attribute{
Description: "Maximum number of additional VMs that are created during an update.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
Validators: []validator.Int64{
int64validator.AtLeast(1),
int64validator.AtMost(10),
},
},
"max_unavailable": schema.Int64Attribute{
Description: "Maximum number of VMs that that can be unavailable during an update.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
},
"os_name": schema.StringAttribute{
Description: "The name of the OS image. E.g. `flatcar`.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString(DefaultOSName),
},
"os_version": schema.StringAttribute{
Description: "The OS image version.",
Required: true,
},
"volume_type": schema.StringAttribute{
Description: "Specifies the volume type. E.g. `storage_premium_perf1`.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString(DefaultVolumeType),
},
"volume_size": schema.Int64Attribute{
Description: "The volume size in GB. E.g. `20`",
Optional: true,
Computed: true,
Default: int64default.StaticInt64(DefaultVolumeSizeGB),
},
"labels": schema.MapAttribute{
Description: "Labels to add to each node.",
Optional: true,
Computed: true,
ElementType: types.StringType,
PlanModifiers: []planmodifier.Map{
mapplanmodifier.UseStateForUnknown(),
},
},
"taints": schema.ListNestedAttribute{
Description: "Specifies a taint list as defined below.",
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"effect": schema.StringAttribute{
Description: "The taint effect. E.g `PreferNoSchedule`.",
Required: true,
},
"key": schema.StringAttribute{
Description: "Taint key to be applied to a node.",
Required: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"value": schema.StringAttribute{
Description: "Taint value corresponding to the taint key.",
Optional: true,
},
},
},
},
"cri": schema.StringAttribute{
Description: "Specifies the container runtime. E.g. `containerd`",
Optional: true,
Computed: true,
Default: stringdefault.StaticString(DefaultCRI),
},
},
},
},
"maintenance": schema.SingleNestedAttribute{
Description: "A single maintenance block as defined below.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Object{
objectplanmodifier.UseStateForUnknown(),
},
Attributes: map[string]schema.Attribute{
"enable_kubernetes_version_updates": schema.BoolAttribute{
Description: "Flag to enable/disable auto-updates of the Kubernetes version.",
Required: true,
},
"enable_machine_image_version_updates": schema.BoolAttribute{
Description: "Flag to enable/disable auto-updates of the OS image version.",
Required: true,
},
"start": schema.StringAttribute{
Description: "Time for maintenance window start. E.g. `01:23:45Z`, `05:00:00+02:00`.",
Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile(`^(((\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$`),
"must be a full-time as defined by RFC3339, Section 5.6. E.g. `01:23:45Z`, `05:00:00+02:00`",
),
},
},
"end": schema.StringAttribute{
Description: "Time for maintenance window end. E.g. `01:23:45Z`, `05:00:00+02:00`.",
Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile(`^(((\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$`),
"must be a full-time as defined by RFC3339, Section 5.6. E.g. `01:23:45Z`, `05:00:00+02:00`",
),
},
},
},
},
"hibernations": schema.ListNestedAttribute{
Description: "One or more hibernation block as defined below.",
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"start": schema.StringAttribute{
Description: "Start time of cluster hibernation in crontab syntax. E.g. `0 18 * * *` for starting everyday at 6pm.",
Required: true,
},
"end": schema.StringAttribute{
Description: "End time of hibernation in crontab syntax. E.g. `0 8 * * *` for waking up the cluster at 8am.",
Required: true,
},
"timezone": schema.StringAttribute{
Description: "Timezone name corresponding to a file in the IANA Time Zone database. i.e. `Europe/Berlin`.",
Optional: true,
},
},
},
},
"extensions": schema.SingleNestedAttribute{
Description: "A single extensions block as defined below.",
Optional: true,
PlanModifiers: []planmodifier.Object{
objectplanmodifier.UseStateForUnknown(),
},
Attributes: map[string]schema.Attribute{
"argus": schema.SingleNestedAttribute{
Description: "A single argus block as defined below.",
Optional: true,
Attributes: map[string]schema.Attribute{
"enabled": schema.BoolAttribute{
Description: "Flag to enable/disable Argus extensions.",
Required: true,
},
"argus_instance_id": schema.StringAttribute{
Description: "Argus instance ID to choose which Argus instance is used. Required when enabled is set to `true`.",
Optional: true,
},
},
},
"acl": schema.SingleNestedAttribute{
Description: "Cluster access control configuration.",
Optional: true,
Attributes: map[string]schema.Attribute{
"enabled": schema.BoolAttribute{
Description: "Is ACL enabled?",
Required: true,
},
"allowed_cidrs": schema.ListAttribute{
Description: "Specify a list of CIDRs to whitelist.",
Optional: true,
ElementType: types.StringType,
},
},
},
},
},
"kube_config": schema.StringAttribute{
Description: "Static token kubeconfig used for connecting to the cluster. This field will be empty for clusters with Kubernetes v1.27+, or if you have obtained the kubeconfig or performed credentials rotation using the new process, either through the Portal or the SKE API. Use the stackit_ske_kubeconfig resource instead. For more information, see How to rotate SKE credentials (https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html).",
Sensitive: true,
Computed: true,
DeprecationMessage: "This field will be empty for clusters with Kubernetes v1.27+, or if you have obtained the kubeconfig or performed credentials rotation using the new process, either through the Portal or the SKE API. Use the stackit_ske_kubeconfig resource instead. For more information, see How to rotate SKE credentials (https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html).",
},
},
}
}
// needs to be executed inside the Create and Update methods
// since ValidateConfig runs before variables are rendered to their value,
// which causes errors like this: https://github.com/stackitcloud/terraform-provider-stackit/issues/201
func checkAllowPrivilegedContainers(allowPrivilegeContainers types.Bool, kubernetesVersion types.String) diag.Diagnostics {
var diags diag.Diagnostics
if kubernetesVersion.IsNull() {
diags.AddError("'Kubernetes version' missing", "This field is required")
return diags
}
comparison := semver.Compare(fmt.Sprintf("v%s", kubernetesVersion.ValueString()), "v1.25")
if comparison < 0 {
if allowPrivilegeContainers.IsNull() {
diags.AddError("'Allow privilege containers' missing", "This field is required for Kubernetes prior to 1.25")
}
} else {
if !allowPrivilegeContainers.IsNull() {
diags.AddError("'Allow privilege containers' deprecated", "This field is deprecated as of Kubernetes 1.25 and later. Please remove this field")
}
}
return diags
}
// Create creates the resource and sets the initial Terraform state.
func (r *clusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
diags = checkAllowPrivilegedContainers(model.AllowPrivilegedContainers, model.KubernetesVersion)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
clusterName := model.Name.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "name", clusterName)
availableVersions, err := r.loadAvaiableVersions(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating cluster", fmt.Sprintf("Loading available Kubernetes versions: %v", err))
return
}
r.createOrUpdateCluster(ctx, &resp.Diagnostics, &model, availableVersions)
if resp.Diagnostics.HasError() {
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "SKE cluster created")
}
func (r *clusterResource) loadAvaiableVersions(ctx context.Context) ([]ske.KubernetesVersion, error) {
c := r.client
res, err := c.ListProviderOptions(ctx).Execute()
if err != nil {
return nil, fmt.Errorf("calling API: %w", err)
}
if res.KubernetesVersions == nil {
return nil, fmt.Errorf("API response has nil kubernetesVersions")
}
return *res.KubernetesVersions, nil
}
func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag.Diagnostics, model *Model, availableVersions []ske.KubernetesVersion) {
// cluster vars
projectId := model.ProjectId.ValueString()
name := model.Name.ValueString()
kubernetes, hasDeprecatedVersion, err := toKubernetesPayload(model, availableVersions)
if err != nil {
core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating cluster config API payload: %v", err))
return
}
if hasDeprecatedVersion {
diags.AddWarning("Deprecated Kubernetes version", fmt.Sprintf("Version %s of Kubernetes is deprecated, please update it", *kubernetes.Version))
}
nodePools, err := toNodepoolsPayload(ctx, model)
if err != nil {
core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating node pools API payload: %v", err))
return
}
maintenance, err := toMaintenancePayload(ctx, model)
if err != nil {
core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating maintenance API payload: %v", err))
return
}
hibernations, err := toHibernationsPayload(ctx, model)
if err != nil {
core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating hibernations API payload: %v", err))
return
}
extensions, err := toExtensionsPayload(ctx, model)
if err != nil {
core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating extension API payload: %v", err))
return
}
payload := ske.CreateOrUpdateClusterPayload{
Extensions: extensions,
Hibernation: hibernations,
Kubernetes: kubernetes,
Maintenance: maintenance,
Nodepools: &nodePools,
}
_, err = r.client.CreateOrUpdateCluster(ctx, projectId, name).CreateOrUpdateClusterPayload(payload).Execute()
if err != nil {
core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Calling API: %v", err))
return
}
waitResp, err := wait.CreateOrUpdateClusterWaitHandler(ctx, r.client, projectId, name).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Cluster creation waiting: %v", err))
return
}
if waitResp.Status.Error != nil && waitResp.Status.Error.Message != nil && *waitResp.Status.Error.Code == wait.InvalidArgusInstanceErrorCode {
core.LogAndAddWarning(ctx, diags, "Warning during creating/updating cluster", fmt.Sprintf("Cluster is in Impaired state due to an invalid argus instance id, the cluster is usable but metrics won't be forwarded: %s", *waitResp.Status.Error.Message))
}
err = mapFields(ctx, waitResp, model)
if err != nil {
core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Handle credential
err = r.getCredential(ctx, diags, model)
if err != nil {
core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Getting credential: %v", err))
return
}
}
func (r *clusterResource) getCredential(ctx context.Context, diags *diag.Diagnostics, model *Model) error {
c := r.client
// for kubernetes with version >= 1.27, the deprecated endpoint will not work, so we set kubeconfig to nil
if semver.Compare(fmt.Sprintf("v%s", model.KubernetesVersion.ValueString()), "v1.27") >= 0 {
core.LogAndAddWarning(ctx, diags, "The kubelogin field is set to null", "Kubernetes version is 1.27 or higher, you must use the stackit_ske_kubeconfig resource instead.")
model.KubeConfig = types.StringPointerValue(nil)
return nil
}
res, err := c.GetCredentials(ctx, model.ProjectId.ValueString(), model.Name.ValueString()).Execute()
if err != nil {
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if !ok {
return fmt.Errorf("fetch cluster credentials: could not convert error to oapierror.GenericOpenAPIError")
}
if oapiErr.StatusCode == http.StatusBadRequest {
// deprecated endpoint will return 400 if the new endpoints have been used
// if that's the case, we set the field to null
core.LogAndAddWarning(ctx, diags, "The kubelogin field is set to null", "Failed to get static token kubeconfig, which means the new credentials rotation flow might already been triggered for this cluster. If you are already using the stackit_ske_kubeconfig resource you can ignore this warning. If not, you must use it to access this cluster's short-lived admin kubeconfig.")
model.KubeConfig = types.StringPointerValue(nil)
return nil
}
return fmt.Errorf("fetching cluster credentials: %w", err)
}
model.KubeConfig = types.StringPointerValue(res.Kubeconfig)
return nil
}
func toNodepoolsPayload(ctx context.Context, m *Model) ([]ske.Nodepool, error) {
nodePools := []nodePool{}
diags := m.NodePools.ElementsAs(ctx, &nodePools, false)
if diags.HasError() {
return nil, core.DiagsToError(diags)
}
cnps := []ske.Nodepool{}
for i := range nodePools {
nodePool := nodePools[i]
// taints
taintsModel := []taint{}
diags := nodePool.Taints.ElementsAs(ctx, &taintsModel, false)
if diags.HasError() {
return nil, core.DiagsToError(diags)
}
ts := []ske.Taint{}
for _, v := range taintsModel {
t := ske.Taint{
Effect: conversion.StringValueToPointer(v.Effect),
Key: conversion.StringValueToPointer(v.Key),
Value: conversion.StringValueToPointer(v.Value),
}
ts = append(ts, t)
}
// labels
var ls *map[string]string
if nodePool.Labels.IsNull() {
ls = nil
} else {
lsm := map[string]string{}
for k, v := range nodePool.Labels.Elements() {
nv, err := conversion.ToString(ctx, v)
if err != nil {
lsm[k] = ""
continue
}
lsm[k] = nv
}
ls = &lsm
}
// zones
zs := []string{}
for _, v := range nodePool.AvailabilityZones.Elements() {
if v.IsNull() || v.IsUnknown() {
continue
}
s, err := conversion.ToString(context.TODO(), v)
if err != nil {
continue
}
zs = append(zs, s)
}
cn := ske.CRI{
Name: conversion.StringValueToPointer(nodePool.CRI),
}
cnp := ske.Nodepool{
Name: conversion.StringValueToPointer(nodePool.Name),
Minimum: conversion.Int64ValueToPointer(nodePool.Minimum),
Maximum: conversion.Int64ValueToPointer(nodePool.Maximum),
MaxSurge: conversion.Int64ValueToPointer(nodePool.MaxSurge),
MaxUnavailable: conversion.Int64ValueToPointer(nodePool.MaxUnavailable),
Machine: &ske.Machine{
Type: conversion.StringValueToPointer(nodePool.MachineType),
Image: &ske.Image{
Name: conversion.StringValueToPointer(nodePool.OSName),
Version: conversion.StringValueToPointer(nodePool.OSVersion),
},
},
Volume: &ske.Volume{
Type: conversion.StringValueToPointer(nodePool.VolumeType),
Size: conversion.Int64ValueToPointer(nodePool.VolumeSize),
},
Taints: &ts,
Cri: &cn,
Labels: ls,
AvailabilityZones: &zs,
}
cnps = append(cnps, cnp)
}
return cnps, nil
}
func toHibernationsPayload(ctx context.Context, m *Model) (*ske.Hibernation, error) {
hibernation := []hibernation{}
diags := m.Hibernations.ElementsAs(ctx, &hibernation, false)
if diags.HasError() {
return nil, core.DiagsToError(diags)
}
if len(hibernation) == 0 {
return nil, nil
}
scs := []ske.HibernationSchedule{}
for _, h := range hibernation {
sc := ske.HibernationSchedule{
Start: conversion.StringValueToPointer(h.Start),
End: conversion.StringValueToPointer(h.End),
}
if !h.Timezone.IsNull() && !h.Timezone.IsUnknown() {
tz := h.Timezone.ValueString()
sc.Timezone = &tz
}
scs = append(scs, sc)
}
return &ske.Hibernation{
Schedules: &scs,
}, nil
}
func toExtensionsPayload(ctx context.Context, m *Model) (*ske.Extension, error) {
if m.Extensions.IsNull() || m.Extensions.IsUnknown() {
return nil, nil
}
ex := extensions{}
diags := m.Extensions.As(ctx, &ex, basetypes.ObjectAsOptions{})
if diags.HasError() {
return nil, fmt.Errorf("converting extensions object: %v", diags.Errors())
}
var skeAcl *ske.ACL
if !(ex.ACL.IsNull() || ex.ACL.IsUnknown()) {
acl := acl{}
diags = ex.ACL.As(ctx, &acl, basetypes.ObjectAsOptions{})
if diags.HasError() {
return nil, fmt.Errorf("converting extensions.acl object: %v", diags.Errors())
}
aclEnabled := conversion.BoolValueToPointer(acl.Enabled)
cidrs := []string{}
diags = acl.AllowedCIDRs.ElementsAs(ctx, &cidrs, true)
if diags.HasError() {
return nil, fmt.Errorf("converting extensions.acl.cidrs object: %v", diags.Errors())
}
skeAcl = &ske.ACL{
Enabled: aclEnabled,
AllowedCidrs: &cidrs,
}
}
var skeArgus *ske.Argus
if !(ex.Argus.IsNull() || ex.Argus.IsUnknown()) {
argus := argus{}
diags = ex.Argus.As(ctx, &argus, basetypes.ObjectAsOptions{})
if diags.HasError() {
return nil, fmt.Errorf("converting extensions.argus object: %v", diags.Errors())
}
argusEnabled := conversion.BoolValueToPointer(argus.Enabled)
argusInstanceId := conversion.StringValueToPointer(argus.ArgusInstanceId)
skeArgus = &ske.Argus{
Enabled: argusEnabled,
ArgusInstanceId: argusInstanceId,
}
}
return &ske.Extension{
Acl: skeAcl,
Argus: skeArgus,
}, nil
}
func toMaintenancePayload(ctx context.Context, m *Model) (*ske.Maintenance, error) {
if m.Maintenance.IsNull() || m.Maintenance.IsUnknown() {
return nil, nil
}
maintenance := maintenance{}
diags := m.Maintenance.As(ctx, &maintenance, basetypes.ObjectAsOptions{})
if diags.HasError() {
return nil, fmt.Errorf("converting maintenance object: %v", diags.Errors())
}
var timeWindowStart *string
if !(maintenance.Start.IsNull() || maintenance.Start.IsUnknown()) {
// API expects RFC3339 datetime
timeWindowStart = utils.Ptr(
fmt.Sprintf("0000-01-01T%s", maintenance.Start.ValueString()),
)
}
var timeWindowEnd *string
if !(maintenance.End.IsNull() || maintenance.End.IsUnknown()) {
// API expects RFC3339 datetime
timeWindowEnd = utils.Ptr(
fmt.Sprintf("0000-01-01T%s", maintenance.End.ValueString()),
)
}
return &ske.Maintenance{
AutoUpdate: &ske.MaintenanceAutoUpdate{
KubernetesVersion: conversion.BoolValueToPointer(maintenance.EnableKubernetesVersionUpdates),
MachineImageVersion: conversion.BoolValueToPointer(maintenance.EnableMachineImageVersionUpdates),
},
TimeWindow: &ske.TimeWindow{
Start: timeWindowStart,
End: timeWindowEnd,
},
}, nil
}
func mapFields(ctx context.Context, cl *ske.Cluster, m *Model) error {
if cl == nil {
return fmt.Errorf("response input is nil")
}
if m == nil {
return fmt.Errorf("model input is nil")
}
var name string
if m.Name.ValueString() != "" {
name = m.Name.ValueString()
} else if cl.Name != nil {
name = *cl.Name
} else {
return fmt.Errorf("name not present")
}
m.Name = types.StringValue(name)
idParts := []string{
m.ProjectId.ValueString(),
name,
}
m.Id = types.StringValue(
strings.Join(idParts, core.Separator),
)
if cl.Kubernetes != nil {
// The k8s version returned by the API includes the patch version, while we only support major and minor in the kubernetes_version field
// This prevents inconsistent state by automatic updates to the patch version in the API
versionPreffixed := "v" + *cl.Kubernetes.Version
majorMinorVersionPreffixed := semver.MajorMinor(versionPreffixed)
majorMinorVersion, _ := strings.CutPrefix(majorMinorVersionPreffixed, "v")
m.KubernetesVersion = types.StringPointerValue(utils.Ptr(majorMinorVersion))
m.KubernetesVersionUsed = types.StringPointerValue(cl.Kubernetes.Version)
m.AllowPrivilegedContainers = types.BoolPointerValue(cl.Kubernetes.AllowPrivilegedContainers)
}
err := mapNodePools(ctx, cl, m)
if err != nil {
return fmt.Errorf("mapping node_pools: %w", err)
}
err = mapMaintenance(ctx, cl, m)
if err != nil {
return fmt.Errorf("mapping maintenance: %w", err)
}
err = mapHibernations(cl, m)
if err != nil {
return fmt.Errorf("mapping hibernations: %w", err)
}
err = mapExtensions(ctx, cl, m)
if err != nil {
return fmt.Errorf("mapping extensions: %w", err)
}
return nil
}
func mapNodePools(ctx context.Context, cl *ske.Cluster, m *Model) error {
if cl.Nodepools == nil {
m.NodePools = types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes})
return nil
}
nodePools := []attr.Value{}
for i, nodePoolResp := range *cl.Nodepools {
nodePool := map[string]attr.Value{
"name": types.StringPointerValue(nodePoolResp.Name),
"machine_type": types.StringPointerValue(nodePoolResp.Machine.Type),
"os_name": types.StringNull(),
"os_version": types.StringNull(),
"minimum": types.Int64PointerValue(nodePoolResp.Minimum),
"maximum": types.Int64PointerValue(nodePoolResp.Maximum),
"max_surge": types.Int64PointerValue(nodePoolResp.MaxSurge),
"max_unavailable": types.Int64PointerValue(nodePoolResp.MaxUnavailable),
"volume_type": types.StringNull(),
"volume_size": types.Int64PointerValue(nodePoolResp.Volume.Size),
"labels": types.MapNull(types.StringType),
"cri": types.StringNull(),
"availability_zones": types.ListNull(types.StringType),
}
if nodePoolResp.Machine != nil && nodePoolResp.Machine.Image != nil {
nodePool["os_name"] = types.StringPointerValue(nodePoolResp.Machine.Image.Name)
nodePool["os_version"] = types.StringPointerValue(nodePoolResp.Machine.Image.Version)
}
if nodePoolResp.Volume != nil {
nodePool["volume_type"] = types.StringPointerValue(nodePoolResp.Volume.Type)
}
if nodePoolResp.Cri != nil {
nodePool["cri"] = types.StringPointerValue(nodePoolResp.Cri.Name)
}
err := mapTaints(nodePoolResp.Taints, nodePool)
if err != nil {
return fmt.Errorf("mapping index %d, field taints: %w", i, err)
}
if nodePoolResp.Labels != nil {
elems := map[string]attr.Value{}
for k, v := range *nodePoolResp.Labels {
elems[k] = types.StringValue(v)
}
elemsTF, diags := types.MapValue(types.StringType, elems)
if diags.HasError() {
return fmt.Errorf("mapping index %d, field labels: %w", i, core.DiagsToError(diags))
}
nodePool["labels"] = elemsTF
}
if nodePoolResp.AvailabilityZones != nil {
elemsTF, diags := types.ListValueFrom(ctx, types.StringType, *nodePoolResp.AvailabilityZones)
if diags.HasError() {
return fmt.Errorf("mapping index %d, field availability_zones: %w", i, core.DiagsToError(diags))
}
nodePool["availability_zones"] = elemsTF
}
nodePoolTF, diags := basetypes.NewObjectValue(nodePoolTypes, nodePool)
if diags.HasError() {
return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags))
}
nodePools = append(nodePools, nodePoolTF)
}
nodePoolsTF, diags := basetypes.NewListValue(types.ObjectType{AttrTypes: nodePoolTypes}, nodePools)
if diags.HasError() {
return core.DiagsToError(diags)
}
m.NodePools = nodePoolsTF
return nil
}
func mapTaints(t *[]ske.Taint, nodePool map[string]attr.Value) error {
if t == nil || len(*t) == 0 {
nodePool["taints"] = types.ListNull(types.ObjectType{AttrTypes: taintTypes})
return nil
}
taints := []attr.Value{}
for i, taintResp := range *t {
taint := map[string]attr.Value{
"effect": types.StringPointerValue(taintResp.Effect),
"key": types.StringPointerValue(taintResp.Key),
"value": types.StringPointerValue(taintResp.Value),
}
taintTF, diags := basetypes.NewObjectValue(taintTypes, taint)
if diags.HasError() {
return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags))
}
taints = append(taints, taintTF)
}
taintsTF, diags := basetypes.NewListValue(types.ObjectType{AttrTypes: taintTypes}, taints)
if diags.HasError() {
return core.DiagsToError(diags)
}
nodePool["taints"] = taintsTF
return nil
}
func mapHibernations(cl *ske.Cluster, m *Model) error {
if cl.Hibernation == nil {
if !m.Hibernations.IsNull() {
emptyHibernations, diags := basetypes.NewListValue(basetypes.ObjectType{AttrTypes: hibernationTypes}, []attr.Value{})
if diags.HasError() {
return fmt.Errorf("hibernations is an empty list, converting to terraform empty list: %w", core.DiagsToError(diags))
}
m.Hibernations = emptyHibernations
return nil
}
m.Hibernations = basetypes.NewListNull(basetypes.ObjectType{AttrTypes: hibernationTypes})
return nil
}
if cl.Hibernation.Schedules == nil {
emptyHibernations, diags := basetypes.NewListValue(basetypes.ObjectType{AttrTypes: hibernationTypes}, []attr.Value{})
if diags.HasError() {
return fmt.Errorf("hibernations is an empty list, converting to terraform empty list: %w", core.DiagsToError(diags))
}
m.Hibernations = emptyHibernations
return nil
}
hibernations := []attr.Value{}
for i, hibernationResp := range *cl.Hibernation.Schedules {
hibernation := map[string]attr.Value{
"start": types.StringPointerValue(hibernationResp.Start),
"end": types.StringPointerValue(hibernationResp.End),
"timezone": types.StringPointerValue(hibernationResp.Timezone),
}
hibernationTF, diags := basetypes.NewObjectValue(hibernationTypes, hibernation)
if diags.HasError() {
return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags))
}
hibernations = append(hibernations, hibernationTF)
}
hibernationsTF, diags := basetypes.NewListValue(types.ObjectType{AttrTypes: hibernationTypes}, hibernations)
if diags.HasError() {
return core.DiagsToError(diags)
}
m.Hibernations = hibernationsTF
return nil
}
func mapMaintenance(ctx context.Context, cl *ske.Cluster, m *Model) error {
// Aligned with SKE team that a flattened data structure is fine, because no extensions are planned.
if cl.Maintenance == nil {
m.Maintenance = types.ObjectNull(maintenanceTypes)
return nil
}
ekvu := types.BoolNull()
if cl.Maintenance.AutoUpdate.KubernetesVersion != nil {
ekvu = types.BoolValue(*cl.Maintenance.AutoUpdate.KubernetesVersion)
}
emvu := types.BoolNull()
if cl.Maintenance.AutoUpdate.KubernetesVersion != nil {
emvu = types.BoolValue(*cl.Maintenance.AutoUpdate.MachineImageVersion)
}
startTime, endTime, err := getMaintenanceTimes(ctx, cl, m)
if err != nil {
return fmt.Errorf("getting maintenance times: %w", err)
}
maintenanceValues := map[string]attr.Value{
"enable_kubernetes_version_updates": ekvu,
"enable_machine_image_version_updates": emvu,
"start": types.StringValue(startTime),
"end": types.StringValue(endTime),
}
maintenanceObject, diags := types.ObjectValue(maintenanceTypes, maintenanceValues)
if diags.HasError() {
return fmt.Errorf("creating flavor: %w", core.DiagsToError(diags))
}
m.Maintenance = maintenanceObject
return nil
}
func getMaintenanceTimes(ctx context.Context, cl *ske.Cluster, m *Model) (startTime, endTime string, err error) {
startTimeAPI, err := time.Parse(time.RFC3339, *cl.Maintenance.TimeWindow.Start)
if err != nil {
return "", "", fmt.Errorf("parsing start time '%s' from API response as RFC3339 datetime: %w", *cl.Maintenance.TimeWindow.Start, err)
}
endTimeAPI, err := time.Parse(time.RFC3339, *cl.Maintenance.TimeWindow.End)
if err != nil {
return "", "", fmt.Errorf("parsing end time '%s' from API response as RFC3339 datetime: %w", *cl.Maintenance.TimeWindow.End, err)
}
if m.Maintenance.IsNull() || m.Maintenance.IsUnknown() {
return startTimeAPI.Format("15:04:05Z07:00"), endTimeAPI.Format("15:04:05Z07:00"), nil
}
maintenance := &maintenance{}
diags := m.Maintenance.As(ctx, maintenance, basetypes.ObjectAsOptions{})
if diags.HasError() {
return "", "", fmt.Errorf("converting maintenance object %w", core.DiagsToError(diags.Errors()))
}
if maintenance.Start.IsNull() || maintenance.Start.IsUnknown() {
startTime = startTimeAPI.Format("15:04:05Z07:00")
} else {
startTimeTF, err := time.Parse("15:04:05Z07:00", maintenance.Start.ValueString())
if err != nil {
return "", "", fmt.Errorf("parsing start time '%s' from TF config as RFC time: %w", maintenance.Start.ValueString(), err)
}
if startTimeAPI.Format("15:04:05Z07:00") != startTimeTF.Format("15:04:05Z07:00") {
return "", "", fmt.Errorf("start time '%v' from API response doesn't match start time '%v' from TF config", *cl.Maintenance.TimeWindow.Start, maintenance.Start.ValueString())
}
startTime = maintenance.Start.ValueString()
}
if maintenance.End.IsNull() || maintenance.End.IsUnknown() {
endTime = endTimeAPI.Format("15:04:05Z07:00")
} else {
endTimeTF, err := time.Parse("15:04:05Z07:00", maintenance.End.ValueString())
if err != nil {
return "", "", fmt.Errorf("parsing end time '%s' from TF config as RFC time: %w", maintenance.End.ValueString(), err)
}
if endTimeAPI.Format("15:04:05Z07:00") != endTimeTF.Format("15:04:05Z07:00") {
return "", "", fmt.Errorf("end time '%v' from API response doesn't match end time '%v' from TF config", *cl.Maintenance.TimeWindow.End, maintenance.End.ValueString())
}
endTime = maintenance.End.ValueString()
}
return startTime, endTime, nil
}
func checkDisabledExtensions(ctx context.Context, ex extensions) (aclDisabled, argusDisabled bool, err error) {
var diags diag.Diagnostics
acl := acl{}
if ex.ACL.IsNull() {
acl.Enabled = types.BoolValue(false)
} else {
diags = ex.ACL.As(ctx, &acl, basetypes.ObjectAsOptions{})
if diags.HasError() {
return false, false, fmt.Errorf("converting extensions.acl object: %v", diags.Errors())
}
}
argus := argus{}
if ex.Argus.IsNull() {
argus.Enabled = types.BoolValue(false)
} else {
diags = ex.Argus.As(ctx, &argus, basetypes.ObjectAsOptions{})
if diags.HasError() {
return false, false, fmt.Errorf("converting extensions.argus object: %v", diags.Errors())
}
}
return !acl.Enabled.ValueBool(), !argus.Enabled.ValueBool(), nil
}
func mapExtensions(ctx context.Context, cl *ske.Cluster, m *Model) error {
if cl.Extensions == nil {
m.Extensions = types.ObjectNull(extensionsTypes)
return nil
}
var diags diag.Diagnostics
ex := extensions{}
if !m.Extensions.IsNull() {
diags := m.Extensions.As(ctx, &ex, basetypes.ObjectAsOptions{})
if diags.HasError() {
return fmt.Errorf("converting extensions object: %v", diags.Errors())
}
}
// If the user provides the extensions block with the enabled flags as false
// the SKE API will return an empty extensions block, which throws an inconsistent
// result after apply error. To prevent this error, if both flags are false,
// we set the fields provided by the user in the terraform model
// If the extensions field is not provided, the SKE API returns an empty object.
// If we parse that object into the terraform model, it will produce an inconsistent result after apply
// error
aclDisabled, argusDisabled, err := checkDisabledExtensions(ctx, ex)
if err != nil {
return fmt.Errorf("checking if extensions are disabled: %w", err)
}
disabledExtensions := false
if aclDisabled && argusDisabled {
disabledExtensions = true
}
emptyExtensions := &ske.Extension{}
if *cl.Extensions == *emptyExtensions && (disabledExtensions || m.Extensions.IsNull()) {
if m.Extensions.Attributes() == nil {
m.Extensions = types.ObjectNull(extensionsTypes)
}
return nil
}
aclExtension := types.ObjectNull(aclTypes)
if cl.Extensions.Acl != nil {
enabled := types.BoolNull()
if cl.Extensions.Acl.Enabled != nil {
enabled = types.BoolValue(*cl.Extensions.Acl.Enabled)
}
cidrsList, diags := types.ListValueFrom(ctx, types.StringType, cl.Extensions.Acl.AllowedCidrs)
if diags.HasError() {
return fmt.Errorf("creating allowed_cidrs list: %w", core.DiagsToError(diags))
}
aclValues := map[string]attr.Value{
"enabled": enabled,
"allowed_cidrs": cidrsList,
}
aclExtension, diags = types.ObjectValue(aclTypes, aclValues)
if diags.HasError() {
return fmt.Errorf("creating acl: %w", core.DiagsToError(diags))
}
} else if aclDisabled && !ex.ACL.IsNull() {
aclExtension = ex.ACL
}
argusExtension := types.ObjectNull(argusTypes)
if cl.Extensions.Argus != nil {
enabled := types.BoolNull()
if cl.Extensions.Argus.Enabled != nil {
enabled = types.BoolValue(*cl.Extensions.Argus.Enabled)
}
argusInstanceId := types.StringNull()
if cl.Extensions.Argus.ArgusInstanceId != nil {
argusInstanceId = types.StringValue(*cl.Extensions.Argus.ArgusInstanceId)
}
argusExtensionValues := map[string]attr.Value{
"enabled": enabled,
"argus_instance_id": argusInstanceId,
}
argusExtension, diags = types.ObjectValue(argusTypes, argusExtensionValues)
if diags.HasError() {
return fmt.Errorf("creating argus extension: %w", core.DiagsToError(diags))
}
} else if argusDisabled && !ex.Argus.IsNull() {
argusExtension = ex.Argus
}
extensionsValues := map[string]attr.Value{
"acl": aclExtension,
"argus": argusExtension,
}
extensions, diags := types.ObjectValue(extensionsTypes, extensionsValues)
if diags.HasError() {
return fmt.Errorf("creating extensions: %w", core.DiagsToError(diags))
}
m.Extensions = extensions
return nil
}
func toKubernetesPayload(m *Model, availableVersions []ske.KubernetesVersion) (kubernetesPayload *ske.Kubernetes, hasDeprecatedVersion bool, err error) {
versionUsed, hasDeprecatedVersion, err := latestMatchingVersion(availableVersions, conversion.StringValueToPointer(m.KubernetesVersion))
if err != nil {
return nil, false, fmt.Errorf("getting latest matching kubernetes version: %w", err)
}
k := &ske.Kubernetes{
Version: versionUsed,
AllowPrivilegedContainers: conversion.BoolValueToPointer(m.AllowPrivilegedContainers),
}
return k, hasDeprecatedVersion, nil
}
func latestMatchingVersion(availableVersions []ske.KubernetesVersion, providedVersion *string) (version *string, deprecated bool, err error) {
deprecated = false
if availableVersions == nil {
return nil, false, fmt.Errorf("nil available kubernetes versions")
}
if providedVersion == nil {
return nil, false, fmt.Errorf("provided version is nil")
}
providedVersionPrefixed := "v" + *providedVersion
if !semver.IsValid(providedVersionPrefixed) {
return nil, false, fmt.Errorf("provided version is invalid")
}
var versionUsed *string
var availableVersionsArray []string
// Get the higher available version that matches the major and minor version provided by the user
for _, v := range availableVersions {
if v.State == nil || v.Version == nil {
continue
}
availableVersionsArray = append(availableVersionsArray, *v.Version)
vPreffixed := "v" + *v.Version
if semver.MajorMinor(vPreffixed) == semver.MajorMinor(providedVersionPrefixed) &&
(semver.Compare(vPreffixed, providedVersionPrefixed) == 1 || semver.Compare(vPreffixed, providedVersionPrefixed) == 0) {
versionUsed = v.Version
if strings.EqualFold(*v.State, VersionStateDeprecated) {
deprecated = true
} else {
deprecated = false
}
}
}
// Throwing error if we could not match the version with the available versions
if versionUsed == nil {
return nil, false, fmt.Errorf("provided version is not one of the available kubernetes versions, available versions are: %s", strings.Join(availableVersionsArray, ","))
}
return versionUsed, deprecated, nil
}
func (r *clusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var state Model
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := state.ProjectId.ValueString()
name := state.Name.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "name", name)
clResp, err := r.client.GetCluster(ctx, projectId, name).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading cluster", fmt.Sprintf("Calling API: %v", err))
return
}
err = mapFields(ctx, clResp, &state)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading cluster", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Handle credential
err = r.getCredential(ctx, &resp.Diagnostics, &state)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading cluster", fmt.Sprintf("Getting credential: %v", err))
return
}
diags = resp.State.Set(ctx, state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "SKE cluster read")
}
func (r *clusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
diags = checkAllowPrivilegedContainers(model.AllowPrivilegedContainers, model.KubernetesVersion)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
clName := model.Name.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "name", clName)
availableVersions, err := r.loadAvaiableVersions(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating cluster", fmt.Sprintf("Loading available Kubernetes versions: %v", err))
return
}
r.createOrUpdateCluster(ctx, &resp.Diagnostics, &model, availableVersions)
if resp.Diagnostics.HasError() {
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "SKE cluster updated")
}
func (r *clusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
resp.Diagnostics.Append(req.State.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
name := model.Name.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "name", name)
c := r.client
_, err := c.DeleteCluster(ctx, projectId, name).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting cluster", fmt.Sprintf("Calling API: %v", err))
return
}
_, err = wait.DeleteClusterWaitHandler(ctx, r.client, projectId, name).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting cluster", fmt.Sprintf("Cluster deletion waiting: %v", err))
return
}
tflog.Info(ctx, "SKE cluster deleted")
}
// ImportState imports a resource into the Terraform state on success.
// The expected format of the resource import identifier is: project_id,name
func (r *clusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing cluster",
fmt.Sprintf("Expected import identifier with format: [project_id],[name] Got: %q", req.ID),
)
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[1])...)
tflog.Info(ctx, "SKE cluster state imported")
}