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
42 changes: 41 additions & 1 deletion api/v1beta1/disk_pressure.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,34 @@ type DiskPressureSpec struct {

// DiskPressureThrottlingSpec represents a throttle on read and write disk operations
type DiskPressureThrottlingSpec struct {
ReadBytesPerSec *int `json:"readBytesPerSec,omitempty"`
// +kubebuilder:validation:Minimum=0
ReadBytesPerSec *int `json:"readBytesPerSec,omitempty"`
// +kubebuilder:validation:Minimum=0
WriteBytesPerSec *int `json:"writeBytesPerSec,omitempty"`
// +kubebuilder:validation:Minimum=0
ReadIOPSPerSec *int `json:"readIOPSPerSec,omitempty"`
// +kubebuilder:validation:Minimum=0
WriteIOPSPerSec *int `json:"writeIOPSPerSec,omitempty"`
}

// Validate validates args for the given disruption
func (s *DiskPressureSpec) Validate() error {
// a negative throttle value makes no sense and is rejected by the cgroup write.
// zero is allowed and means "no throttle" (it removes the limit on cgroups v1 and
// maps to "max" on cgroups v2). Reject negatives at the API level to fail fast.
throttles := map[string]*int{
"readBytesPerSec": s.Throttling.ReadBytesPerSec,
"writeBytesPerSec": s.Throttling.WriteBytesPerSec,
"readIOPSPerSec": s.Throttling.ReadIOPSPerSec,
"writeIOPSPerSec": s.Throttling.WriteIOPSPerSec,
}

for name, value := range throttles {
if value != nil && *value < 0 {
return fmt.Errorf("disk pressure throttling %s must be greater than or equal to 0, got %d", name, *value)
}
}

return nil
}

Expand All @@ -47,6 +69,16 @@ func (s *DiskPressureSpec) GenerateArgs() []string {
args = append(args, []string{"--write-bytes-per-sec", strconv.Itoa(*s.Throttling.WriteBytesPerSec)}...)
}

// add read iops throttling flag if specified
if s.Throttling.ReadIOPSPerSec != nil {
args = append(args, []string{"--read-iops-per-sec", strconv.Itoa(*s.Throttling.ReadIOPSPerSec)}...)
}

// add write iops throttling flag if specified
if s.Throttling.WriteIOPSPerSec != nil {
args = append(args, []string{"--write-iops-per-sec", strconv.Itoa(*s.Throttling.WriteIOPSPerSec)}...)
}

return args
}

Expand All @@ -61,5 +93,13 @@ func (s *DiskPressureSpec) Explain() []string {
explanation += fmt.Sprintf("%d write bytes per second.", *s.Throttling.WriteBytesPerSec)
}

if s.Throttling.ReadIOPSPerSec != nil {
explanation += fmt.Sprintf("%d read io per second ", *s.Throttling.ReadIOPSPerSec)
}

if s.Throttling.WriteIOPSPerSec != nil {
explanation += fmt.Sprintf("%d write io per second.", *s.Throttling.WriteIOPSPerSec)
}

return []string{"", explanation}
}
154 changes: 154 additions & 0 deletions api/v1beta1/disk_pressure_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// 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("DiskPressureSpec", func() {
intPtr := func(i int) *int { return &i }

