Skip to content
Merged
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
38 changes: 35 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,53 @@ on:
permissions:
contents: read
jobs:
unit-tests:
runs-on: ubuntu-latest
name: Unit tests 🧪
steps:
- name: Checkout 🧲
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup Go ⬇️
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true

- name: Run unit tests 🧪
run: make test

fuzz:
runs-on: ubuntu-latest
name: Fuzz tests 🎲
steps:
- name: Checkout 🧲
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup Go ⬇️
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true

- name: Fuzz the pod mutation logic 🎲
run: go test -run '^$' -fuzz=FuzzMutateContainers -fuzztime=60s ./pkg/overcommit

e2e:
runs-on: ubuntu-latest
name: E2E tests 🔃
steps:
- name: Checkout 🧲
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Restore go build cache ⏬
uses: actions/cache@v6
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/go-build
key: ${{ runner.os }}-go-build-v1

- name: Setup Go ⬇️
uses: actions/setup-go@v7
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: 1.26.8

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
if: >-
github.event_name == 'schedule' ||
(
vars.DEVELOPMENT_FLOW == 'trunk-based-development' &&
(vars.DEVELOPMENT_FLOW == '' || vars.DEVELOPMENT_FLOW == 'trunk-based-development') &&
(
(github.event_name == 'push' && (github.ref_name == 'main' || startsWith(github.ref_name, 'main-'))) ||
(github.event_name == 'pull_request' && (github.event.pull_request.base.ref == 'main' || startsWith(github.event.pull_request.base.ref, 'main-'))) ||
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build the manager binary
FROM golang:1.27.2-alpine3.23 AS builder
FROM golang:1.27.2-alpine3.23@sha256:9e45f0eb4a63ed37ad6e604950407558b7c738e34e015ae77a5d4ad369cde079 AS builder

Check warning on line 2 in Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use either the version tag or the digest for the image instead of both.

See more on https://sonarcloud.io/project/issues?id=InditexTech_k8s-overcommit-operator&issues=AaGmwGQXV4s19tLS3jEa&open=AaGmwGQXV4s19tLS3jEa&pullRequest=113
ARG TARGETOS
ARG TARGETARCH

Expand Down Expand Up @@ -27,7 +27,7 @@

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
FROM gcr.io/distroless/static:nonroot@sha256:25ac8f0c3b2334a9eb05452266bd4697864a8c8ef0ef71e5e6480d7f92868329

Check warning on line 30 in Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use either the version tag or the digest for the image instead of both.

See more on https://sonarcloud.io/project/issues?id=InditexTech_k8s-overcommit-operator&issues=AaGmwGQXV4s19tLS3jEb&open=AaGmwGQXV4s19tLS3jEb&pullRequest=113
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532
Expand Down
12 changes: 6 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,14 @@ require (
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/mod v0.41.0 // indirect
golang.org/x/net v0.60.0 // indirect
golang.org/x/net v0.62.0 // indirect
golang.org/x/oauth2 v0.37.0 // indirect
golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/term v0.46.0 // indirect
golang.org/x/text v0.42.0 // indirect
golang.org/x/sync v0.24.0 // indirect
golang.org/x/sys v0.49.0 // indirect
golang.org/x/term v0.47.0 // indirect
golang.org/x/text v0.43.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.49.0 // indirect
golang.org/x/tools v0.51.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect
Expand Down
24 changes: 12 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -201,22 +201,22 @@ golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJk
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
Expand Down
2 changes: 1 addition & 1 deletion hack/tilt/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

# hack/tilt/Dockerfile

FROM gcr.io/distroless/static:nonroot
FROM gcr.io/distroless/static:nonroot@sha256:25ac8f0c3b2334a9eb05452266bd4697864a8c8ef0ef71e5e6480d7f92868329

Check warning on line 8 in hack/tilt/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use either the version tag or the digest for the image instead of both.

See more on https://sonarcloud.io/project/issues?id=InditexTech_k8s-overcommit-operator&issues=AaGmwGQVV4s19tLS3jEZ&open=AaGmwGQVV4s19tLS3jEZ&pullRequest=113
WORKDIR /
COPY hack/tilt/bin/manager manager

Expand Down
127 changes: 127 additions & 0 deletions pkg/overcommit/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: 2025 2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.)
// SPDX-FileContributor: enriqueavi@inditex.com
//
// SPDX-License-Identifier: Apache-2.0

package overcommit

import (
"reflect"
"testing"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)

// validRatio reports whether r is a ratio the OvercommitClass CRD accepts.
// cpuOvercommit and memoryOvercommit are validated to the [0.0001, 1] range.
func validRatio(r float64) bool {
return r >= 0.0001 && r <= 1
}

// fuzzInput is one generated case for the mutation target.
type fuzzInput struct {
cpuMilli int64
memBytes int64
cpuRatio float64
memRatio float64
withLimits bool
withRequests bool
}

// inDomain keeps the target inside the values the API and the CRD admit:
// ratios in [0.0001, 1] and limits small enough that the float64 -> int64
// conversions in mutateContainers cannot overflow.
func (in fuzzInput) inDomain() bool {
return validRatio(in.cpuRatio) && validRatio(in.memRatio) &&
in.cpuMilli >= 0 && in.cpuMilli <= 1_000_000_000 &&
in.memBytes >= 0 && in.memBytes <= 1<<50
}

// container builds the input container and the limits it declares.
func (in fuzzInput) container() (corev1.Container, corev1.ResourceList) {
limits := corev1.ResourceList{
corev1.ResourceCPU: *resource.NewMilliQuantity(in.cpuMilli, resource.DecimalSI),
corev1.ResourceMemory: *resource.NewQuantity(in.memBytes, resource.BinarySI),
}

c := corev1.Container{Name: "fuzzed"}
if in.withLimits {
c.Resources.Limits = limits
}
if in.withRequests {
c.Resources.Requests = corev1.ResourceList{
corev1.ResourceCPU: *resource.NewMilliQuantity(1, resource.DecimalSI),
corev1.ResourceMemory: *resource.NewQuantity(1, resource.BinarySI),
}
}
return c, limits
}

// FuzzMutateContainers exercises the request rewriting done by the mutating
// webhook: it must never panic and must keep every generated request inside
// [0, limit], for any input the API admits.
func FuzzMutateContainers(f *testing.F) {
for _, seed := range []fuzzInput{
{1000, 1 << 30, 0.0001, 0.0001, true, true},
{1000, 1 << 30, 1.0, 1.0, true, true},
{500, 256 << 20, 0.5, 0.8, true, false},
{0, 0, 0.5, 0.5, true, true},
{32000, 512 << 20, 1.0, 0.0001, true, true},
{2000, 64 << 20, 0.5, 0.5, false, true},
} {
f.Add(seed.cpuMilli, seed.memBytes, seed.cpuRatio, seed.memRatio, seed.withLimits, seed.withRequests)
}

f.Fuzz(func(t *testing.T, cpuMilli, memBytes int64, cpuRatio, memRatio float64, withLimits, withRequests bool) {
in := fuzzInput{cpuMilli, memBytes, cpuRatio, memRatio, withLimits, withRequests}
if !in.inDomain() {
t.Skip()
}

original, limits := in.container()
containers := []corev1.Container{original}
mutateContainers(containers, in.cpuRatio, in.memRatio)

assertRequests(t, containers[0].Resources.Requests, original.Resources.Requests, limits, in)
})
}

// assertRequests checks the invariants of mutateContainers on a single container.
func assertRequests(t *testing.T, got, before, limits corev1.ResourceList, in fuzzInput) {
t.Helper()

if !in.withLimits {
// Without limits there is nothing to overcommit: requests must not move.
if !reflect.DeepEqual(got, before) {
t.Fatalf("requests changed without limits: got %v, want %v", got, before)
}
return
}

assertResource(t, corev1.ResourceCPU, got, before, limits[corev1.ResourceCPU], in.cpuRatio)
assertResource(t, corev1.ResourceMemory, got, before, limits[corev1.ResourceMemory], in.memRatio)
}

// assertResource checks one resource: a ratio of 1 is a no-op, any other ratio
// produces a request in [0, limit].
func assertResource(t *testing.T, key corev1.ResourceName, got, before corev1.ResourceList, limit resource.Quantity, ratio float64) {
t.Helper()

req, present := got[key]
prev, hadPrev := before[key]

if ratio == 1 {
if hadPrev && (!present || prev.Cmp(req) != 0) {
t.Fatalf("%s request changed for ratio 1: got %v, want %v", key, req.String(), prev.String())
}
return
}

if !present {
t.Fatalf("%s request missing for ratio %v", key, ratio)
}
if req.Sign() < 0 || req.Cmp(limit) > 0 {
t.Fatalf("%s request out of bounds: got %s, limit %s, ratio %v", key, req.String(), limit.String(), ratio)
}
}
Loading