Compare commits

..

3 Commits

Author SHA1 Message Date
Dario Tranchitella
a5bfbaaf72 feat!: cidr validation via cel (#1095)
* fix(docs): container probes

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>

* feat(api): cidr validation using cel

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>

* refactor: cidr validation is offloaded to cel

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>

* feat(test): integration test for cidr and ip cel functions

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>

* docs: bumping up minimum management cluster version

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>

---------

Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>
2026-03-02 14:30:10 +01:00
Dario Tranchitella
adaaef0857 chore(goreleaser): prerelease must be false (#1096)
Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>
2026-03-02 09:48:58 +01:00
Dario Tranchitella
7ad75e8216 chore(release)!: switch to goreleaser and migrating tag format (#1094)
Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>
2026-03-01 21:53:41 +01:00
20 changed files with 2823 additions and 304 deletions

View File

@@ -1,10 +0,0 @@
This edge release can be pulled from Docker Hub as follows:
```
docker pull clastix/kamaji:$TAG
```
> As from the v1.0.0 release, CLASTIX no longer provides stable release artefacts.
>
> Stable release artefacts are offered on a subscription basis by CLASTIX, the main Kamaji project contributor.
> Learn more from CLASTIX's [Support](https://clastix.io/support/) section.

View File

@@ -2,17 +2,8 @@ name: Container image build
on:
push:
tags:
- edge-*
- v*
branches:
- master
workflow_dispatch:
inputs:
tag:
description: "Tag to build"
required: true
type: string
jobs:
ko:
@@ -21,17 +12,19 @@ jobs:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: "ko: install"
run: make ko
- name: "ko: login to quay.io container registry"
run: ./bin/ko login quay.io -u ${{ secrets.QUAY_IO_USERNAME }} -p ${{ secrets.QUAY_IO_TOKEN }}
- name: "ko: login to docker.io container registry"
run: ./bin/ko login docker.io -u ${{ secrets.DOCKER_IO_USERNAME }} -p ${{ secrets.DOCKER_IO_TOKEN }}
- name: "ko: build and push tag"
run: make VERSION=${{ github.event.inputs.tag }} KO_LOCAL=false KO_PUSH=true build
if: github.event_name == 'workflow_dispatch'
- name: "ko: build and push latest"
run: make VERSION=latest KO_LOCAL=false KO_PUSH=true build

View File

@@ -15,8 +15,9 @@ jobs:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: generating date metadata
id: date
- name: "tag: compute"
id: git
run: |
CURRENT_DATE=$(date -u +'%Y-%m-%d')
YY=$(date -u +'%y')
@@ -24,52 +25,36 @@ jobs:
FIRST_OF_MONTH=$(date -u -d "$CURRENT_DATE" +%Y-%m-01)
WEEK_NUM=$(( (($(date -u +%s) - $(date -u -d "$FIRST_OF_MONTH" +%s)) / 86400 + $(date -u -d "$FIRST_OF_MONTH" +%u) - 1) / 7 + 1 ))
echo "yy=$YY" >> $GITHUB_OUTPUT
echo "month=$M" >> $GITHUB_OUTPUT
echo "week=$WEEK_NUM" >> $GITHUB_OUTPUT
echo "date=$CURRENT_DATE" >> $GITHUB_OUTPUT
- name: generating tag metadata
id: tag
run: |
TAG="edge-${{ steps.date.outputs.yy }}.${{ steps.date.outputs.month }}.${{ steps.date.outputs.week }}"
TAG="$YY.$M.$WEEK_NUM-edge"
echo "tag=$TAG" >> $GITHUB_OUTPUT
- name: generate release notes from template
- name: "tag: push"
run: |
export TAG="${{ steps.tag.outputs.tag }}"
envsubst < .github/release-template.md > release-notes.md
- name: generate release notes from template
run: |
export TAG="${{ steps.tag.outputs.tag }}"
envsubst < .github/release-template.md > release-notes-header.md
- name: generate GitHub release notes
git tag ${{ steps.git.outputs.tag }}
git push origin ${{ steps.git.outputs.tag }}
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: "deps: installing ko"
run: make ko
- name: "ko: login to quay.io container registry"
run: ./bin/ko login quay.io -u ${{ secrets.QUAY_IO_USERNAME }} -p ${{ secrets.QUAY_IO_TOKEN }}
- name: "ko: login to docker.io container registry"
run: ./bin/ko login docker.io -u ${{ secrets.DOCKER_IO_USERNAME }} -p ${{ secrets.DOCKER_IO_TOKEN }}
- name: "path: expanding with local binaries"
run: echo "${{ github.workspace }}/bin" >> $GITHUB_PATH
- name: "goreleaser: release"
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: "~> v2"
args: release --clean
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release --repo "$GITHUB_REPOSITORY" \
create "${{ steps.tag.outputs.tag }}" \
--generate-notes \
--draft \
--title "temp" \
--notes "temp" > /dev/null || true
gh release view "${{ steps.tag.outputs.tag }}" \
--json body --jq .body > auto-notes.md
gh release delete "${{ steps.tag.outputs.tag }}" --yes || true
- name: combine notes
run: |
cat release-notes-header.md auto-notes.md > release-notes.md
- name: create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${{ steps.tag.outputs.tag }}" \
--title "${{ steps.tag.outputs.tag }}" \
--notes-file release-notes.md
- name: trigger container build workflow
env:
GH_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
run: |
gh workflow run "Container image build" \
--ref master \
-f tag="${{ steps.tag.outputs.tag }}"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@@ -38,3 +38,4 @@ bin
!deploy/kine/mysql/server-csr.json
!deploy/kine/nats/server-csr.json
charts/kamaji/charts
dist

91
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,91 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
project_name: kamaji
builds:
- id: kamaji
main: .
binary: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}"
env:
- CGO_ENABLED=0
flags:
- -trimpath
mod_timestamp: '{{ .CommitTimestamp }}'
ldflags:
- "-X github.com/clastix/kamaji/internal.GitCommit={{.Commit}}"
- "-X github.com/clastix/kamaji/internal.GitTag={{.Tag}}"
- "-X github.com/clastix/kamaji/internal.GitDirty={{ if eq .GitTreeState \"dirty\" }}.dev{{ end }}"
- "-X github.com/clastix/kamaji/internal.BuildTime={{.Date}}"
- "-X github.com/clastix/kamaji/internal.GitRepo={{ .GitURL }}"
goos:
- linux
goarch:
- amd64
- arm
- arm64
kos:
- repositories:
- docker.io/clastix/kamaji
- quay.io/clastix/kamaji
tags:
- "{{ .Tag }}"
bare: true
preserve_import_paths: false
platforms:
- linux/amd64
- linux/arm64
- linux/arm
release:
footer: |
**Container Images**
```
docker pull clastix/{{ .ProjectName }}:{{ .Tag }}
```
> This is an **edge release** and is intended for testing and evaluation purposes only.
> It may include experimental features and does not provide the stability guarantees of a production-ready build.
>
> **Stable release artefacts** are available on a subscription basis from CLASTIX,
> the primary contributor to the Kamaji project.
>
> For production-grade releases and enterprise support,
> please refer to CLASTIX's [Support](https://clastix.io/support/) offerings.
**Full Changelog**: https://github.com/clastix/{{ .ProjectName }}/compare/{{ .PreviousTag }}...{{ .Tag }}
changelog:
sort: asc
use: github
filters:
exclude:
- 'merge conflict'
- Merge pull request
- Merge remote-tracking branch
- Merge branch
groups:
- title: '🛠 Dependency updates'
regexp: '^.*?(feat|fix)\(deps\)!?:.+$'
order: 300
- title: '✨ New Features'
regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$'
order: 100
- title: '🐛 Bug fixes'
regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$'
order: 200
- title: '📖 Documentation updates'
regexp: ^.*?docs(\([[:word:]]+\))??!?:.+$
order: 400
- title: '🛡️ Security updates'
regexp: ^.*?(sec)(\([[:word:]]+\))??!?:.+$
order: 500
- title: '🚀 Build process updates'
regexp: ^.*?(build|ci)(\([[:word:]]+\))??!?:.+$
order: 600
- title: '📦 Other work'
order: 9999
checksum:
name_template: "checksums.txt"

View File

@@ -47,6 +47,9 @@ GOLANGCI_LINT ?= $(LOCALBIN)/golangci-lint
HELM ?= $(LOCALBIN)/helm
KIND ?= $(LOCALBIN)/kind
KO ?= $(LOCALBIN)/ko
GORELEASER ?= $(LOCALBIN)/goreleaser
COSIGN ?= $(LOCALBIN)/cosign
SYFT ?= $(LOCALBIN)/syft
YQ ?= $(LOCALBIN)/yq
ENVTEST ?= $(LOCALBIN)/setup-envtest
@@ -81,6 +84,21 @@ docs: ## Serve documentation locally with Docker.
##@ Binary
.PHONY: cosign
cosign: $(COSIGN) ## Download cosign locally if necessary.
$(COSIGN): $(LOCALBIN)
test -s $(LOCALBIN)/cosign || GOBIN=$(LOCALBIN) go install github.com/sigstore/cosign/v3/cmd/cosign@v3.0.5
.PHONY: syft
syft: $(SYFT) ## Download syft locally if necessary.
$(SYFT): $(LOCALBIN)
test -s $(LOCALBIN)/syft || GOBIN=$(LOCALBIN) go install github.com/anchore/syft/cmd/syft@v1.42.1
.PHONY: goreleaser
goreleaser: $(GORELEASER) ## Download goreleaser locally if necessary.
$(GORELEASER): $(LOCALBIN)
test -s $(LOCALBIN)/goreleaser || GOBIN=$(LOCALBIN) go install github.com/goreleaser/goreleaser/v2@v2.14.1
.PHONY: ko
ko: $(KO) ## Download ko locally if necessary.
$(KO): $(LOCALBIN)

View File

@@ -0,0 +1,260 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var _ = Describe("NetworkProfile validation", func() {
var (
ctx context.Context
tcp *TenantControlPlane
)
const (
ipv6CIDRBlock = "fd00::/108"
)
BeforeEach(func() {
ctx = context.Background()
tcp = &TenantControlPlane{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "tcp-network-",
Namespace: "default",
},
Spec: TenantControlPlaneSpec{
ControlPlane: ControlPlane{
Service: ServiceSpec{
ServiceType: ServiceTypeClusterIP,
},
},
},
}
})
AfterEach(func() {
// When creation is denied by validation, GenerateName is never resolved
// and tcp.Name remains empty, so there is nothing to delete.
if tcp.Name == "" {
return
}
if err := k8sClient.Delete(ctx, tcp); err != nil && !apierrors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
})
Context("serviceCidr", func() {
It("allows creation with the default IPv4 CIDR", func() {
tcp.Spec.NetworkProfile.ServiceCIDR = "10.96.0.0/16"
Expect(k8sClient.Create(ctx, tcp)).To(Succeed())
})
It("allows creation with a non-default valid IPv4 CIDR", func() {
tcp.Spec.NetworkProfile.ServiceCIDR = "172.16.0.0/12"
Expect(k8sClient.Create(ctx, tcp)).To(Succeed())
})
It("allows creation with a valid IPv6 CIDR", func() {
tcp.Spec.NetworkProfile.ServiceCIDR = ipv6CIDRBlock
Expect(k8sClient.Create(ctx, tcp)).To(Succeed())
})
It("allows creation when serviceCidr is empty", func() {
tcp.Spec.NetworkProfile.ServiceCIDR = ""
Expect(k8sClient.Create(ctx, tcp)).To(Succeed())
})
It("denies creation with a plain IP address instead of a CIDR", func() {
tcp.Spec.NetworkProfile.ServiceCIDR = "10.96.0.1"
err := k8sClient.Create(ctx, tcp)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("serviceCidr must be empty or a valid CIDR"))
})
It("denies creation with an arbitrary non-CIDR string", func() {
tcp.Spec.NetworkProfile.ServiceCIDR = "not-a-cidr"
err := k8sClient.Create(ctx, tcp)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("serviceCidr must be empty or a valid CIDR"))
})
})
Context("podCidr", func() {
It("allows creation with the default IPv4 CIDR", func() {
tcp.Spec.NetworkProfile.PodCIDR = "10.244.0.0/16"
Expect(k8sClient.Create(ctx, tcp)).To(Succeed())
})
It("allows creation with a non-default valid IPv4 CIDR", func() {
tcp.Spec.NetworkProfile.PodCIDR = "192.168.128.0/17"
Expect(k8sClient.Create(ctx, tcp)).To(Succeed())
})
It("allows creation with a valid IPv6 CIDR", func() {
tcp.Spec.NetworkProfile.PodCIDR = "2001:db8::/48"
Expect(k8sClient.Create(ctx, tcp)).To(Succeed())
})
It("allows creation when podCidr is empty", func() {
tcp.Spec.NetworkProfile.PodCIDR = ""
Expect(k8sClient.Create(ctx, tcp)).To(Succeed())
})
It("denies creation with a plain IP address instead of a CIDR", func() {
tcp.Spec.NetworkProfile.PodCIDR = "10.244.0.1"
err := k8sClient.Create(ctx, tcp)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("podCidr must be empty or a valid CIDR"))
})
It("denies creation with an arbitrary non-CIDR string", func() {
tcp.Spec.NetworkProfile.PodCIDR = "not-a-cidr"
err := k8sClient.Create(ctx, tcp)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("podCidr must be empty or a valid CIDR"))
})
})
Context("loadBalancerSourceRanges CIDR format", func() {
BeforeEach(func() {
tcp.Spec.ControlPlane.Service.ServiceType = ServiceTypeLoadBalancer
})
It("allows creation with a single valid CIDR", func() {
tcp.Spec.NetworkProfile.LoadBalancerSourceRanges = []string{"10.0.0.0/8"}
Expect(k8sClient.Create(ctx, tcp)).To(Succeed())
})
It("allows creation with multiple valid CIDRs", func() {
tcp.Spec.NetworkProfile.LoadBalancerSourceRanges = []string{
"10.0.0.0/8",
"192.168.0.0/24",
"172.16.0.0/12",
}
Expect(k8sClient.Create(ctx, tcp)).To(Succeed())
})
It("allows creation with valid IPv6 CIDRs", func() {
tcp.Spec.NetworkProfile.LoadBalancerSourceRanges = []string{
"2001:db8::/32",
"fd00::/8",
}
Expect(k8sClient.Create(ctx, tcp)).To(Succeed())
})
It("denies creation when an entry is a plain IP address", func() {
tcp.Spec.NetworkProfile.LoadBalancerSourceRanges = []string{"192.168.1.1"}
err := k8sClient.Create(ctx, tcp)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("all LoadBalancer source range entries must be valid CIDR"))
})
It("denies creation when an entry is an arbitrary string", func() {
tcp.Spec.NetworkProfile.LoadBalancerSourceRanges = []string{"not-a-cidr"}
err := k8sClient.Create(ctx, tcp)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("all LoadBalancer source range entries must be valid CIDR"))
})
It("denies creation when at least one entry in a mixed list is invalid", func() {
tcp.Spec.NetworkProfile.LoadBalancerSourceRanges = []string{
"10.0.0.0/8",
"not-a-cidr",
}
err := k8sClient.Create(ctx, tcp)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("all LoadBalancer source range entries must be valid CIDR"))
})
})
Context("dnsServiceIPs", func() {
BeforeEach(func() {
tcp.Spec.NetworkProfile.ServiceCIDR = "10.96.0.0/16"
})
It("allows creation when dnsServiceIPs is not set", func() {
Expect(k8sClient.Create(ctx, tcp)).To(Succeed())
})
It("allows creation with an explicitly empty dnsServiceIPs list", func() {
tcp.Spec.NetworkProfile.DNSServiceIPs = []string{}
Expect(k8sClient.Create(ctx, tcp)).To(Succeed())
})
It("allows creation when all IPs are within the service CIDR", func() {
tcp.Spec.NetworkProfile.DNSServiceIPs = []string{"10.96.0.10"}
Expect(k8sClient.Create(ctx, tcp)).To(Succeed())
})
It("allows creation with multiple IPs all within the service CIDR", func() {
tcp.Spec.NetworkProfile.DNSServiceIPs = []string{
"10.96.0.10",
"10.96.0.11",
}
Expect(k8sClient.Create(ctx, tcp)).To(Succeed())
})
It("denies creation when a DNS service IP is outside the service CIDR", func() {
tcp.Spec.NetworkProfile.DNSServiceIPs = []string{"192.168.1.10"}
err := k8sClient.Create(ctx, tcp)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("all DNS service IPs must be part of the Service CIDR"))
})
It("denies creation when at least one IP in a mixed list is outside the service CIDR", func() {
tcp.Spec.NetworkProfile.DNSServiceIPs = []string{
"10.96.0.10",
"192.168.1.10",
}
err := k8sClient.Create(ctx, tcp)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("all DNS service IPs must be part of the Service CIDR"))
})
It("allows creation with an IPv6 DNS service IP within an IPv6 service CIDR", func() {
tcp.Spec.NetworkProfile.ServiceCIDR = ipv6CIDRBlock
tcp.Spec.NetworkProfile.DNSServiceIPs = []string{"fd00::10"}
Expect(k8sClient.Create(ctx, tcp)).To(Succeed())
})
It("denies creation when an IPv6 DNS service IP is outside the IPv6 service CIDR", func() {
tcp.Spec.NetworkProfile.ServiceCIDR = ipv6CIDRBlock
tcp.Spec.NetworkProfile.DNSServiceIPs = []string{"2001:db8::10"}
err := k8sClient.Create(ctx, tcp)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("all DNS service IPs must be part of the Service CIDR"))
})
})
})