When("Call the 'GenerateArgs' method", func() {
DescribeTable("argument generation",
func(spec DiskPressureSpec, expected []string) {
Expect(spec.GenerateArgs()).To(Equal(expected))
},
Entry("with only bandwidth throttling (back-compat)",
DiskPressureSpec{
Path: "/mnt/data",
Throttling: DiskPressureThrottlingSpec{
ReadBytesPerSec: intPtr(1024),
WriteBytesPerSec: intPtr(4096),
},
},
[]string{
"disk-pressure", "--path", "/mnt/data",
"--read-bytes-per-sec", "1024",
"--write-bytes-per-sec", "4096",
},
),
Entry("with only read iops throttling",
DiskPressureSpec{
Path: "/mnt/data",
Throttling: DiskPressureThrottlingSpec{
ReadIOPSPerSec: intPtr(50),
},
},
[]string{
"disk-pressure", "--path", "/mnt/data",
"--read-iops-per-sec", "50",
},
),
Entry("with only write iops throttling",
DiskPressureSpec{
Path: "/mnt/data",
Throttling: DiskPressureThrottlingSpec{
WriteIOPSPerSec: intPtr(75),
},
},
[]string{
"disk-pressure", "--path", "/mnt/data",
"--write-iops-per-sec", "75",
},
),
Entry("with bandwidth and iops throttling combined",
DiskPressureSpec{
Path: "/mnt/data",
Throttling: DiskPressureThrottlingSpec{
ReadBytesPerSec: intPtr(1024),
WriteBytesPerSec: intPtr(4096),
ReadIOPSPerSec: intPtr(50),
WriteIOPSPerSec: intPtr(75),
},
},
[]string{
"disk-pressure", "--path", "/mnt/data",
"--read-bytes-per-sec", "1024",
"--write-bytes-per-sec", "4096",
"--read-iops-per-sec", "50",
"--write-iops-per-sec", "75",
},
),
Entry("with no throttling set",
DiskPressureSpec{Path: "/mnt/data"},
[]string{"disk-pressure", "--path", "/mnt/data"},
),
)
})

When("Call the 'Validate' method", func() {
It("accepts a spec with no throttling set", func() {
spec := DiskPressureSpec{Path: "/mnt/data"}
Expect(spec.Validate()).To(Succeed())
})

It("accepts positive throttle values", func() {
spec := DiskPressureSpec{
Path: "/mnt/data",
Throttling: DiskPressureThrottlingSpec{
ReadBytesPerSec: intPtr(1024),
ReadIOPSPerSec: intPtr(50),
},
}
Expect(spec.Validate()).To(Succeed())
})

It("accepts a zero throttle value (no-op, removes the limit)", func() {
spec := DiskPressureSpec{
Path: "/mnt/data",
Throttling: DiskPressureThrottlingSpec{
ReadBytesPerSec: intPtr(0),
WriteIOPSPerSec: intPtr(0),
},
}
Expect(spec.Validate()).To(Succeed())
})

DescribeTable("rejects negative throttle values",
func(spec DiskPressureSpec, field string) {
err := spec.Validate()
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring(field))
},
Entry("negative read bytes",
DiskPressureSpec{Path: "/mnt/data", Throttling: DiskPressureThrottlingSpec{ReadBytesPerSec: intPtr(-1)}},
"readBytesPerSec",
),
Entry("negative write bytes",
DiskPressureSpec{Path: "/mnt/data", Throttling: DiskPressureThrottlingSpec{WriteBytesPerSec: intPtr(-1)}},
"writeBytesPerSec",
),
Entry("negative read iops",
DiskPressureSpec{Path: "/mnt/data", Throttling: DiskPressureThrottlingSpec{ReadIOPSPerSec: intPtr(-1)}},
"readIOPSPerSec",
),
Entry("negative write iops",
DiskPressureSpec{Path: "/mnt/data", Throttling: DiskPressureThrottlingSpec{WriteIOPSPerSec: intPtr(-5)}},
"writeIOPSPerSec",
),
)
})

When("Call the 'Explain' method", func() {
It("mentions iops throttling when set", func() {
spec := DiskPressureSpec{
Path: "/mnt/data",
Throttling: DiskPressureThrottlingSpec{
ReadIOPSPerSec: intPtr(50),
WriteIOPSPerSec: intPtr(75),
},
}

explanation := spec.Explain()

Expect(explanation).To(ContainElement(ContainSubstring("50 read io per second")))
Expect(explanation).To(ContainElement(ContainSubstring("75 write io per second")))
})
})
})
10 changes: 10 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,16 @@ spec:
description: DiskPressureThrottlingSpec represents a throttle on read and write disk operations
properties:
readBytesPerSec:
minimum: 0
type: integer
readIOPSPerSec:
minimum: 0
type: integer
writeBytesPerSec:
minimum: 0
type: integer
writeIOPSPerSec:
minimum: 0
type: integer
type: object
required:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,16 @@ spec:
description: DiskPressureThrottlingSpec represents a throttle on read and write disk operations
properties:
readBytesPerSec:
minimum: 0
type: integer
readIOPSPerSec:
minimum: 0
type: integer
writeBytesPerSec:
minimum: 0
type: integer
writeIOPSPerSec:
minimum: 0
type: integer
type: object
required:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,16 @@ spec:
description: DiskPressureThrottlingSpec represents a throttle on read and write disk operations
properties:
readBytesPerSec:
minimum: 0
type: integer
readIOPSPerSec:
minimum: 0
type: integer
writeBytesPerSec:
minimum: 0
type: integer
writeIOPSPerSec:
minimum: 0
type: integer
type: object
required:
Expand Down
5 changes: 2 additions & 3 deletions cli/chaosli/chaosli.DOCKERFILE
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# ---------------------------------------
FROM amd64/golang:1.20-alpine as build
FROM golang:1.25-alpine as build

