Skip to content
Open
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 @@ -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
}
}
```

Expand All @@ -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.
Expand Down Expand Up @@ -129,6 +135,14 @@ Optional:



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

Optional:

- `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 @@ -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
}
}
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
74 changes: 74 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,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
Expand Down Expand Up @@ -1055,13 +1084,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 +1426,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 +1686,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 +1998,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
Comment thread
rubenhoenle marked this conversation as resolved.
// 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
Loading