Skip to content
Draft
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
130 changes: 130 additions & 0 deletions api/v1beta1/disk_full.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2026 Datadog, Inc.

package v1beta1

import (
"fmt"
"strconv"
"strings"

"github.com/hashicorp/go-multierror"
"k8s.io/apimachinery/pkg/api/resource"
)

// DiskFullSpec represents a disk full (ENOSPC) disruption that fills a target volume
type DiskFullSpec struct {
// Path is the mount path inside the target pod to fill (e.g., "/data", "/var/log")
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
Path string `json:"path" chaos_validate:"required"`
// Capacity is the target fill percentage of total volume capacity (e.g., "95%").
// Mutually exclusive with Remaining.
// +kubebuilder:validation:Pattern=`^\d{1,3}%$`
Capacity string `json:"capacity,omitempty"`
// Remaining is the amount of free space to leave on the volume (e.g., "50Mi", "1Gi").
// Mutually exclusive with Capacity.
Remaining string `json:"remaining,omitempty"`
}

// Validate validates args for the given disruption
func (s *DiskFullSpec) Validate() (retErr error) {
if strings.TrimSpace(s.Path) == "" {
retErr = multierror.Append(retErr, fmt.Errorf("the path of the disk full disruption must not be empty"))
}

hasCapacity := s.Capacity != ""
hasRemaining := s.Remaining != ""

if hasCapacity && hasRemaining {
retErr = multierror.Append(retErr, fmt.Errorf("capacity and remaining are mutually exclusive, only one can be set"))
}

if !hasCapacity && !hasRemaining {
retErr = multierror.Append(retErr, fmt.Errorf("one of capacity or remaining must be set"))
}

if hasCapacity {
if err := validateCapacity(s.Capacity); err != nil {
retErr = multierror.Append(retErr, err)
}
}

if hasRemaining {
if err := validateRemaining(s.Remaining); err != nil {
retErr = multierror.Append(retErr, err)
}
}

return retErr
}

func validateCapacity(capacity string) error {
if !strings.HasSuffix(capacity, "%") {
return fmt.Errorf("capacity must be a percentage suffixed with %%, got %q", capacity)
}

valueStr := strings.TrimSuffix(capacity, "%")

value, err := strconv.Atoi(valueStr)
if err != nil {
return fmt.Errorf("capacity percentage must be an integer, got %q: %w", valueStr, err)
}

if value < 1 || value > 100 {
return fmt.Errorf("capacity percentage must be between 1 and 100, got %d", value)
}

return nil
}

func validateRemaining(remaining string) error {
qty, err := resource.ParseQuantity(remaining)
if err != nil {
return fmt.Errorf("remaining must be a valid Kubernetes resource quantity (e.g., 50Mi, 1Gi), got %q: %w", remaining, err)
}

if qty.Value() < 0 {
return fmt.Errorf("remaining must not be negative, got %q", remaining)
}

return nil
}

// GenerateArgs generates injection or cleanup pod arguments for the given spec
func (s *DiskFullSpec) GenerateArgs() []string {
args := []string{
"disk-full",
"--path",
s.Path,
}

if s.Capacity != "" {
args = append(args, "--capacity", s.Capacity)
}

if s.Remaining != "" {
args = append(args, "--remaining", s.Remaining)
}

return args
}

// Explain returns a human-readable description of the disruption
func (s *DiskFullSpec) Explain() []string {
explanation := fmt.Sprintf("spec.diskFull will fill the volume mounted at %s", s.Path)

if s.Capacity != "" {
explanation += fmt.Sprintf(" to %s of its total capacity", s.Capacity)
}

if s.Remaining != "" {
explanation += fmt.Sprintf(", leaving only %s of free space", s.Remaining)
}

explanation += ", causing ENOSPC errors on subsequent write operations."

return []string{"", explanation}
}
198 changes: 198 additions & 0 deletions api/v1beta1/disk_full_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2026 Datadog, Inc.

package v1beta1_test

