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 @@ -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 @@ -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,
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 @@ -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{}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1084,13 +1113,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 @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
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, applicationLoadBalancerDisabled bool, err error) {
var diags diag.Diagnostics
acl := acl{}
Expand Down
122 changes: 122 additions & 0 deletions stackit/internal/services/ske/cluster/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Loading
Loading