Deprecate kubeconfig field and add stackit_ske_kubeconfig resource (#256)

* Implement kubeconfig resource

* Update acc test, skip get credentials

* Update acc test

* Add warning on Create

* Add option to refresh

* Fix lint

* Add comment, generate docs

* Update stackit/internal/services/ske/cluster/resource.go

Co-authored-by: João Palet <joao.palet@outlook.com>

* Update stackit/internal/services/ske/kubeconfig/resource.go

Co-authored-by: João Palet <joao.palet@outlook.com>

* Changes after review

* Fix schema

* Gen docs

* Rename

* Credentials handling in datasource, update acc test

* Fix datasource

* Update descriptions

---------

Co-authored-by: João Palet <joao.palet@outlook.com>
This commit is contained in:
Vicente Pinto 2024-02-09 10:17:04 +00:00 committed by GitHub
parent 91b2c42a19
commit bde1fc55e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 618 additions and 39 deletions

View file

@ -35,7 +35,7 @@ This should be used with care since it also disables a couple of other features
- `extensions` (Attributes) A single extensions block as defined below (see [below for nested schema](#nestedatt--extensions))
- `hibernations` (Attributes List) One or more hibernation block as defined below. (see [below for nested schema](#nestedatt--hibernations))
- `id` (String) Terraform's internal data source. ID. It is structured as "`project_id`,`name`".
- `kube_config` (String, Sensitive) Kube config file used for connecting to the cluster
- `kube_config` (String, Sensitive, Deprecated) Kube config file 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).
- `kubernetes_version` (String) Kubernetes version.
- `kubernetes_version_used` (String) Full Kubernetes version used. For example, if `1.22` was selected, this value may result to `1.22.15`
- `maintenance` (Attributes) A single maintenance block as defined below (see [below for nested schema](#nestedatt--maintenance))

View file

@ -58,7 +58,7 @@ Deprecated as of Kubernetes 1.25 and later
### Read-Only
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`name`".
- `kube_config` (String, Sensitive) Kube config file used for connecting to the cluster
- `kube_config` (String, Sensitive, Deprecated) 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).
- `kubernetes_version_used` (String) Full Kubernetes version used. For example, if 1.22 was selected, this value may result to 1.22.15
<a id="nestedatt--node_pools"></a>

View file

@ -0,0 +1,33 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "stackit_ske_kubeconfig Resource - stackit"
subcategory: ""
description: |-
SKE kubeconfig resource schema. Must have a region specified in the provider configuration.
---
# stackit_ske_kubeconfig (Resource)
SKE kubeconfig resource schema. Must have a `region` specified in the provider configuration.
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `cluster_name` (String) Name of the SKE cluster.
- `project_id` (String) STACKIT project ID to which the cluster is associated.
### Optional
- `expiration` (Number) Expiration time of the kubeconfig, in seconds.
- `refresh` (Boolean) If set to true, the provider will check if the kubeconfig has expired and will generated a new valid one in-place
### Read-Only
- `expires_at` (String) Timestamp when the kubeconfig expires
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`cluster_name`,`kube_config_id`".
- `kube_config` (String, Sensitive) Raw short-lived admin kubeconfig.
- `kube_config_id` (String) Internally generated UUID to identify a kubeconfig resource in Terraform, since the SKE API doesnt return a kubeconfig identifier

View file

@ -3,6 +3,7 @@ package ske
import (
"context"
"fmt"
"net/http"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
@ -11,9 +12,11 @@ import (
"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/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/ske"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
"golang.org/x/mod/semver"
)
// Ensure the implementation satisfies the expected interfaces.
@ -269,9 +272,10 @@ func (r *clusterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest
},
},
"kube_config": schema.StringAttribute{
Description: "Kube config file used for connecting to the cluster",
Sensitive: true,
Computed: true,
Description: "Kube config file 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).",
},
},
}
@ -301,7 +305,12 @@ func (r *clusterDataSource) Read(ctx context.Context, req datasource.ReadRequest
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading cluster", fmt.Sprintf("Processing API payload: %v", err))
return
}
r.getCredential(ctx, &diags, &state)
// 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
}
// Set refreshed state
diags = resp.State.Set(ctx, state)
resp.Diagnostics.Append(diags...)
@ -311,12 +320,29 @@ func (r *clusterDataSource) Read(ctx context.Context, req datasource.ReadRequest
tflog.Info(ctx, "SKE cluster read")
}
func (r *clusterDataSource) getCredential(ctx context.Context, diags *diag.Diagnostics, model *Model) {
func (r *clusterDataSource) 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 {
diags.AddError("failed fetching cluster credentials for data source", err.Error())
return
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", "The call to GetCredentials failed, 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 start using it.")
model.KubeConfig = types.StringPointerValue(nil)
return nil
}
return fmt.Errorf("fetching cluster credentials: %w", err)
}
model.KubeConfig = types.StringPointerValue(res.Kubeconfig)
return nil
}

View file

@ -3,6 +3,7 @@ package ske
import (
"context"
"fmt"
"net/http"
"regexp"
"strings"
"time"
@ -27,6 +28,7 @@ import (
"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"
@ -497,9 +499,10 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re
},
},
"kube_config": schema.StringAttribute{
Description: "Kube config file used for connecting to the cluster",
Sensitive: true,
Computed: true,
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).",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
@ -648,17 +651,34 @@ func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag
}
// Handle credential
err = r.getCredential(ctx, model)
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, model *Model) error {
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", "The call to GetCredentials failed, 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 start using it.")
model.KubeConfig = types.StringPointerValue(nil)
return nil
}
return fmt.Errorf("fetching cluster credentials: %w", err)
}
model.KubeConfig = types.StringPointerValue(res.Kubeconfig)