ENV GOARCH=amd64
ENV CGO_ENABLED=0
WORKDIR /app

Expand All @@ -16,7 +15,7 @@ RUN go build \
./cli/chaosli

# ---------------------------------------
FROM amd64/golang:1.20-alpine as bin
FROM golang:1.25-alpine as bin

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
Expand Down
10 changes: 10 additions & 0 deletions cli/chaosli/cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,16 @@ func getDiskPressure() *v1beta1.DiskPressureSpec {
spec.Throttling.WriteBytesPerSec = &writeBPS
}

if confirmOption("Would you like to apply read iops throttling?", "This applies read-based IO operations throttling (check the docs)") {
readIOPS, _ := strconv.Atoi(getInput("Specify the target amount of throttling, in io operations per second.", "check the docs", survey.WithValidator(integerValidator)))
spec.Throttling.ReadIOPSPerSec = &readIOPS
}

if confirmOption("Would you like to apply write iops throttling?", "This applies write-based IO operations throttling (check the docs)") {
writeIOPS, _ := strconv.Atoi(getInput("Specify the target amount of throttling, in io operations per second.", "check the docs", survey.WithValidator(integerValidator)))
spec.Throttling.WriteIOPSPerSec = &writeIOPS
}

return spec
}

Expand Down
16 changes: 16 additions & 0 deletions cli/injector/disk_pressure.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ var diskPressureCmd = &cobra.Command{
path, _ := cmd.Flags().GetString("path")
writeBytesPerSec, _ := cmd.Flags().GetInt("write-bytes-per-sec")
readBytesPerSec, _ := cmd.Flags().GetInt("read-bytes-per-sec")
writeIOPSPerSec, _ := cmd.Flags().GetInt("write-iops-per-sec")
readIOPSPerSec, _ := cmd.Flags().GetInt("read-iops-per-sec")

// prepare spec
var writeBytesPerSecP *int
Expand All @@ -37,11 +39,23 @@ var diskPressureCmd = &cobra.Command{
readBytesPerSecP = &readBytesPerSec
}

var writeIOPSPerSecP *int
if writeIOPSPerSec != 0 {
writeIOPSPerSecP = &writeIOPSPerSec
}

var readIOPSPerSecP *int
if readIOPSPerSec != 0 {
readIOPSPerSecP = &readIOPSPerSec
}

spec := v1beta1.DiskPressureSpec{
Path: path,
Throttling: v1beta1.DiskPressureThrottlingSpec{
ReadBytesPerSec: readBytesPerSecP,
WriteBytesPerSec: writeBytesPerSecP,
ReadIOPSPerSec: readIOPSPerSecP,
WriteIOPSPerSec: writeIOPSPerSecP,
},
}

Expand Down Expand Up @@ -72,6 +86,8 @@ func init() {
diskPressureCmd.Flags().String("path", "", "Path to apply/clean disk pressure to/from (will be applied to the whole disk)")
diskPressureCmd.Flags().Int("write-bytes-per-sec", 0, "Bytes per second throttling limit")
diskPressureCmd.Flags().Int("read-bytes-per-sec", 0, "Bytes per second throttling limit")
diskPressureCmd.Flags().Int("write-iops-per-sec", 0, "IO operations per second throttling limit")
diskPressureCmd.Flags().Int("read-iops-per-sec", 0, "IO operations per second throttling limit")

_ = cobra.MarkFlagRequired(diskPressureCmd.PersistentFlags(), "path")
}
Loading