diff --git a/docs/data-sources/ske_cluster.md b/docs/data-sources/ske_cluster.md
index 2687bb5cf..e79bf2f96 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. (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 b34254d9e..e77aa329c 100644
--- a/docs/resources/ske_cluster.md
+++ b/docs/resources/ske_cluster.md
@@ -38,6 +38,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
+ }
}
```
@@ -54,6 +59,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.
@@ -129,6 +135,14 @@ Optional:
+
+### Nested Schema for `audit`
+
+Optional:
+
+- `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 35d3faac6..90f8b7fb7 100644
--- a/examples/resources/stackit_ske_cluster/resource.tf
+++ b/examples/resources/stackit_ske_cluster/resource.tf
@@ -20,4 +20,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/stackit/internal/services/ske/cluster/datasource.go b/stackit/internal/services/ske/cluster/datasource.go
index dae8717b4..c5461488f 100644
--- a/stackit/internal/services/ske/cluster/datasource.go
+++ b/stackit/internal/services/ske/cluster/datasource.go
@@ -343,6 +343,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 c2d2051ec..360d4ecdf 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"`
@@ -284,6 +285,16 @@ var dnsTypes = map[string]attr.Type{
"gateway_api": basetypes.BoolType{},
}
+// 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{}
@@ -426,6 +437,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.
@@ -846,6 +859,22 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re
},
},
},
+ "audit": schema.SingleNestedAttribute{
+ Description: descriptions["audit"],
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.Object{
+ objectplanmodifier.UseStateForUnknown(),
+ },
+ Attributes: map[string]schema.Attribute{
+ "enabled": schema.BoolAttribute{
+ Description: descriptions["audit_enabled"],
+ Optional: true,
+ Computed: true,
+ Default: booldefault.StaticBool(false),
+ },
+ },
+ },
"region": schema.StringAttribute{
Optional: true,
// must be computed to allow for storing the override value from the provider
@@ -1084,6 +1113,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))
@@ -1091,6 +1125,7 @@ func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag
}
payload := ske.CreateOrUpdateClusterPayload{
+ Audit: audit,
Extensions: extensions,
Hibernation: hibernations,
Kubernetes: *kubernetes,
@@ -1420,6 +1455,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
@@ -1680,6 +1731,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)
@@ -1988,6 +2043,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
+ // 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, applicationLoadBalancerDisabled 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 ec6ad006e..3bb452b19 100644
--- a/stackit/internal/services/ske/cluster/resource_test.go
+++ b/stackit/internal/services/ske/cluster/resource_test.go
@@ -837,6 +837,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)
@@ -2442,6 +2445,60 @@ func TestToNetworkPayload(t *testing.T) {
}
}
+func TestToAuditPayload(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ name string
+ input types.Object
+ want *ske.Audit
+ }{
+ {
+ 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 {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ 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
@@ -2817,6 +2874,71 @@ 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
+ }{
+ {
+ 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 err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ 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 {
diff --git a/stackit/internal/services/ske/ske_acc_test.go b/stackit/internal/services/ske/ske_acc_test.go
index b898f9df6..2ac4d7bec 100644
--- a/stackit/internal/services/ske/ske_acc_test.go
+++ b/stackit/internal/services/ske/ske_acc_test.go
@@ -96,6 +96,7 @@ var testConfigVarsMax = config.Variables{
"dns_name": config.StringVariable("acc-" + acctest.RandStringFromCharSet(6, acctest.CharSetAlpha) + ".runs.onstackit.cloud"),
"network_control_plane_access_scope": config.StringVariable("PUBLIC"),
"access_idp_enabled": config.BoolVariable(true),
+ "audit_enabled": config.BoolVariable(true),
}
var testConfigDatasource = config.Variables{
@@ -116,6 +117,7 @@ func configVarsMaxUpdated() config.Variables {
updatedConfig["maintenance_end"] = config.StringVariable("03:03:03+00:00")
updatedConfig["access_idp_enabled"] = config.BoolVariable(false)
updatedConfig["ext_application_load_balancer_enabled"] = config.BoolVariable(false)
+ updatedConfig["audit_enabled"] = config.BoolVariable(false)
return updatedConfig
}
@@ -165,6 +167,9 @@ func TestAccSKEMin(t *testing.T) {
// Access: resource-min does not define an access block, we expect idp: { enabled: false, type: stackit } here because of the default
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.enabled", "false"),
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.type", "stackit"),
+
+ // Audit: resource-min does not define an audit block, we expect enabled: false here because of the default
+ resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "audit.enabled", "false"),
),
},
// 2) Data source
@@ -194,6 +199,7 @@ func TestAccSKEMin(t *testing.T) {
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.end", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_end"])),
resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "region"),
resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "network.control_plane.access_scope", testutil.ConvertConfigVariable(testConfigVarsMin["network_control_plane_access_scope"])),
+ resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "audit.enabled", "false"),
),
},
// 3) Import cluster
@@ -250,6 +256,7 @@ func TestAccSKEMin(t *testing.T) {
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "region", testutil.ConvertConfigVariable(configVarsMinUpdated()["region"])),
resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "kubernetes_version_used"),
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "network.control_plane.access_scope", testutil.ConvertConfigVariable(configVarsMinUpdated()["network_control_plane_access_scope"])),
+ resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "audit.enabled", "false"),
// Kubeconfig
resource.TestCheckResourceAttrPair(
@@ -338,6 +345,9 @@ func TestAccSKEMax(t *testing.T) {
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.enabled", testutil.ConvertConfigVariable(testConfigVarsMax["access_idp_enabled"])),
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.type", "stackit"),
+ // Audit
+ resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "audit.enabled", testutil.ConvertConfigVariable(testConfigVarsMax["audit_enabled"])),
+
// Kubeconfig
resource.TestCheckResourceAttrPair(
"stackit_ske_kubeconfig.kubeconfig", "project_id",
@@ -417,6 +427,9 @@ func TestAccSKEMax(t *testing.T) {
// Access
resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "access.idp.enabled", testutil.ConvertConfigVariable(testConfigVarsMax["access_idp_enabled"])),
resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "access.idp.type", "stackit"),
+
+ // Audit
+ resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "audit.enabled", testutil.ConvertConfigVariable(testConfigVarsMax["audit_enabled"])),
),
},
// 3) Import cluster
@@ -510,6 +523,9 @@ func TestAccSKEMax(t *testing.T) {
// Access
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.enabled", testutil.ConvertConfigVariable(configVarsMaxUpdated()["access_idp_enabled"])),
resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.type", "stackit"),
+
+ // Audit: updated from true to false
+ resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "audit.enabled", testutil.ConvertConfigVariable(configVarsMaxUpdated()["audit_enabled"])),
),
},
// Deletion is done by the framework implicitly
diff --git a/stackit/internal/services/ske/testdata/resource-max.tf b/stackit/internal/services/ske/testdata/resource-max.tf
index 54ac080a7..07f36a471 100644
--- a/stackit/internal/services/ske/testdata/resource-max.tf
+++ b/stackit/internal/services/ske/testdata/resource-max.tf
@@ -39,6 +39,7 @@ variable "dns_zone_name" {}
variable "dns_name" {}
variable "network_control_plane_access_scope" {}
variable "access_idp_enabled" {}
+variable "audit_enabled" {}
resource "stackit_ske_cluster" "cluster" {
project_id = var.project_id
@@ -111,6 +112,9 @@ resource "stackit_ske_cluster" "cluster" {
type = "stackit"
}
}
+ audit = {
+ enabled = var.audit_enabled
+ }
}
resource "stackit_ske_kubeconfig" "kubeconfig" {