mirror of
https://github.com/clastix/kamaji.git
synced 2026-03-17 17:10:49 +00:00
Compare commits
6 Commits
26.3.2-edg
...
26.3.4-edg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d2444e646 | ||
|
|
cedd0f642c | ||
|
|
e4da581e69 | ||
|
|
6ed71b1e3e | ||
|
|
a5bfbaaf72 | ||
|
|
adaaef0857 |
@@ -39,7 +39,6 @@ kos:
|
||||
- linux/arm
|
||||
|
||||
release:
|
||||
prerelease: auto
|
||||
footer: |
|
||||
**Container Images**
|
||||
```
|
||||
|
||||
260
api/v1alpha1/networkprofile_types_test.go
Normal file
260
api/v1alpha1/networkprofile_types_test.go
Normal 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"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -267,25 +267,24 @@ func getKubernetesStorageResources(c client.Client, dbConnection datastore.Conne
|
||||
func getKubernetesAdditionalStorageResources(c client.Client, dbConnections map[string]datastore.Connection, dataStoreOverrides []builder.DataStoreOverrides, threshold time.Duration) []resources.Resource {
|
||||
res := make([]resources.Resource, 0, len(dataStoreOverrides))
|
||||
for _, dso := range dataStoreOverrides {
|
||||
datastore := dso.DataStore
|
||||
res = append(res,
|
||||
&ds.MultiTenancy{
|
||||
DataStore: datastore,
|
||||
DataStore: dso.DataStore,
|
||||
},
|
||||
&ds.Config{
|
||||
Client: c,
|
||||
ConnString: dbConnections[dso.Resource].GetConnectionString(),
|
||||
DataStore: datastore,
|
||||
DataStore: dso.DataStore,
|
||||
IsOverride: true,
|
||||
},
|
||||
&ds.Setup{
|
||||
Client: c,
|
||||
Connection: dbConnections[dso.Resource],
|
||||
DataStore: datastore,
|
||||
DataStore: dso.DataStore,
|
||||
},
|
||||
&ds.Certificate{
|
||||
Client: c,
|
||||
DataStore: datastore,
|
||||
DataStore: dso.DataStore,
|
||||
CertExpirationThreshold: threshold,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -34,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.
|
||||
|
||||
126
e2e/tcp_postgres_datastore_config_secret_test.go
Normal file
126
e2e/tcp_postgres_datastore_config_secret_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/client-go/util/retry"
|
||||
pointer "k8s.io/utils/ptr"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
)
|
||||
|
||||
var _ = Describe("When the datastore-config Secret is corrupted for a PostgreSQL-backed TenantControlPlane", func() {
|
||||
tcp := &kamajiv1alpha1.TenantControlPlane{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "postgresql-secret-regeneration",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: kamajiv1alpha1.TenantControlPlaneSpec{
|
||||
DataStore: "postgresql-bronze",
|
||||
ControlPlane: kamajiv1alpha1.ControlPlane{
|
||||
Deployment: kamajiv1alpha1.DeploymentSpec{
|
||||
Replicas: pointer.To(int32(1)),
|
||||
},
|
||||
Service: kamajiv1alpha1.ServiceSpec{
|
||||
ServiceType: "ClusterIP",
|
||||
},
|
||||
},
|
||||
Kubernetes: kamajiv1alpha1.KubernetesSpec{
|
||||
Version: "v1.23.6",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
Expect(k8sClient.Create(context.Background(), tcp)).NotTo(HaveOccurred())
|
||||
StatusMustEqualTo(tcp, kamajiv1alpha1.VersionReady)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.Background(), tcp)).Should(Succeed())
|
||||
})
|
||||
|
||||
It("Should regenerate the Secret and restart the TCP pods successfully", func() {
|
||||
By("recording the UIDs of the currently running TenantControlPlane pods")
|
||||
initialPodUIDs := sets.New[types.UID]()
|
||||
Eventually(func() int {
|
||||
podList := &corev1.PodList{}
|
||||
if err := k8sClient.List(context.Background(), podList,
|
||||
client.InNamespace(tcp.GetNamespace()),
|
||||
client.MatchingLabels{"kamaji.clastix.io/name": tcp.GetName()},
|
||||
); err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
initialPodUIDs.Clear()
|
||||
for _, pod := range podList.Items {
|
||||
initialPodUIDs.Insert(pod.GetUID())
|
||||
}
|
||||
|
||||
return initialPodUIDs.Len()
|
||||
}, time.Minute, time.Second).Should(Not(BeZero()))
|
||||
|
||||
By("retrieving the current datastore-config Secret and its checksum")
|
||||
secretName := fmt.Sprintf("%s-datastore-config", tcp.GetName())
|
||||
|
||||
var secret corev1.Secret
|
||||
Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: tcp.GetNamespace()}, &secret)).To(Succeed())
|
||||
|
||||
originalChecksum := secret.GetAnnotations()["kamaji.clastix.io/checksum"]
|
||||
Expect(originalChecksum).NotTo(BeEmpty(), "expected datastore-config Secret to carry a checksum annotation")
|
||||
|
||||
By("corrupting the DB_PASSWORD in the datastore-config Secret")
|
||||
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
if err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(&secret), &secret); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secret.Data["DB_PASSWORD"] = []byte("corrupted-password")
|
||||
|
||||
return k8sClient.Update(context.Background(), &secret)
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
By("waiting for the controller to detect the corruption and regenerate the Secret with a new checksum")
|
||||
Eventually(func() string {
|
||||
if err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(&secret), &secret); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return secret.GetAnnotations()["kamaji.clastix.io/checksum"]
|
||||
}, 5*time.Minute, time.Second).ShouldNot(Equal(originalChecksum))
|
||||
|
||||
By("waiting for at least one new TenantControlPlane pod to replace the pre-existing ones")
|
||||
Eventually(func() bool {
|
||||
var podList corev1.PodList
|
||||
if err := k8sClient.List(context.Background(), &podList,
|
||||
client.InNamespace(tcp.GetNamespace()),
|
||||
client.MatchingLabels{"kamaji.clastix.io/name": tcp.GetName()},
|
||||
); err != nil {
|
||||
return false
|
||||
}
|
||||
for _, pod := range podList.Items {
|
||||
if !initialPodUIDs.Has(pod.GetUID()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}, 5*time.Minute, time.Second).Should(BeTrue())
|
||||
|
||||
By("verifying the TenantControlPlane is Ready after the restart with the regenerated Secret")
|
||||
StatusMustEqualTo(tcp, kamajiv1alpha1.VersionReady)
|
||||
})
|
||||
})
|
||||
4
go.mod
4
go.mod
@@ -33,9 +33,9 @@ require (
|
||||
k8s.io/apimachinery v0.35.0
|
||||
k8s.io/client-go v0.35.0
|
||||
k8s.io/cluster-bootstrap v0.0.0
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/klog/v2 v2.140.0
|
||||
k8s.io/kubelet v0.0.0
|
||||
k8s.io/kubernetes v1.35.1
|
||||
k8s.io/kubernetes v1.35.2
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
|
||||
sigs.k8s.io/controller-runtime v0.22.4
|
||||
sigs.k8s.io/gateway-api v1.4.1
|
||||
|
||||
8
go.sum
8
go.sum
@@ -539,8 +539,8 @@ k8s.io/cri-api v0.35.0 h1:fxLSKyJHqbyCSUsg1rW4DRpmjSEM/elZ1GXzYTSLoDQ=
|
||||
k8s.io/cri-api v0.35.0/go.mod h1:Cnt29u/tYl1Se1cBRL30uSZ/oJ5TaIp4sZm1xDLvcMc=
|
||||
k8s.io/cri-client v0.35.0 h1:U1K4bteO93yioUS38804ybN+kWaon9zrzVtB37I3fCs=
|
||||
k8s.io/cri-client v0.35.0/go.mod h1:XG5GkuuSpxvungsJVzW58NyWBoGSQhMMJmE5c66m9N8=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||
k8s.io/kms v0.35.0 h1:/x87FED2kDSo66csKtcYCEHsxF/DBlNl7LfJ1fVQs1o=
|
||||
k8s.io/kms v0.35.0/go.mod h1:VT+4ekZAdrZDMgShK37vvlyHUVhwI9t/9tvh0AyCWmQ=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
@@ -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.1 h1:qmjXSCDPnOuXPuJb5pv+eLzpXhhlD09Jid1pG/OvFU8=
|
||||
k8s.io/kubernetes v1.35.1/go.mod h1:AaPpCpiS8oAqRbEwpY5r3RitLpwpVp5lVXKFkJril58=
|
||||
k8s.io/kubernetes v1.35.2 h1:2HthVDfK3YJYv624imuKXPzUJ17xQop9OT5dgT+IMKE=
|
||||
k8s.io/kubernetes v1.35.2/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=
|
||||
|
||||
@@ -47,6 +47,7 @@ func NewStorageConnection(ctx context.Context, client client.Client, ds kamajiv1
|
||||
|
||||
type Connection interface {
|
||||
CreateUser(ctx context.Context, user, password string) error
|
||||
UpdateUser(ctx context.Context, user, password string) error
|
||||
CreateDB(ctx context.Context, dbName string) error
|
||||
GrantPrivileges(ctx context.Context, user, dbName string) error
|
||||
UserExists(ctx context.Context, user string) (bool, error)
|
||||
|
||||
@@ -5,6 +5,10 @@ package errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
func NewUpdateUserError(err error) error {
|
||||
return fmt.Errorf("cannot update user: %w", err)
|
||||
}
|
||||
|
||||
func NewCreateUserError(err error) error {
|
||||
return fmt.Errorf("cannot create user: %w", err)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@ func (e *EtcdClient) CreateUser(ctx context.Context, user, password string) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EtcdClient) UpdateUser(ctx context.Context, user, password string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EtcdClient) CreateDB(context.Context, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ const (
|
||||
mysqlShowGrantsStatement = "SHOW GRANTS FOR `%s`@`%%`"
|
||||
mysqlCreateDBStatement = "CREATE DATABASE IF NOT EXISTS %s"
|
||||
mysqlCreateUserStatement = "CREATE USER `%s`@`%%` IDENTIFIED BY '%s'"
|
||||
mysqlUpdateUserStatement = "ALTER USER `%s`@`%%` IDENTIFIED BY '%s'"
|
||||
mysqlGrantPrivilegesStatement = "GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX ON `%s`.* TO `%s`@`%%`"
|
||||
mysqlDropDBStatement = "DROP DATABASE IF EXISTS `%s`"
|
||||
mysqlDropUserStatement = "DROP USER IF EXISTS `%s`"
|
||||
@@ -158,6 +159,14 @@ func (c *MySQLConnection) CreateUser(ctx context.Context, user, password string)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *MySQLConnection) UpdateUser(ctx context.Context, user, password string) error {
|
||||
if err := c.mutate(ctx, mysqlUpdateUserStatement, user, password); err != nil {
|
||||
return errors.NewUpdateUserError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *MySQLConnection) CreateDB(ctx context.Context, dbName string) error {
|
||||
if err := c.mutate(ctx, mysqlCreateDBStatement, dbName); err != nil {
|
||||
return errors.NewCreateDBError(err)
|
||||
|
||||
@@ -70,6 +70,10 @@ func (nc *NATSConnection) CreateUser(_ context.Context, _, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nc *NATSConnection) UpdateUser(_ context.Context, _, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nc *NATSConnection) CreateDB(_ context.Context, dbName string) error {
|
||||
_, err := nc.js.CreateKeyValue(&nats.KeyValueConfig{Bucket: dbName})
|
||||
if err != nil {
|
||||
|
||||
@@ -20,6 +20,7 @@ const (
|
||||
postgresqlCreateDBStatement = `CREATE DATABASE "%s"`
|
||||
postgresqlUserExists = "SELECT 1 FROM pg_roles WHERE rolname = ?"
|
||||
postgresqlCreateUserStatement = `CREATE ROLE "%s" LOGIN PASSWORD ?`
|
||||
postgresqlUpdateUserStatement = `ALTER ROLE "%s" WITH PASSWORD ?`
|
||||
postgresqlShowGrantsStatement = "SELECT has_database_privilege(rolname, ?, 'create') from pg_roles where rolcanlogin and rolname = ?"
|
||||
postgresqlShowOwnershipStatement = "SELECT 't' FROM pg_catalog.pg_database AS d WHERE d.datname = ? AND pg_catalog.pg_get_userbyid(d.datdba) = ?"
|
||||
postgresqlShowTableOwnershipStatement = "SELECT 't' from pg_tables where tableowner = ? AND tablename = ?"
|
||||
@@ -142,6 +143,15 @@ func (r *PostgreSQLConnection) CreateUser(ctx context.Context, user, password st
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PostgreSQLConnection) UpdateUser(ctx context.Context, user, password string) error {
|
||||
_, err := r.db.ExecContext(ctx, fmt.Sprintf(postgresqlUpdateUserStatement, user), password)
|
||||
if err != nil {
|
||||
return errors.NewUpdateUserError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PostgreSQLConnection) DBExists(ctx context.Context, dbName string) (bool, error) {
|
||||
rows, err := r.db.ExecContext(ctx, postgresqlFetchDBStatement, dbName)
|
||||
if err != nil {
|
||||
|
||||
@@ -104,6 +104,7 @@ func getKubeletConfigmapContent(kubeletConfiguration KubeletConfiguration, patch
|
||||
return nil, fmt.Errorf("unable to apply JSON patching to KubeletConfiguration: %w", patchErr)
|
||||
}
|
||||
|
||||
kc = kubelettypes.KubeletConfiguration{}
|
||||
if patchErr = utilities.DecodeFromJSON(string(kubeletConfig), &kc); patchErr != nil {
|
||||
return nil, fmt.Errorf("unable to decode JSON to KubeletConfiguration: %w", patchErr)
|
||||
}
|
||||
|
||||
@@ -230,6 +230,10 @@ func (r *Setup) createUser(ctx context.Context, _ *kamajiv1alpha1.TenantControlP
|
||||
}
|
||||
|
||||
if exists {
|
||||
if updateErr := r.Connection.UpdateUser(ctx, r.resource.user, r.resource.password); updateErr != nil {
|
||||
return controllerutil.OperationResultNone, fmt.Errorf("unable to update the user to : %w", updateErr)
|
||||
}
|
||||
|
||||
return controllerutil.OperationResultNone, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user