View file

@ -0,0 +1,361 @@
package ske
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-log/tflog"
"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"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/ske"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &kubeconfigResource{}
_ resource.ResourceWithConfigure = &kubeconfigResource{}
_ resource.ResourceWithModifyPlan = &kubeconfigResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ClusterName types.String `tfsdk:"cluster_name"`
ProjectId types.String `tfsdk:"project_id"`
KubeconfigId types.String `tfsdk:"kube_config_id"` // uuid generated internally because kubeconfig has no identifier
Kubeconfig types.String `tfsdk:"kube_config"`
Expiration types.Int64 `tfsdk:"expiration"`
Refresh types.Bool `tfsdk:"refresh"`
ExpiresAt types.String `tfsdk:"expires_at"`
}
// NewKubeconfigResource is a helper function to simplify the provider implementation.
func NewKubeconfigResource() resource.Resource {
return &kubeconfigResource{}
}
// kubeconfigResource is the resource implementation.
type kubeconfigResource struct {
client *ske.APIClient
}
// Metadata returns the resource type name.
func (r *kubeconfigResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_ske_kubeconfig"
}
// Configure adds the provider configured client to the resource.
func (r *kubeconfigResource) 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 kubeconfig client configured")
}
// Schema defines the schema for the resource.
func (r *kubeconfigResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
descriptions := map[string]string{
"main": "SKE kubeconfig resource schema. Must have a `region` specified in the provider configuration.",
"id": "Terraform's internal resource ID. It is structured as \"`project_id`,`cluster_name`,`kube_config_id`\".",
"kube_config_id": "Internally generated UUID to identify a kubeconfig resource in Terraform, since the SKE API doesnt return a kubeconfig identifier",
"cluster_name": "Name of the SKE cluster.",
"project_id": "STACKIT project ID to which the cluster is associated.",
"kube_config": "Raw short-lived admin kubeconfig.",
"expiration": "Expiration time of the kubeconfig, in seconds.",
"expires_at": "Timestamp when the kubeconfig expires",
"refresh": "If set to true, the provider will check if the kubeconfig has expired and will generated a new valid one in-place",
}
resp.Schema = schema.Schema{
Description: descriptions["main"],
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: descriptions["id"],
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"kube_config_id": schema.StringAttribute{
Description: descriptions["kube_config_id"],
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"cluster_name": schema.StringAttribute{
Description: descriptions["cluster_name"],
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
validate.NoSeparator(),
},
},
"project_id": schema.StringAttribute{
Description: descriptions["project_id"],
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"expiration": schema.Int64Attribute{
Description: descriptions["expiration"],
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
int64planmodifier.UseStateForUnknown(),
},
},
"refresh": schema.BoolAttribute{
Description: descriptions["refresh"],
Optional: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.RequiresReplace(),
},
},
"kube_config": schema.StringAttribute{
Description: descriptions["kube_config"],
Computed: true,
Sensitive: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"expires_at": schema.StringAttribute{
Description: descriptions["expires_at"],
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}
// ModifyPlan will be called in the Plan phase and will check if the plan is a creation of the resource
// If so, show warning related to deprecated credentials endpoints
func (r *kubeconfigResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
if req.State.Raw.IsNull() {
// Planned to create a kubeconfig
core.LogAndAddWarning(ctx, &resp.Diagnostics, "Planned to create kubeconfig", "Once this resource is created, you will no longer be able to use the deprecated credentials endpoints and the kube_config field on the cluster resource will be empty for this cluster. For more info check How to Rotate SKE Credentials (https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html)")
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *kubeconfigResource) 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
}
projectId := model.ProjectId.ValueString()
clusterName := model.ClusterName.ValueString()
kubeconfigUUID := uuid.New().String()
model.KubeconfigId = types.StringValue(kubeconfigUUID)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "cluster_name", clusterName)
ctx = tflog.SetField(ctx, "kube_config_id", kubeconfigUUID)
err := r.createKubeconfig(ctx, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating kubeconfig", fmt.Sprintf("Creating kubeconfig: %v", err))
return
}
// Set state to fully populated data
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "SKE kubeconfig created")
}
// Read refreshes the Terraform state with the latest data.
// There is no GET kubeconfig endpoint.
// If the refresh field is set, Read will check the expiration date and will get a new valid kubeconfig if it has expired
func (r *kubeconfigResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
clusterName := model.ClusterName.ValueString()
kubeconfigUUID := model.KubeconfigId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "cluster_name", clusterName)
ctx = tflog.SetField(ctx, "kube_config_id", kubeconfigUUID)
if model.Refresh.ValueBool() && !model.ExpiresAt.IsNull() {
expiresAt, err := time.Parse("2006-01-02T15:04:05Z07:00", model.ExpiresAt.ValueString())
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("Converting expiresAt field to timestamp: %v", err))
return
}
currentTime := time.Now()
if expiresAt.Before(currentTime) {
err := r.createKubeconfig(ctx, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("The existing kubeconfig is expired and the refresh field is enabled, creating a new one: %v", err))
return
}
// Set state to fully populated data
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
}
tflog.Info(ctx, "SKE kubeconfig read")
}
func (r *kubeconfigResource) createKubeconfig(ctx context.Context, model *Model) error {
// Generate API request body from model
payload, err := toCreatePayload(model)
if err != nil {
return fmt.Errorf("creating API payload: %w", err)
}
// Create new kubeconfig
kubeconfigResp, err := r.client.CreateKubeconfig(ctx, model.ProjectId.ValueString(), model.ClusterName.ValueString()).CreateKubeconfigPayload(*payload).Execute()
if err != nil {
return fmt.Errorf("calling API: %w", err)
}
// Map response body to schema
err = mapFields(kubeconfigResp, model)
if err != nil {
return fmt.Errorf("processing API payload: %w", err)
}
return nil
}
func (r *kubeconfigResource) 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 kubeconfig", "Kubeconfig can't be updated")
}
// Delete deletes the resource and removes the Terraform state on success.
func (r *kubeconfigResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
core.LogAndAddWarning(ctx, &resp.Diagnostics, "Deleting kubeconfig", "Deleted this resource will only remove the values from the terraform state, it will not trigger a deletion or revoke of the actual kubeconfig as this is not supported by the SKE API. The kubeconfig will still be valid until it expires.")
// Retrieve values from plan
var model Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
clusterName := model.ClusterName.ValueString()
kubeconfigUUID := model.KubeconfigId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "cluster_name", clusterName)
ctx = tflog.SetField(ctx, "kube_config_id", kubeconfigUUID)
// kubeconfig is deleted automatically from the state
tflog.Info(ctx, "SKE kubeconfig deleted")
}
func mapFields(kubeconfigResp *ske.Kubeconfig, model *Model) error {
if kubeconfigResp == nil {
return fmt.Errorf("response is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
idParts := []string{
model.ProjectId.ValueString(),
model.ClusterName.ValueString(),
model.KubeconfigId.ValueString(),
}
model.Id = types.StringValue(
strings.Join(idParts, core.Separator),
)
if kubeconfigResp.Kubeconfig == nil {
return fmt.Errorf("kubeconfig not present")
}
model.Kubeconfig = types.StringPointerValue(kubeconfigResp.Kubeconfig)
model.ExpiresAt = types.StringPointerValue(kubeconfigResp.ExpirationTimestamp)
return nil
}
func toCreatePayload(model *Model) (*ske.CreateKubeconfigPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
expiration := conversion.Int64ValueToPointer(model.Expiration)
var expirationStringPtr *string
if expiration != nil {
expirationStringPtr = utils.Ptr(strconv.FormatInt(*expiration, 10))
}
return &ske.CreateKubeconfigPayload{
ExpirationSeconds: expirationStringPtr,
}, nil
}

View file

@ -0,0 +1,127 @@
package ske
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/ske"
)
func TestMapFields(t *testing.T) {
tests := []struct {
description string
input *ske.Kubeconfig
expected Model
isValid bool
}{
{
"simple_values",
&ske.Kubeconfig{
ExpirationTimestamp: utils.Ptr("2024-02-07T16:42:12Z"),
Kubeconfig: utils.Ptr("kubeconfig"),
},
Model{
ClusterName: types.StringValue("name"),
ProjectId: types.StringValue("pid"),
Kubeconfig: types.StringValue("kubeconfig"),
Expiration: types.Int64Null(),
Refresh: types.BoolNull(),
ExpiresAt: types.StringValue("2024-02-07T16:42:12Z"),
},
true,
},
{
"nil_response",
nil,
Model{},
false,
},
{
"empty_kubeconfig",
&ske.Kubeconfig{},
Model{},
false,
},
{
"no_kubeconfig_field",
&ske.Kubeconfig{
ExpirationTimestamp: utils.Ptr("2024-02-07T16:42:12Z"),
},
Model{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
state := &Model{
ProjectId: tt.expected.ProjectId,
ClusterName: tt.expected.ClusterName,
}
err := mapFields(tt.input, state)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(state, &tt.expected, cmpopts.IgnoreFields(Model{}, "Id")) // Id includes a random uuid
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
input *Model
expected *ske.CreateKubeconfigPayload
isValid bool
}{
{
"default_values",
&Model{},
&ske.CreateKubeconfigPayload{},
true,
},
{
"simple_values",
&Model{
Expiration: types.Int64Value(3600),
},
&ske.CreateKubeconfigPayload{
ExpirationSeconds: utils.Ptr("3600"),
},
true,
},
{
"nil_model",
nil,
nil,
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output, err := toCreatePayload(tt.input)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(output, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}

View file

@ -25,16 +25,15 @@ var clusterResource = map[string]string{
"project_id": testutil.ProjectId,
"name": fmt.Sprintf("cl-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)),
"name_min": fmt.Sprintf("cl-min-%s", acctest.RandStringFromCharSet(3, acctest.CharSetAlphaNum)),
"kubernetes_version": "1.24",
"kubernetes_version_used": "1.24.17",
"kubernetes_version_new": "1.25",
"kubernetes_version_used_new": "1.25.15",
"allowPrivilegedContainers": "true",
"kubernetes_version": "1.25",
"kubernetes_version_used": "1.25.16",
"kubernetes_version_new": "1.26",
"kubernetes_version_used_new": "1.26.13",
"nodepool_name": "np-acc-test",
"nodepool_name_min": "np-acc-min-test",
"nodepool_machine_type": "b1.2",
"nodepool_os_version": "3602.2.1",
"nodepool_os_version_min": "3602.2.1",
"nodepool_os_version": "3602.2.2",
"nodepool_os_version_min": "3602.2.2",
"nodepool_os_name": "flatcar",
"nodepool_minimum": "2",
"nodepool_maximum": "3",
@ -61,17 +60,10 @@ var clusterResource = map[string]string{
"maintenance_start": "01:23:45Z",
"maintenance_end": "05:00:00+02:00",
"maintenance_end_new": "03:03:03+00:00",
"kubeconfig_expiration": "3600",
}
func getConfig(version string, apc *bool, maintenanceEnd *string) string {
apcConfig := ""
if apc != nil {
if *apc {
apcConfig = "allow_privileged_containers = true"
} else {
apcConfig = "allow_privileged_containers = false"
}
}
func getConfig(version string, maintenanceEnd *string) string {
maintenanceEndTF := clusterResource["maintenance_end"]
if maintenanceEnd != nil {
maintenanceEndTF = *maintenanceEnd
@ -87,7 +79,6 @@ func getConfig(version string, apc *bool, maintenanceEnd *string) string {
project_id = stackit_ske_project.project.project_id
name = "%s"
kubernetes_version = "%s"
%s
node_pools = [{
name = "%s"
machine_type = "%s"
@ -133,6 +124,12 @@ func getConfig(version string, apc *bool, maintenanceEnd *string) string {
}
}
resource "stackit_ske_kubeconfig" "kubeconfig" {
project_id = stackit_ske_project.project.project_id
cluster_name = stackit_ske_cluster.cluster.name
expiration = "%s"
}
resource "stackit_ske_cluster" "cluster_min" {
project_id = stackit_ske_project.project.project_id
name = "%s"
@ -157,7 +154,6 @@ func getConfig(version string, apc *bool, maintenanceEnd *string) string {
projectResource["project_id"],
clusterResource["name"],
version,
apcConfig,
clusterResource["nodepool_name"],
clusterResource["nodepool_machine_type"],
clusterResource["nodepool_minimum"],
@ -187,6 +183,9 @@ func getConfig(version string, apc *bool, maintenanceEnd *string) string {
clusterResource["maintenance_start"],
maintenanceEndTF,
// Kubeconfig
clusterResource["kubeconfig_expiration"],
// Minimal
clusterResource["name_min"],
clusterResource["kubernetes_version_new"],
@ -211,7 +210,7 @@ func TestAccSKE(t *testing.T) {
// 1) Creation
{
Config: getConfig(clusterResource["kubernetes_version"], utils.Ptr(true), nil),
Config: getConfig(clusterResource["kubernetes_version"], nil),
Check: resource.ComposeAggregateTestCheckFunc(
// project data
resource.TestCheckResourceAttr("stackit_ske_project.project", "project_id", projectResource["project_id"]),
@ -223,7 +222,6 @@ func TestAccSKE(t *testing.T) {
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "name", clusterResource["name"]),
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "kubernetes_version", clusterResource["kubernetes_version"]),
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "kubernetes_version_used", clusterResource["kubernetes_version_used"]),
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "allow_privileged_containers", clusterResource["allowPrivilegedContainers"]),
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.name", clusterResource["nodepool_name"]),
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.availability_zones.#", "1"),
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.availability_zones.0", clusterResource["nodepool_zone"]),
@ -255,9 +253,22 @@ func TestAccSKE(t *testing.T) {
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.enable_machine_image_version_updates", clusterResource["maintenance_enable_machine_image_version_updates"]),
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.start", clusterResource["maintenance_start"]),
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.end", clusterResource["maintenance_end"]),
resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "kube_config"),
// Kubeconfig
resource.TestCheckResourceAttrPair(
"stackit_ske_kubeconfig.kubeconfig", "project_id",
"stackit_ske_cluster.cluster", "project_id",
),
resource.TestCheckResourceAttrPair(
"stackit_ske_kubeconfig.kubeconfig", "cluster_name",
"stackit_ske_cluster.cluster", "name",
),
resource.TestCheckResourceAttr("stackit_ske_kubeconfig.kubeconfig", "expiration", clusterResource["kubeconfig_expiration"]),
resource.TestCheckResourceAttrSet("stackit_ske_kubeconfig.kubeconfig", "kube_config"),
resource.TestCheckResourceAttrSet("stackit_ske_kubeconfig.kubeconfig", "expires_at"),
// Minimal cluster
resource.TestCheckResourceAttrPair(
"stackit_ske_project.project", "project_id",
@ -314,7 +325,7 @@ func TestAccSKE(t *testing.T) {
}
`,
getConfig(clusterResource["kubernetes_version"], utils.Ptr(true), nil),
getConfig(clusterResource["kubernetes_version"], nil),
projectResource["project_id"],
clusterResource["project_id"],
clusterResource["name"],
@ -334,7 +345,6 @@ func TestAccSKE(t *testing.T) {
resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "name", clusterResource["name"]),
resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "kubernetes_version", clusterResource["kubernetes_version"]),
resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "kubernetes_version_used", clusterResource["kubernetes_version_used"]),
resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "allow_privileged_containers", clusterResource["allowPrivilegedContainers"]),
resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.name", clusterResource["nodepool_name"]),
resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.availability_zones.#", "1"),
resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.availability_zones.0", clusterResource["nodepool_zone"]),
@ -366,7 +376,7 @@ func TestAccSKE(t *testing.T) {
resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "maintenance.start", clusterResource["maintenance_start"]),
resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "maintenance.end", clusterResource["maintenance_end"]),
resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster", "kube_config"),
resource.TestCheckNoResourceAttr("data.stackit_ske_cluster.cluster", "kube_config"), // when using the kubeconfig resource, the kubeconfig field becomes null
// Minimal cluster
resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster_min", "name", clusterResource["name_min"]),
@ -457,7 +467,7 @@ func TestAccSKE(t *testing.T) {
},
// 6) Update kubernetes version and maximum
{
Config: getConfig(clusterResource["kubernetes_version_new"], nil, utils.Ptr(clusterResource["maintenance_end_new"])),
Config: getConfig(clusterResource["kubernetes_version_new"], utils.Ptr(clusterResource["maintenance_end_new"])),
Check: resource.ComposeAggregateTestCheckFunc(
// cluster data
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "project_id", clusterResource["project_id"]),

View file

@ -39,6 +39,7 @@ import (
secretsManagerInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/instance"
secretsManagerUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/user"
skeCluster "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/cluster"
skeKubeconfig "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/kubeconfig"
skeProject "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/project"
sdkauth "github.com/stackitcloud/stackit-sdk-go/core/auth"
@ -405,5 +406,6 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
secretsManagerUser.NewUserResource,
skeProject.NewProjectResource,
skeCluster.NewClusterResource,
skeKubeconfig.NewKubeconfigResource,
}
}