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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/data-sources/ske_cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ data "stackit_ske_cluster" "example" {
### Read-Only

- `access` (Attributes) Configure access to the cluster (see [below for nested schema](#nestedatt--access))
- `audit` (Attributes) Cluster audit log forwarding configuration. (see [below for nested schema](#nestedatt--audit))
- `egress_address_ranges` (List of String) The outgoing network ranges (in CIDR notation) of traffic originating from workload on the cluster.
- `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))
Expand Down Expand Up @@ -63,6 +64,14 @@ Read-Only:



<a id="nestedatt--audit"></a>
### Nested Schema for `audit`

Read-Only:

- `enabled` (Boolean) Enable cluster audit log forwarding to a Telemetry Router.


<a id="nestedatt--extensions"></a>
### Nested Schema for `extensions`

Expand Down
14 changes: 14 additions & 0 deletions docs/resources/ske_cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ resource "stackit_ske_cluster" "example" {
access_scope = "PUBLIC"
}
}
# Cluster audit log forwarding to a Telemetry Router.
# Private preview: only configurable for enabled accounts.
audit = {
enabled = true
}
}
```

Expand All @@ -60,6 +65,7 @@ To keep your Terraform plans clean and readable, always append new node pools to
### Optional

- `access` (Attributes) Configure access to the cluster (see [below for nested schema](#nestedatt--access))
- `audit` (Attributes) Cluster audit log forwarding configuration. (see [below for nested schema](#nestedatt--audit))
- `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))
- `kubernetes_version_min` (String) The minimum Kubernetes version. This field will be used to set the minimum kubernetes version on creation/update of the cluster. If unset, the latest supported Kubernetes version will be used. SKE automatically updates the cluster Kubernetes version if you have set `maintenance.enable_kubernetes_version_updates` to true or if there is a mandatory update, as described in [General information for Kubernetes & OS updates](https://docs.stackit.cloud/products/runtime/kubernetes-engine/basics/version-updates/). To get the current kubernetes version being used for your cluster, use the read-only `kubernetes_version_used` field.
Expand Down Expand Up @@ -135,6 +141,14 @@ Optional:



<a id="nestedatt--audit"></a>
### Nested Schema for `audit`

Required:

- `enabled` (Boolean) Enable cluster audit log forwarding to a Telemetry Router.


<a id="nestedatt--extensions"></a>
### Nested Schema for `extensions`

Expand Down
5 changes: 5 additions & 0 deletions examples/resources/stackit_ske_cluster/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,9 @@ resource "stackit_ske_cluster" "example" {
access_scope = "PUBLIC"
}
}
# Cluster audit log forwarding to a Telemetry Router.
# Private preview: only configurable for enabled accounts.
audit = {
enabled = true
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ require (
github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.20.0
github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.7.0
github.com/stackitcloud/stackit-sdk-go/services/sfs v0.11.0
github.com/stackitcloud/stackit-sdk-go/services/ske v1.18.0
github.com/stackitcloud/stackit-sdk-go/services/ske v1.19.0
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.16.0
github.com/stackitcloud/stackit-sdk-go/services/telemetrylink v0.2.0
github.com/stackitcloud/stackit-sdk-go/services/telemetryrouter v0.3.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -732,8 +732,8 @@ github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.7.0 h1:TNZH
github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.7.0/go.mod h1:fXq3TmVLb4JMSve989NFFViMFoYa83s7M3hJWgN6mdQ=
github.com/stackitcloud/stackit-sdk-go/services/sfs v0.11.0 h1:YhQ2GF+AyHVUps1iEjLj7z6OlzBzMGyD5nwHaZ0pXus=
github.com/stackitcloud/stackit-sdk-go/services/sfs v0.11.0/go.mod h1:jMlBoXqrPNX5nXbo6oT7exalqilw1jiLPoIp4Cn0CdI=
github.com/stackitcloud/stackit-sdk-go/services/ske v1.18.0 h1:6Umom7V12bVa7D3dnMJAKj/GWg7wqR9FpqmZ8TjsO1Q=
github.com/stackitcloud/stackit-sdk-go/services/ske v1.18.0/go.mod h1:TbqmZhLMofmfl+HhVl6oHYcI3zvXTm1vRjN3A/fOkM4=
github.com/stackitcloud/stackit-sdk-go/services/ske v1.19.0 h1:/SNc1zedje1HNhMCWf+IRx2eKpFXeGdmCRCIZ4PCrhs=
github.com/stackitcloud/stackit-sdk-go/services/ske v1.19.0/go.mod h1:TbqmZhLMofmfl+HhVl6oHYcI3zvXTm1vRjN3A/fOkM4=
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.16.0 h1:A6zN5JUz5DOTigTxAyAC+4p5VpJzaYDftx2yIH0mgn8=
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.16.0/go.mod h1:AiUoMAqQcOlMgDtkVJlqI7P/VGD5xjN3dYjERGnwN/M=
github.com/stackitcloud/stackit-sdk-go/services/telemetrylink v0.2.0 h1:U1mQoCk0TXc8NsSc/Sl9PKMdEyJpWNU2zLnsqmx6wEc=
Expand Down
10 changes: 10 additions & 0 deletions stackit/internal/services/ske/cluster/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,16 @@ func (r *clusterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest
},
},
},
"audit": schema.SingleNestedAttribute{
Description: descriptions["audit"],
Computed: true,
Attributes: map[string]schema.Attribute{
"enabled": schema.BoolAttribute{
Description: descriptions["audit_enabled"],
Computed: true,
},
},
},
"region": schema.StringAttribute{
// the region cannot be found, so it has to be passed
Optional: true,
Expand Down
71 changes: 71 additions & 0 deletions stackit/internal/services/ske/cluster/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ type Model struct {
Network types.Object `tfsdk:"network"`
Hibernations types.List `tfsdk:"hibernations"`
Extensions types.Object `tfsdk:"extensions"`
Audit types.Object `tfsdk:"audit"`
EgressAddressRanges types.List `tfsdk:"egress_address_ranges"`
PodAddressRanges types.List `tfsdk:"pod_address_ranges"`
ServiceAccountIssuer types.String `tfsdk:"service_account_issuer"`
Expand Down Expand Up @@ -270,6 +271,16 @@ var dnsTypes = map[string]attr.Type{
"zones": basetypes.ListType{ElemType: types.StringType},
}

// Struct corresponding to Model.Audit
type audit struct {
Enabled types.Bool `tfsdk:"enabled"`
}

// Types corresponding to audit
var auditTypes = map[string]attr.Type{
"enabled": basetypes.BoolType{},
}

// NewClusterResource is a helper function to simplify the provider implementation.
func NewClusterResource() resource.Resource {
return &clusterResource{}
Expand Down Expand Up @@ -412,6 +423,8 @@ var descriptions = map[string]string{
"access_idp": "Configure IDP",
"access_idp_enabled": "Enable IDP integration for the cluster.",
"access_idp_type": "The IDP type. Possible values: 'stackit'.",
"audit": "Cluster audit log forwarding configuration.",
"audit_enabled": "Enable cluster audit log forwarding to a Telemetry Router.",
}

// Schema defines the schema for the resource.
Expand Down Expand Up @@ -817,6 +830,19 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re
},
},
},
"audit": schema.SingleNestedAttribute{
Description: descriptions["audit"],
Optional: true,
PlanModifiers: []planmodifier.Object{
objectplanmodifier.UseStateForUnknown(),
},
Attributes: map[string]schema.Attribute{
"enabled": schema.BoolAttribute{
Description: descriptions["audit_enabled"],
Required: true,
},
},
},
"region": schema.StringAttribute{
Optional: true,
// must be computed to allow for storing the override value from the provider
Expand Down Expand Up @@ -1055,13 +1081,19 @@ func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag
core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating extension API payload: %v", err))
return
}
audit, err := toAuditPayload(ctx, model)
if err != nil {
core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating audit API payload: %v", err))
return
}
access, err := toAccessPayload(ctx, model)
if err != nil {
core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating access API payload: %v", err))
return
}

payload := ske.CreateOrUpdateClusterPayload{
Audit: audit,
Extensions: extensions,
Hibernation: hibernations,
Kubernetes: *kubernetes,
Expand Down Expand Up @@ -1391,6 +1423,22 @@ func toHibernationsPayload(ctx context.Context, m *Model) (*ske.Hibernation, err
}, nil
}

func toAuditPayload(ctx context.Context, m *Model) (*ske.Audit, error) {
if utils.IsUndefined(m.Audit) {
return nil, nil
}

auditModel := audit{}
diags := m.Audit.As(ctx, &auditModel, basetypes.ObjectAsOptions{})
if diags.HasError() {
return nil, fmt.Errorf("converting audit object: %v", diags.Errors())
}

return &ske.Audit{
Enabled: auditModel.Enabled.ValueBool(),
}, nil
}

func toExtensionsPayload(ctx context.Context, m *Model) (*ske.Extension, error) {
if m.Extensions.IsNull() || m.Extensions.IsUnknown() {
return nil, nil
Expand Down Expand Up @@ -1635,6 +1683,10 @@ func mapFields(ctx context.Context, cl *ske.Cluster, m *Model, region string) er
if err != nil {
return fmt.Errorf("map hibernations: %w", err)
}
err = mapAudit(cl, m)
if err != nil {
return fmt.Errorf("map audit: %w", err)
}
err = mapExtensions(ctx, cl, m)
if err != nil {
return fmt.Errorf("map extensions: %w", err)
Expand Down Expand Up @@ -1943,6 +1995,25 @@ func getMaintenanceTimes(ctx context.Context, cl *ske.Cluster, m *Model) (startT
return startTime, endTime, nil
}

func mapAudit(cl *ske.Cluster, m *Model) error {
// A missing audit block only occurs in regions where the feature is

@rubenhoenle rubenhoenle Jul 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully get this, can't the API just include false in the response in that case?

{
    ...
    "audit": {
        "enabled": false
    }
}

Else you might have users which will run into state drifts: When Terraform users set audit.enabled to false in their Terraform configuration, Terraform will also send false to the API. From my current understanding the API then just won't return the audit object at all in the response. This will lead to Terraform setting audit.enabled to null here which will then result in a state drift error.

@tobias-pfaffelmoser-ske tobias-pfaffelmoser-ske Jul 15, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

Just checked with the team: when the feature is available in the respective region, the API always echoes the audit object back, including { "enabled": false }. So setting audit.enabled = false returns enabled: false and maps cleanly back into state - so no drift expected in this scenario.

When the feature is not available in the respective region, the API omits the audit object entirely. That is the situation where cl.Audit == nil, and in that case, audit shouldn't be configured on the cluster in the first place. Mapping it to null might make sense: if a user tries to configure audit in a region without the feature, the resulting diff could be the desired signal that the configuration has no effect at all. WDYT?

// unavailable; normalize it to null there.
if cl.Audit == nil {
m.Audit = types.ObjectNull(auditTypes)
return nil
}

auditValues := map[string]attr.Value{
"enabled": types.BoolValue(cl.Audit.Enabled),
}
auditObject, diags := types.ObjectValue(auditTypes, auditValues)
if diags.HasError() {
return fmt.Errorf("creating audit object: %w", core.DiagsToError(diags))
}
m.Audit = auditObject
return nil
}

func checkDisabledExtensions(ctx context.Context, ex *extensions) (aclDisabled, observabilityDisabled, dnsDisabled bool, err error) {
var diags diag.Diagnostics
acl := acl{}
Expand Down
130 changes: 130 additions & 0 deletions stackit/internal/services/ske/cluster/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,9 @@ func TestMapFields(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
if tt.expected.Audit.Attributes() == nil {
tt.expected.Audit = types.ObjectNull(auditTypes)
}
diff := cmp.Diff(state, &tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
Expand Down Expand Up @@ -2401,6 +2404,64 @@ func TestToNetworkPayload(t *testing.T) {
}
}

func TestToAuditPayload(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input types.Object
want *ske.Audit
wantErr bool
}{
{
name: "null audit",
input: types.ObjectNull(auditTypes),
want: nil,
},
{
name: "unknown audit",
input: types.ObjectUnknown(auditTypes),
want: nil,
},
{
name: "audit enabled",
input: types.ObjectValueMust(auditTypes, map[string]attr.Value{
"enabled": types.BoolValue(true),
}),
want: &ske.Audit{
Enabled: true,
},
},
{
name: "audit disabled",
input: types.ObjectValueMust(auditTypes, map[string]attr.Value{
"enabled": types.BoolValue(false),
}),
want: &ske.Audit{
Enabled: false,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
m := &Model{
Audit: tt.input,
}
got, err := toAuditPayload(t.Context(), m)
if err != nil && !tt.wantErr {
t.Fatalf("unexpected error: %v", err)
}
if err == nil && tt.wantErr {
t.Fatalf("expected error, but got none")
}
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}
}

func TestVerifySystemComponentNodepools(t *testing.T) {
tests := []struct {
description string
Expand Down Expand Up @@ -2754,6 +2815,75 @@ func TestValidateConfig(t *testing.T) {
}
}

func TestMapAudit(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input *ske.Audit
stateAudit types.Object
want types.Object
wantErr bool
}{
{
name: "nil audit",
input: nil,
stateAudit: types.ObjectNull(auditTypes),
want: types.ObjectNull(auditTypes),
},
{
name: "audit enabled",
input: &ske.Audit{
Enabled: true,
},
stateAudit: types.ObjectNull(auditTypes),
want: types.ObjectValueMust(auditTypes, map[string]attr.Value{
"enabled": types.BoolValue(true),
}),
},
{
name: "audit disabled echoed by API",
input: &ske.Audit{
Enabled: false,
},
stateAudit: types.ObjectNull(auditTypes),
want: types.ObjectValueMust(auditTypes, map[string]attr.Value{
"enabled": types.BoolValue(false),
}),
},
{
name: "null when API omits audit despite state value",
input: nil,
stateAudit: types.ObjectValueMust(auditTypes, map[string]attr.Value{
"enabled": types.BoolValue(false),
}),
want: types.ObjectNull(auditTypes),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
m := &Model{
Audit: tt.stateAudit,
}
cluster := &ske.Cluster{
Audit: tt.input,
}

err := mapAudit(cluster, m)
if !tt.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tt.wantErr && err == nil {
t.Fatalf("expected error, but got none")
}
if diff := cmp.Diff(tt.want, m.Audit); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}
}

func TestMapAccess(t *testing.T) {
t.Parallel()
tests := []struct {
Expand Down
Loading