diff --git a/docs/data-sources/ske_cluster.md b/docs/data-sources/ske_cluster.md
index c0ead7d66..d414f0616 100644
--- a/docs/data-sources/ske_cluster.md
+++ b/docs/data-sources/ske_cluster.md
@@ -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. This feature is in private preview. Enabling audit logging is only possible for enabled accounts. (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))
@@ -63,6 +64,14 @@ Read-Only:
+
+### Nested Schema for `audit`
+
+Read-Only:
+
+- `enabled` (Boolean) Enable cluster audit log forwarding to a Telemetry Router.
+
+
### Nested Schema for `extensions`
diff --git a/docs/resources/ske_cluster.md b/docs/resources/ske_cluster.md
index 876ed8007..0d82d49e8 100644
--- a/docs/resources/ske_cluster.md
+++ b/docs/resources/ske_cluster.md
@@ -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
+ }
}
```
@@ -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. This feature is in private preview. Enabling audit logging is only possible for enabled accounts. (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.
@@ -135,6 +141,14 @@ Optional:
+
+### Nested Schema for `audit`
+
+Required:
+
+- `enabled` (Boolean) Enable cluster audit log forwarding to a Telemetry Router.
+
+
### Nested Schema for `extensions`
diff --git a/examples/resources/stackit_ske_cluster/resource.tf b/examples/resources/stackit_ske_cluster/resource.tf
index 36f980f06..3ea78ce0a 100644
--- a/examples/resources/stackit_ske_cluster/resource.tf
+++ b/examples/resources/stackit_ske_cluster/resource.tf
@@ -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
+ }
}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 942312854..ecaae741d 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index bca7caf55..7e6a9a93a 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/stackit/internal/services/ske/cluster/datasource.go b/stackit/internal/services/ske/cluster/datasource.go
index bd3013815..6d00cd996 100644
--- a/stackit/internal/services/ske/cluster/datasource.go
+++ b/stackit/internal/services/ske/cluster/datasource.go
@@ -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,
diff --git a/stackit/internal/services/ske/cluster/resource.go b/stackit/internal/services/ske/cluster/resource.go
index d6ffb4b4c..729807551 100644
--- a/stackit/internal/services/ske/cluster/resource.go
+++ b/stackit/internal/services/ske/cluster/resource.go
@@ -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"`
@@ -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{}
@@ -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. This feature is in private preview. Enabling audit logging is only possible for enabled accounts.",
+ "audit_enabled": "Enable cluster audit log forwarding to a Telemetry Router.",
}
// Schema defines the schema for the resource.
@@ -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
@@ -1055,6 +1081,11 @@ 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))
@@ -1062,6 +1093,7 @@ func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag
}
payload := ske.CreateOrUpdateClusterPayload{
+ Audit: audit,
Extensions: extensions,
Hibernation: hibernations,
Kubernetes: *kubernetes,
@@ -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
@@ -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(ctx, 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)
@@ -1943,6 +1995,47 @@ func getMaintenanceTimes(ctx context.Context, cl *ske.Cluster, m *Model) (startT
return startTime, endTime, nil
}
+func mapAudit(ctx context.Context, cl *ske.Cluster, m *Model) error {
+ // The API may omit the audit block entirely (e.g. when audit logging is not
+ // enabled or not available for the account). In that case we must decide what
+ // to keep in the Terraform state without producing a spurious plan diff:
+ if cl.Audit == nil {
+ // No audit configured on either side (or the value is still unknown): normalize to null.
+ if m.Audit.IsNull() || m.Audit.IsUnknown() || m.Audit.Attributes() == nil {
+ m.Audit = types.ObjectNull(auditTypes)
+ return nil
+ }
+
+ auditModel := audit{}
+ diags := m.Audit.As(ctx, &auditModel, basetypes.ObjectAsOptions{})
+ if diags.HasError() {
+ return fmt.Errorf("converting audit object: %v", diags.Errors())
+ }
+
+ // The user explicitly configured `audit = { enabled = false }`. The API does
+ // not echo a disabled audit config back, so we keep the configured value to
+ // avoid a permanent diff between config and state.
+ if !auditModel.Enabled.IsUnknown() && !auditModel.Enabled.ValueBool() {
+ return nil
+ }
+
+ // Otherwise (configured enabled=true, but API returned nothing) fall back to
+ // null so the next plan reflects the actual API state.
+ 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{}
diff --git a/stackit/internal/services/ske/cluster/resource_test.go b/stackit/internal/services/ske/cluster/resource_test.go
index f277522ac..3aecb705b 100644
--- a/stackit/internal/services/ske/cluster/resource_test.go
+++ b/stackit/internal/services/ske/cluster/resource_test.go
@@ -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)
@@ -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
@@ -2754,6 +2815,67 @@ 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: "disabled audit kept when API omits audit",
+ input: nil,
+ stateAudit: types.ObjectValueMust(auditTypes, map[string]attr.Value{
+ "enabled": types.BoolValue(false),
+ }),
+ want: types.ObjectValueMust(auditTypes, map[string]attr.Value{
+ "enabled": types.BoolValue(false),
+ }),
+ },
+ }
+
+ 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(t.Context(), 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 {