View File

@@ -12,6 +12,7 @@ import (
)
// NetworkProfileSpec defines the desired state of NetworkProfile.
// +kubebuilder:validation:XValidation:rule="!has(self.dnsServiceIPs) || self.dnsServiceIPs.all(r, cidr(self.serviceCidr).containsIP(r))",message="all DNS service IPs must be part of the Service CIDR"
type NetworkProfileSpec struct {
// LoadBalancerSourceRanges restricts the IP ranges that can access
// the LoadBalancer type Service. This field defines a list of IP
@@ -20,14 +21,16 @@ type NetworkProfileSpec struct {
// This feature is useful for restricting access to API servers or services
// to specific networks for security purposes.
// Example: {"192.168.1.0/24", "10.0.0.0/8"}
//+kubebuilder:validation:MaxItems=16
//+kubebuilder:validation:XValidation:rule="self.all(r, isCIDR(r))",message="all LoadBalancer source range entries must be valid CIDR"
LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"`
// Specify the LoadBalancer class in case of multiple load balancer implementations.
// Field supported only for Tenant Control Plane instances exposed using a LoadBalancer Service.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="LoadBalancerClass is immutable"
LoadBalancerClass *string `json:"loadBalancerClass,omitempty"`
// Address where API server of will be exposed.
// In case of LoadBalancer Service, this can be empty in order to use the exposed IP provided by the cloud controller manager.
// Address where API server will be exposed.
// In the case of LoadBalancer Service, this can be empty in order to use the exposed IP provided by the cloud controller manager.
Address string `json:"address,omitempty"`
// The default domain name used for DNS resolution within the cluster.
//+kubebuilder:default="cluster.local"
@@ -37,7 +40,7 @@ type NetworkProfileSpec struct {
// AllowAddressAsExternalIP will include tenantControlPlane.Spec.NetworkProfile.Address in the section of
// ExternalIPs of the Kubernetes Service (only ClusterIP or NodePort)
AllowAddressAsExternalIP bool `json:"allowAddressAsExternalIP,omitempty"`
// Port where API server of will be exposed
// Port where API server will be exposed
//+kubebuilder:default=6443
Port int32 `json:"port,omitempty"`
// CertSANs sets extra Subject Alternative Names (SANs) for the API Server signing certificate.
@@ -45,14 +48,20 @@ type NetworkProfileSpec struct {
CertSANs []string `json:"certSANs,omitempty"`
// CIDR for Kubernetes Services: if empty, defaulted to 10.96.0.0/16.
//+kubebuilder:default="10.96.0.0/16"
//+kubebuilder:validation:Optional
//+kubebuilder:validation:XValidation:rule="self == '' || isCIDR(self)",message="serviceCidr must be empty or a valid CIDR"
ServiceCIDR string `json:"serviceCidr,omitempty"`
// CIDR for Kubernetes Pods: if empty, defaulted to 10.244.0.0/16.
//+kubebuilder:default="10.244.0.0/16"
//+kubebuilder:validation:Optional
//+kubebuilder:validation:XValidation:rule="self == '' || isCIDR(self)",message="podCidr must be empty or a valid CIDR"
PodCIDR string `json:"podCidr,omitempty"`
// The DNS Service for internal resolution, it must match the Service CIDR.
// In case of an empty value, it is automatically computed according to the Service CIDR, e.g.:
// Service CIDR 10.96.0.0/16, the resulting DNS Service IP will be 10.96.0.10 for IPv4,
// for IPv6 from the CIDR 2001:db8:abcd::/64 the resulting DNS Service IP will be 2001:db8:abcd::10.
//+kubebuilder:validation:MaxItems=8
//+kubebuilder:validation:Optional
DNSServiceIPs []string `json:"dnsServiceIPs,omitempty"`
}

View File

@@ -9,7 +9,8 @@ package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
apisv1 "sigs.k8s.io/gateway-api/apis/v1"
)
@@ -653,6 +654,13 @@ func (in *DataStoreStatus) DeepCopyInto(out *DataStoreStatus) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataStoreStatus.
@@ -957,7 +965,7 @@ func (in *JSONPatch) DeepCopyInto(out *JSONPatch) {
*out = *in
if in.Value != nil {
in, out := &in.Value, &out.Value
*out = new(v1.JSON)
*out = new(apiextensionsv1.JSON)
(*in).DeepCopyInto(*out)
}
}

View File

@@ -7576,8 +7576,8 @@ versions:
properties:
address:
description: |-
Address where API server of will be exposed.
In case of LoadBalancer Service, this can be empty in order to use the exposed IP provided by the cloud controller manager.
Address where API server will be exposed.
In the case of LoadBalancer Service, this can be empty in order to use the exposed IP provided by the cloud controller manager.
type: string
allowAddressAsExternalIP:
description: |-
@@ -7607,6 +7607,7 @@ versions:
for IPv6 from the CIDR 2001:db8:abcd::/64 the resulting DNS Service IP will be 2001:db8:abcd::10.
items:
type: string
maxItems: 8
type: array
loadBalancerClass:
description: |-
@@ -7628,21 +7629,34 @@ versions:
Example: {"192.168.1.0/24", "10.0.0.0/8"}
items:
type: string
maxItems: 16
type: array
x-kubernetes-validations:
- message: all LoadBalancer source range entries must be valid CIDR
rule: self.all(r, isCIDR(r))
podCidr:
default: 10.244.0.0/16
description: 'CIDR for Kubernetes Pods: if empty, defaulted to 10.244.0.0/16.'
type: string
x-kubernetes-validations:
- message: podCidr must be empty or a valid CIDR
rule: self == '' || isCIDR(self)
port:
default: 6443
description: Port where API server of will be exposed
description: Port where API server will be exposed
format: int32
type: integer
serviceCidr:
default: 10.96.0.0/16
description: 'CIDR for Kubernetes Services: if empty, defaulted to 10.96.0.0/16.'
type: string
x-kubernetes-validations:
- message: serviceCidr must be empty or a valid CIDR
rule: self == '' || isCIDR(self)
type: object
x-kubernetes-validations:
- message: all DNS service IPs must be part of the Service CIDR
rule: '!has(self.dnsServiceIPs) || self.dnsServiceIPs.all(r, cidr(self.serviceCidr).containsIP(r))'
writePermissions:
description: |-
WritePermissions allows to select which operations (create, delete, update) must be blocked:

View File

@@ -7584,8 +7584,8 @@ spec:
properties:
address:
description: |-
Address where API server of will be exposed.
In case of LoadBalancer Service, this can be empty in order to use the exposed IP provided by the cloud controller manager.
Address where API server will be exposed.
In the case of LoadBalancer Service, this can be empty in order to use the exposed IP provided by the cloud controller manager.
type: string
allowAddressAsExternalIP:
description: |-
@@ -7615,6 +7615,7 @@ spec:
for IPv6 from the CIDR 2001:db8:abcd::/64 the resulting DNS Service IP will be 2001:db8:abcd::10.
items:
type: string
maxItems: 8
type: array
loadBalancerClass:
description: |-
@@ -7636,21 +7637,34 @@ spec:
Example: {"192.168.1.0/24", "10.0.0.0/8"}
items:
type: string
maxItems: 16
type: array
x-kubernetes-validations:
- message: all LoadBalancer source range entries must be valid CIDR
rule: self.all(r, isCIDR(r))
podCidr:
default: 10.244.0.0/16
description: 'CIDR for Kubernetes Pods: if empty, defaulted to 10.244.0.0/16.'
type: string
x-kubernetes-validations:
- message: podCidr must be empty or a valid CIDR
rule: self == '' || isCIDR(self)
port:
default: 6443
description: Port where API server of will be exposed
description: Port where API server will be exposed
format: int32
type: integer
serviceCidr:
default: 10.96.0.0/16
description: 'CIDR for Kubernetes Services: if empty, defaulted to 10.96.0.0/16.'
type: string
x-kubernetes-validations:
- message: serviceCidr must be empty or a valid CIDR
rule: self == '' || isCIDR(self)
type: object
x-kubernetes-validations:
- message: all DNS service IPs must be part of the Service CIDR
rule: '!has(self.dnsServiceIPs) || self.dnsServiceIPs.all(r, cidr(self.serviceCidr).containsIP(r))'
writePermissions:
description: |-
WritePermissions allows to select which operations (create, delete, update) must be blocked:

View File

@@ -255,8 +255,6 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
Scheme: *mgr.GetScheme(),
},
},
handlers.TenantControlPlaneServiceCIDR{},
handlers.TenantControlPlaneLoadBalancerSourceRanges{},
handlers.TenantControlPlaneGatewayValidation{
Client: mgr.GetClient(),
DiscoveryClient: discoveryClient,

View File

@@ -40,7 +40,7 @@ Throughout the following instructions, shell variables are used to indicate valu
source kamaji.env
```
Any regular and conformant Kubernetes v1.22+ cluster can be turned into a Kamaji setup. To work properly, the Management Cluster should provide:
Any regular and conformant Kubernetes v1.33+ cluster can be turned into a Kamaji setup. To work properly, the Management Cluster should provide:
- CNI module installed, eg. [Calico](https://github.com/projectcalico/calico), [Cilium](https://github.com/cilium/cilium).
- CSI module installed with a Storage Class for the Tenant datastores. The [Local Path Provisioner](https://github.com/rancher/local-path-provisioner) is a suggested choice, even for production environments.

File diff suppressed because it is too large Load Diff

View File

@@ -18,10 +18,13 @@ Usage of the said artefacts is not suggested for production use-case due to miss
### Edge Releases
Edge Release artifacts are published on a monthly basis as part of the open source project.
Versioning follows the form `edge-{year}.{month}.{incremental}` where incremental refers to the monthly release.
For example, `edge-24.7.1` is the first edge release shipped in July 2024.
Versioning follows the form `{year}.{month}.{incremental}-edge` where incremental refers to the monthly release.
For example, `26.3.1-edge` is the first edge release shipped in March 2027.
The full list of edge release artifacts can be found on the Kamaji's GitHub [releases page](https://github.com/clastix/kamaji/releases).
> _Nota Bene_: all edge releases prior to March 2026 used a different pattern (`edge-{year}.{month}.{incremental}`):
> this change has been required to take advantage of GoReleaser to start our support for CRA compliance.
Edge Release artifacts contain the code in from the main branch at the point in time when they were cut.
This means they always have the latest features and fixes, and have undergone automated testing as well as maintainer code review.
Edge Releases may involve partial features that are later modified or backed out.
@@ -31,7 +34,7 @@ Edge Releases are generally considered production ready and the project will mar
| Kamaji | Management Cluster | Tenant Cluster |
|-------------|--------------------|----------------------|
| edge-25.4.1 | v1.22+ | [v1.30.0 .. v1.33.0] |
| 26.3.2-edge | v1.33+ | [v1.30.0 .. v1.35.0] |
Using Edge Release artifacts and reporting bugs helps us ensure a rapid pace of development and is a great way to help maintainers.

2
go.mod
View File

@@ -35,7 +35,7 @@ require (
k8s.io/cluster-bootstrap v0.0.0
k8s.io/klog/v2 v2.130.1
k8s.io/kubelet v0.0.0
k8s.io/kubernetes v1.35.2
k8s.io/kubernetes v1.35.1
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
sigs.k8s.io/controller-runtime v0.22.4
sigs.k8s.io/gateway-api v1.4.1

4
go.sum
View File

@@ -549,8 +549,8 @@ k8s.io/kube-proxy v0.35.0 h1:erv2wYmGZ6nyu/FtmaIb+ORD3q2rfZ4Fhn7VXs/8cPQ=
k8s.io/kube-proxy v0.35.0/go.mod h1:bd9lpN3uLLOOWc/CFZbkPEi9DTkzQQymbE8FqSU4bWk=
k8s.io/kubelet v0.35.0 h1:8cgJHCBCKLYuuQ7/Pxb/qWbJfX1LXIw7790ce9xHq7c=
k8s.io/kubelet v0.35.0/go.mod h1:ciRzAXn7C4z5iB7FhG1L2CGPPXLTVCABDlbXt/Zz8YA=
k8s.io/kubernetes v1.35.2 h1:2HthVDfK3YJYv624imuKXPzUJ17xQop9OT5dgT+IMKE=
k8s.io/kubernetes v1.35.2/go.mod h1:AaPpCpiS8oAqRbEwpY5r3RitLpwpVp5lVXKFkJril58=
k8s.io/kubernetes v1.35.1 h1:qmjXSCDPnOuXPuJb5pv+eLzpXhhlD09Jid1pG/OvFU8=
k8s.io/kubernetes v1.35.1/go.mod h1:AaPpCpiS8oAqRbEwpY5r3RitLpwpVp5lVXKFkJril58=
k8s.io/system-validators v1.12.1 h1:AY1+COTLJN/Sj0w9QzH1H0yvyF3Kl6CguMnh32WlcUU=
k8s.io/system-validators v1.12.1/go.mod h1:awfSS706v9R12VC7u7K89FKfqVy44G+E0L1A0FX9Wmw=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=

View File

@@ -1,58 +0,0 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package handlers
import (
"context"
"fmt"
"net"
"gomodules.xyz/jsonpatch/v2"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/webhook/utils"
)
type TenantControlPlaneLoadBalancerSourceRanges struct{}
func (t TenantControlPlaneLoadBalancerSourceRanges) handle(tcp *kamajiv1alpha1.TenantControlPlane) error {
for _, sourceCIDR := range tcp.Spec.NetworkProfile.LoadBalancerSourceRanges {
_, _, err := net.ParseCIDR(sourceCIDR)
if err != nil {
return fmt.Errorf("invalid LoadBalancer source CIDR %s, %s", sourceCIDR, err.Error())
}
}
return nil
}
func (t TenantControlPlaneLoadBalancerSourceRanges) OnCreate(object runtime.Object) AdmissionResponse {
return func(context.Context, admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
tcp := object.(*kamajiv1alpha1.TenantControlPlane) //nolint:forcetypeassert
if err := t.handle(tcp); err != nil {
return nil, err
}
return nil, nil
}
}
func (t TenantControlPlaneLoadBalancerSourceRanges) OnDelete(runtime.Object) AdmissionResponse {
return utils.NilOp()
}
func (t TenantControlPlaneLoadBalancerSourceRanges) OnUpdate(object runtime.Object, _ runtime.Object) AdmissionResponse {
return func(context.Context, admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
tcp := object.(*kamajiv1alpha1.TenantControlPlane) //nolint:forcetypeassert
if err := t.handle(tcp); err != nil {
return nil, err
}
return nil, nil
}
}

View File

@@ -1,64 +0,0 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package handlers_test
import (
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/webhook/handlers"
)
var _ = Describe("TCP LoadBalancer Source Ranges Webhook", func() {
var (
ctx context.Context
t handlers.TenantControlPlaneLoadBalancerSourceRanges
tcp *kamajiv1alpha1.TenantControlPlane
)
BeforeEach(func() {
t = handlers.TenantControlPlaneLoadBalancerSourceRanges{}
tcp = &kamajiv1alpha1.TenantControlPlane{
ObjectMeta: metav1.ObjectMeta{
Name: "tcp",
Namespace: "default",
},
Spec: kamajiv1alpha1.TenantControlPlaneSpec{},
}
ctx = context.Background()
})
It("allows creation when valid CIDR ranges are provided", func() {
tcp.Spec.ControlPlane.Service.ServiceType = kamajiv1alpha1.ServiceTypeLoadBalancer
tcp.Spec.NetworkProfile.LoadBalancerSourceRanges = []string{"192.168.0.0/24"}
_, err := t.OnCreate(tcp)(ctx, admission.Request{})
Expect(err).ToNot(HaveOccurred())
})
It("allows creation when LoadBalancer service has no CIDR field", func() {
tcp.Spec.ControlPlane.Service.ServiceType = kamajiv1alpha1.ServiceTypeLoadBalancer
_, err := t.OnCreate(tcp)(ctx, admission.Request{})
Expect(err).ToNot(HaveOccurred())
})
It("allows creation when LoadBalancer service has an empty CIDR list", func() {
tcp.Spec.ControlPlane.Service.ServiceType = kamajiv1alpha1.ServiceTypeLoadBalancer
tcp.Spec.NetworkProfile.LoadBalancerSourceRanges = []string{}
_, err := t.OnCreate(tcp)(ctx, admission.Request{})
Expect(err).ToNot(HaveOccurred())
})
It("denies creation when source ranges contain invalid CIDRs", func() {
tcp.Spec.ControlPlane.Service.ServiceType = kamajiv1alpha1.ServiceTypeLoadBalancer
tcp.Spec.NetworkProfile.LoadBalancerSourceRanges = []string{"192.168.0.0/33"}
_, err := t.OnCreate(tcp)(ctx, admission.Request{})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("invalid LoadBalancer source CIDR 192.168.0.0/33"))
})
})

View File

@@ -1,71 +0,0 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package handlers
import (
"context"
"fmt"
"net"
"gomodules.xyz/jsonpatch/v2"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/webhook/utils"
)
type TenantControlPlaneServiceCIDR struct{}
func (t TenantControlPlaneServiceCIDR) handle(tcp *kamajiv1alpha1.TenantControlPlane) error {
if tcp.Spec.Addons.CoreDNS == nil {
return nil
}
_, cidr, err := net.ParseCIDR(tcp.Spec.NetworkProfile.ServiceCIDR)
if err != nil {
return fmt.Errorf("unable to parse Service CIDR, %s", err.Error())
}
for _, serviceIP := range tcp.Spec.NetworkProfile.DNSServiceIPs {
ip := net.ParseIP(serviceIP)
if ip == nil {
return fmt.Errorf("unable to parse IP address %s", serviceIP)
}
if !cidr.Contains(ip) {
return fmt.Errorf("the Service CIDR does not contain the DNS Service IP %s", serviceIP)
}
}
return nil
}
func (t TenantControlPlaneServiceCIDR) OnCreate(object runtime.Object) AdmissionResponse {
return func(context.Context, admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
tcp := object.(*kamajiv1alpha1.TenantControlPlane) //nolint:forcetypeassert
if err := t.handle(tcp); err != nil {
return nil, err
}
return nil, nil
}
}
func (t TenantControlPlaneServiceCIDR) OnDelete(runtime.Object) AdmissionResponse {
return utils.NilOp()
}
func (t TenantControlPlaneServiceCIDR) OnUpdate(object runtime.Object, _ runtime.Object) AdmissionResponse {
return func(context.Context, admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
tcp := object.(*kamajiv1alpha1.TenantControlPlane) //nolint:forcetypeassert
if err := t.handle(tcp); err != nil {
return nil, err
}
return nil, nil
}
}