import (
. "github.com/DataDog/chaos-controller/api/v1beta1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("DiskFullSpec", func() {
When("Call the 'Validate' method", func() {
DescribeTable("success cases",
func(spec DiskFullSpec) {
Expect(spec.Validate()).Should(Succeed())
},
Entry("with capacity percentage",
DiskFullSpec{
Path: "/data",
Capacity: "95%",
},
),
Entry("with capacity at 1%",
DiskFullSpec{
Path: "/data",
Capacity: "1%",
},
),
Entry("with capacity at 100%",
DiskFullSpec{
Path: "/data",
Capacity: "100%",
},
),
Entry("with remaining in Mi",
DiskFullSpec{
Path: "/data",
Remaining: "50Mi",
},
),
Entry("with remaining in Gi",
DiskFullSpec{
Path: "/var/log",
Remaining: "1Gi",
},
),
Entry("with remaining at 0",
DiskFullSpec{
Path: "/data",
Remaining: "0",
},
),
)

DescribeTable("error cases",
func(spec DiskFullSpec, expectedErrors []string) {
err := spec.Validate()
Expect(err).To(HaveOccurred())
for _, expected := range expectedErrors {
Expect(err.Error()).To(ContainSubstring(expected))
}
},
Entry("with empty path",
DiskFullSpec{
Path: "",
Capacity: "95%",
},
[]string{"the path of the disk full disruption must not be empty"},
),
Entry("with blank path",
DiskFullSpec{
Path: " ",
Capacity: "95%",
},
[]string{"the path of the disk full disruption must not be empty"},
),
Entry("with both capacity and remaining set",
DiskFullSpec{
Path: "/data",
Capacity: "95%",
Remaining: "50Mi",
},
[]string{"capacity and remaining are mutually exclusive"},
),
Entry("with neither capacity nor remaining set",
DiskFullSpec{
Path: "/data",
},
[]string{"one of capacity or remaining must be set"},
),
Entry("with capacity missing percent suffix",
DiskFullSpec{
Path: "/data",
Capacity: "95",
},
[]string{"capacity must be a percentage suffixed with %"},
),
Entry("with capacity at 0%",
DiskFullSpec{
Path: "/data",
Capacity: "0%",
},
[]string{"capacity percentage must be between 1 and 100"},
),
Entry("with capacity at 101%",
DiskFullSpec{
Path: "/data",
Capacity: "101%",
},
[]string{"capacity percentage must be between 1 and 100"},
),
Entry("with non-numeric capacity",
DiskFullSpec{
Path: "/data",
Capacity: "abc%",
},
[]string{"capacity percentage must be an integer"},
),
Entry("with invalid remaining quantity",
DiskFullSpec{
Path: "/data",
Remaining: "not-a-quantity",
},
[]string{"remaining must be a valid Kubernetes resource quantity"},
),
Entry("with negative remaining",
DiskFullSpec{
Path: "/data",
Remaining: "-1Mi",
},
[]string{"remaining must not be negative"},
),
Entry("with empty path and no capacity/remaining",
DiskFullSpec{
Path: "",
},
[]string{
"the path of the disk full disruption must not be empty",
"one of capacity or remaining must be set",
},
),
)
})

When("Call the 'GenerateArgs' method", func() {
DescribeTable("success cases",
func(spec DiskFullSpec, expectedArgs []string) {
expectedArgs = append([]string{"disk-full"}, expectedArgs...)
args := spec.GenerateArgs()
Expect(args).Should(Equal(expectedArgs))
},
Entry("with capacity",
DiskFullSpec{
Path: "/data",
Capacity: "95%",
},
[]string{"--path", "/data", "--capacity", "95%"},
),
Entry("with remaining",
DiskFullSpec{
Path: "/data",
Remaining: "50Mi",
},
[]string{"--path", "/data", "--remaining", "50Mi"},
),
)
})

When("Call the 'Explain' method", func() {
It("explains capacity mode", func() {
spec := DiskFullSpec{
Path: "/data",
Capacity: "95%",
}
explanation := spec.Explain()
Expect(explanation).To(HaveLen(2))
Expect(explanation[1]).To(ContainSubstring("/data"))
Expect(explanation[1]).To(ContainSubstring("95%"))
Expect(explanation[1]).To(ContainSubstring("ENOSPC"))
})

It("explains remaining mode", func() {
spec := DiskFullSpec{
Path: "/var/log",
Remaining: "50Mi",
}
explanation := spec.Explain()
Expect(explanation).To(HaveLen(2))
Expect(explanation[1]).To(ContainSubstring("/var/log"))
Expect(explanation[1]).To(ContainSubstring("50Mi"))
Expect(explanation[1]).To(ContainSubstring("ENOSPC"))
})

})
})
Loading
Loading