mirror of
https://github.com/clastix/kamaji.git
synced 2026-02-28 16:50:29 +00:00
Compare commits
14 Commits
edge-25.11
...
edge-25.12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3fb03a752 | ||
|
|
eb86fec050 | ||
|
|
35c83fbd4d | ||
|
|
4ad4721965 | ||
|
|
f4b6de4c40 | ||
|
|
54e795323e | ||
|
|
61a4c152b3 | ||
|
|
eface0f792 | ||
|
|
2316af9731 | ||
|
|
880b36e0fa | ||
|
|
ac7da57454 | ||
|
|
20cc50b748 | ||
|
|
4956790e2b | ||
|
|
9069c9be47 |
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
name: integration
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
name: lint
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
name: diff
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v6
|
||||
|
||||
9
.github/workflows/e2e.yaml
vendored
9
.github/workflows/e2e.yaml
vendored
@@ -35,16 +35,23 @@ jobs:
|
||||
name: Kubernetes
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: reclaim disk space from runner
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
|
||||
- run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y golang-cfssl
|
||||
sudo swapoff -a
|
||||
sudo modprobe br_netfilter
|
||||
- name: install required Go tools
|
||||
run: make kind ko helm ginkgo
|
||||
- name: cleaning up go mod
|
||||
run: go clean -modcache
|
||||
- name: e2e testing
|
||||
run: make e2e
|
||||
|
||||
6
.github/workflows/helm.yaml
vendored
6
.github/workflows/helm.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
name: diff
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: make -C charts/kamaji docs
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: 3.3.4
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
needs: [ "lint", "diff" ]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Publish Helm chart
|
||||
uses: stefanprodan/helm-gh-pages@master
|
||||
with:
|
||||
|
||||
2
.github/workflows/ko-build.yml
vendored
2
.github/workflows/ko-build.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
ko:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v6
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: generating date metadata
|
||||
|
||||
@@ -13,8 +13,10 @@ Feel free to open a Pull-Request to get yours listed.
|
||||
| Vendor | Coredge | 2025 | [link](https://coredge.io/) | Coredge uses Kamaji in its K8saaS offering to save infrastructure costs in its Sovereign Cloud & AI Infrastructure Platform for end-user organisations. |
|
||||
| Vendor | DCloud | 2024 | [link](https://dcloud.co.id) | DCloud is an Indonesian Cloud Provider using Kamaji to build and offer [Managed Kubernetes Service](https://dcloud.co.id/dkubes.html). |
|
||||
| Vendor | Dinova | 2025 | [link](https://dinova.one/) | Dinova is an Italian cloud services provider that integrates Kamaji in its datacenters to offer fully managed Kubernetes clusters. |
|
||||
| Vendor | Hikube | 2024 | [link](https://hikube.cloud/) | Hikube.cloud is a Swiss sovereign cloud platform with triple replication across three Swiss datacenters, offering enterprise-grade infrastructure with full data sovereignty. |
|
||||
| End-user | KINX | 2024 | [link](https://kinx.net/?lang=en) | KINX is an Internet infrastructure service provider and will use kamaji for its new [Managed Kubernetes Service](https://kinx.net/service/cloud/kubernetes/intro/?lang=en). |
|
||||
| Vendor | Netalia | 2025 | [link](https://www.netalia.it) | Netalia uses Kamaji for the Italian cloud
|
||||
| End-user | Namecheap | 2025 | [link](https://www.namecheap.com/) | Namecheap is an ICANN-accredited domain registrar and web hosting company that provides a wide range of internet-related services and uses Kamaji for both internal and external services. |
|
||||
| Vendor | Netalia | 2025 | [link](https://www.netalia.it) | Netalia uses Kamaji for the Italian cloud
|
||||
| Vendor | Netsons | 2023 | [link](https://www.netsons.com) | Netsons is an Italian hosting and cloud provider and uses Kamaji in its [Managed Kubernetes](https://www.netsons.com/kubernetes) offering. |
|
||||
| Vendor | NVIDIA | 2024 | [link](https://github.com/NVIDIA/doca-platform) | DOCA Platform Framework manages provisioning and service orchestration for NVIDIA Bluefield DPUs. |
|
||||
| R&D | Orange | 2024 | [link](https://gitlab.com/Orange-OpenSource/kanod) | Orange is a French telecommunications company using Kamaji for experimental research purpose, with Kanod research solution. |
|
||||
|
||||
21
Makefile
21
Makefile
@@ -177,7 +177,7 @@ datastore-postgres:
|
||||
$(MAKE) NAME=gold _datastore-postgres
|
||||
|
||||
_datastore-etcd:
|
||||
$(HELM) upgrade --install etcd-$(NAME) clastix/kamaji-etcd --create-namespace -n etcd-system --set datastore.enabled=true --set fullnameOverride=etcd-$(NAME)
|
||||
$(HELM) upgrade --install etcd-$(NAME) clastix/kamaji-etcd --create-namespace -n $(NAMESPACE) --set datastore.enabled=true --set fullnameOverride=etcd-$(NAME) $(EXTRA_ARGS)
|
||||
|
||||
_datastore-nats:
|
||||
$(MAKE) NAME=$(NAME) NAMESPACE=nats-system -C deploy/kine/nats nats
|
||||
@@ -186,9 +186,11 @@ _datastore-nats:
|
||||
datastore-etcd: helm
|
||||
$(HELM) repo add clastix https://clastix.github.io/charts
|
||||
$(HELM) repo update
|
||||
$(MAKE) NAME=bronze _datastore-etcd
|
||||
$(MAKE) NAME=silver _datastore-etcd
|
||||
$(MAKE) NAME=gold _datastore-etcd
|
||||
$(MAKE) NAME=bronze NAMESPACE=etcd-system _datastore-etcd
|
||||
$(MAKE) NAME=silver NAMESPACE=etcd-system _datastore-etcd
|
||||
$(MAKE) NAME=gold NAMESPACE=etcd-system _datastore-etcd
|
||||
$(MAKE) NAME=primary NAMESPACE=kamaji-system EXTRA_ARGS='--set certManager.enabled=true --set certManager.issuerRef.kind=Issuer --set certManager.issuerRef.name=kamaji-selfsigned-issuer --set selfSignedCertificates.enabled=false' _datastore-etcd
|
||||
$(MAKE) NAME=secondary NAMESPACE=kamaji-system EXTRA_ARGS='--set certManager.enabled=true --set certManager.ca.create=false --set certManager.ca.nameOverride=etcd-primary-ca --set certManager.issuerRef.kind=Issuer --set certManager.issuerRef.name=kamaji-selfsigned-issuer --set selfSignedCertificates.enabled=false' _datastore-etcd
|
||||
|
||||
datastore-nats: helm
|
||||
$(HELM) repo add nats https://nats-io.github.io/k8s/helm/charts/
|
||||
@@ -240,6 +242,12 @@ cert-manager:
|
||||
$(HELM) repo add jetstack https://charts.jetstack.io
|
||||
$(HELM) upgrade --install cert-manager jetstack/cert-manager --namespace certmanager-system --create-namespace --set "installCRDs=true"
|
||||
|
||||
gateway-api:
|
||||
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
|
||||
# Required for the TLSRoutes. Experimentals.
|
||||
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/experimental-install.yaml
|
||||
kubectl wait --for=condition=Established crd/gateways.gateway.networking.k8s.io --timeout=60s
|
||||
|
||||
load: kind
|
||||
$(KIND) load docker-image --name kamaji ${CONTAINER_REPOSITORY}:${VERSION}
|
||||
|
||||
@@ -249,8 +257,11 @@ load: kind
|
||||
env: kind
|
||||
$(KIND) create cluster --name kamaji
|
||||
|
||||
cleanup: kind
|
||||
$(KIND) delete cluster --name kamaji
|
||||
|
||||
.PHONY: e2e
|
||||
e2e: env build load helm ginkgo cert-manager ## Create a KinD cluster, install Kamaji on it and run the test suite.
|
||||
e2e: env build load helm ginkgo cert-manager gateway-api ## Create a KinD cluster, install Kamaji on it and run the test suite.
|
||||
$(HELM) upgrade --debug --install kamaji-crds ./charts/kamaji-crds --create-namespace --namespace kamaji-system
|
||||
$(HELM) repo add clastix https://clastix.github.io/charts
|
||||
$(HELM) dependency build ./charts/kamaji
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// Package v1alpha1 contains API Schema definitions for the kamaji v1alpha1 API group
|
||||
// +kubebuilder:object:generate=true
|
||||
// +groupName=kamaji.clastix.io
|
||||
//nolint
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
|
||||
47
api/v1alpha1/indexer_gateway_listener.go
Normal file
47
api/v1alpha1/indexer_gateway_listener.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
controllerruntime "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
GatewayListenerNameKey = "spec.listeners.name"
|
||||
)
|
||||
|
||||
type GatewayListener struct{}
|
||||
|
||||
func (g *GatewayListener) Object() client.Object {
|
||||
return &gatewayv1.Gateway{}
|
||||
}
|
||||
|
||||
func (g *GatewayListener) Field() string {
|
||||
return GatewayListenerNameKey
|
||||
}
|
||||
|
||||
func (g *GatewayListener) ExtractValue() client.IndexerFunc {
|
||||
return func(object client.Object) []string {
|
||||
gateway := object.(*gatewayv1.Gateway) //nolint:forcetypeassert
|
||||
|
||||
listenerNames := make([]string, 0, len(gateway.Spec.Listeners))
|
||||
for _, listener := range gateway.Spec.Listeners {
|
||||
// Create a composite key: namespace/gatewayName/listenerName
|
||||
// This allows us to look up gateways by listener name while ensuring uniqueness
|
||||
key := fmt.Sprintf("%s/%s/%s", gateway.Namespace, gateway.Name, listener.Name)
|
||||
listenerNames = append(listenerNames, key)
|
||||
}
|
||||
|
||||
return listenerNames
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GatewayListener) SetupWithManager(ctx context.Context, mgr controllerruntime.Manager) error {
|
||||
return mgr.GetFieldIndexer().IndexField(ctx, g.Object(), g.Field(), g.ExtractValue())
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
// APIServerCertificatesStatus defines the observed state of ETCD Certificate for API server.
|
||||
@@ -187,6 +188,7 @@ type KubernetesStatus struct {
|
||||
Deployment KubernetesDeploymentStatus `json:"deployment,omitempty"`
|
||||
Service KubernetesServiceStatus `json:"service,omitempty"`
|
||||
Ingress *KubernetesIngressStatus `json:"ingress,omitempty"`
|
||||
Gateway *KubernetesGatewayStatus `json:"gateway,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:validation:Enum=Unknown;Provisioning;CertificateAuthorityRotating;Upgrading;Migrating;Ready;NotReady;Sleeping;WriteLimited
|
||||
@@ -244,3 +246,25 @@ type KubernetesIngressStatus struct {
|
||||
// The namespace which the Ingress for the given cluster is deployed.
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
type GatewayAccessPoint struct {
|
||||
Type *gatewayv1.AddressType `json:"type"`
|
||||
Value string `json:"value"`
|
||||
Port int32 `json:"port"`
|
||||
URLs []string `json:"urls,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=false
|
||||
type RouteStatus = gatewayv1.RouteStatus
|
||||
|
||||
// KubernetesGatewayStatus defines the status for the Tenant Control Plane Gateway in the management cluster.
|
||||
type KubernetesGatewayStatus struct {
|
||||
// The TLSRoute status as resported by the gateway controllers.
|
||||
RouteStatus `json:",inline"`
|
||||
|
||||
// Reference to the route created for this tenant.
|
||||
RouteRef corev1.LocalObjectReference `json:"routeRef,omitempty"`
|
||||
|
||||
// A list of valid access points that the route exposes.
|
||||
AccessPoints []GatewayAccessPoint `json:"accessPoints,omitempty"`
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
// NetworkProfileSpec defines the desired state of NetworkProfile.
|
||||
@@ -124,6 +125,7 @@ type AdditionalMetadata struct {
|
||||
|
||||
// ControlPlane defines how the Tenant Control Plane Kubernetes resources must be created in the Admin Cluster,
|
||||
// such as the number of Pod replicas, the Service resource, or the Ingress.
|
||||
// +kubebuilder:validation:XValidation:rule="!(has(self.ingress) && has(self.gateway))",message="using both ingress and gateway is not supported"
|
||||
type ControlPlane struct {
|
||||
// Defining the options for the deployed Tenant Control Plane as Deployment resource.
|
||||
Deployment DeploymentSpec `json:"deployment,omitempty"`
|
||||
@@ -131,6 +133,8 @@ type ControlPlane struct {
|
||||
Service ServiceSpec `json:"service"`
|
||||
// Defining the options for an Optional Ingress which will expose API Server of the Tenant Control Plane
|
||||
Ingress *IngressSpec `json:"ingress,omitempty"`
|
||||
// Defining the options for an Optional Gateway which will expose API Server of the Tenant Control Plane
|
||||
Gateway *GatewaySpec `json:"gateway,omitempty"`
|
||||
}
|
||||
|
||||
// IngressSpec defines the options for the ingress which will expose API Server of the Tenant Control Plane.
|
||||
@@ -142,6 +146,16 @@ type IngressSpec struct {
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
}
|
||||
|
||||
// GatewaySpec defines the options for the Gateway which will expose API Server of the Tenant Control Plane.
|
||||
type GatewaySpec struct {
|
||||
// AdditionalMetadata to add Labels and Annotations support.
|
||||
AdditionalMetadata AdditionalMetadata `json:"additionalMetadata,omitempty"`
|
||||
// GatewayParentRefs is the class of the Gateway resource to use.
|
||||
GatewayParentRefs []gatewayv1.ParentReference `json:"parentRefs,omitempty"`
|
||||
// Hostname is an optional field which will be used as a route hostname.
|
||||
Hostname gatewayv1.Hostname `json:"hostname,omitempty"`
|
||||
}
|
||||
|
||||
type ControlPlaneComponentsResources struct {
|
||||
APIServer *corev1.ResourceRequirements `json:"apiServer,omitempty"`
|
||||
ControllerManager *corev1.ResourceRequirements `json:"controllerManager,omitempty"`
|
||||
@@ -341,6 +355,14 @@ func (p *Permissions) HasAnyLimitation() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// DataStoreOverride defines which kubernetes resource will be stored in a dedicated datastore.
|
||||
type DataStoreOverride struct {
|
||||
// Resource specifies which kubernetes resource to target.
|
||||
Resource string `json:"resource,omitempty"`
|
||||
// DataStore specifies the DataStore that should be used to store the Kubernetes data for the given Resource.
|
||||
DataStore string `json:"dataStore,omitempty"`
|
||||
}
|
||||
|
||||
// TenantControlPlaneSpec defines the desired state of TenantControlPlane.
|
||||
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.dataStore) || has(self.dataStore)", message="unsetting the dataStore is not supported"
|
||||
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.dataStoreSchema) || has(self.dataStoreSchema)", message="unsetting the dataStoreSchema is not supported"
|
||||
@@ -375,8 +397,10 @@ type TenantControlPlaneSpec struct {
|
||||
// to the user to avoid clashes between different TenantControlPlanes. If not set upon creation, Kamaji will default the
|
||||
// DataStoreUsername by concatenating the namespace and name of the TenantControlPlane.
|
||||
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="changing the dataStoreUsername is not supported"
|
||||
DataStoreUsername string `json:"dataStoreUsername,omitempty"`
|
||||
ControlPlane ControlPlane `json:"controlPlane"`
|
||||
DataStoreUsername string `json:"dataStoreUsername,omitempty"`
|
||||
// DataStoreOverride defines which kubernetes resources will be stored in dedicated datastores.
|
||||
DataStoreOverrides []DataStoreOverride `json:"dataStoreOverrides,omitempty"`
|
||||
ControlPlane ControlPlane `json:"controlPlane"`
|
||||
// Kubernetes specification for tenant control plane
|
||||
Kubernetes KubernetesSpec `json:"kubernetes"`
|
||||
// NetworkProfile specifies how the network is
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/api/core/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
@@ -83,21 +84,21 @@ func (in *AdditionalVolumeMounts) DeepCopyInto(out *AdditionalVolumeMounts) {
|
||||
*out = *in
|
||||
if in.APIServer != nil {
|
||||
in, out := &in.APIServer, &out.APIServer
|
||||
*out = make([]v1.VolumeMount, len(*in))
|
||||
*out = make([]corev1.VolumeMount, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.ControllerManager != nil {
|
||||
in, out := &in.ControllerManager, &out.ControllerManager
|
||||
*out = make([]v1.VolumeMount, len(*in))
|
||||
*out = make([]corev1.VolumeMount, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Scheduler != nil {
|
||||
in, out := &in.Scheduler, &out.Scheduler
|
||||
*out = make([]v1.VolumeMount, len(*in))
|
||||
*out = make([]corev1.VolumeMount, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
@@ -360,6 +361,11 @@ func (in *ControlPlane) DeepCopyInto(out *ControlPlane) {
|
||||
*out = new(IngressSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Gateway != nil {
|
||||
in, out := &in.Gateway, &out.Gateway
|
||||
*out = new(GatewaySpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlane.
|
||||
@@ -377,22 +383,22 @@ func (in *ControlPlaneComponentsResources) DeepCopyInto(out *ControlPlaneCompone
|
||||
*out = *in
|
||||
if in.APIServer != nil {
|
||||
in, out := &in.APIServer, &out.APIServer
|
||||
*out = new(v1.ResourceRequirements)
|
||||
*out = new(corev1.ResourceRequirements)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ControllerManager != nil {
|
||||
in, out := &in.ControllerManager, &out.ControllerManager
|
||||
*out = new(v1.ResourceRequirements)
|
||||
*out = new(corev1.ResourceRequirements)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Scheduler != nil {
|
||||
in, out := &in.Scheduler, &out.Scheduler
|
||||
*out = new(v1.ResourceRequirements)
|
||||
*out = new(corev1.ResourceRequirements)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Kine != nil {
|
||||
in, out := &in.Kine, &out.Kine
|
||||
*out = new(v1.ResourceRequirements)
|
||||
*out = new(corev1.ResourceRequirements)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
@@ -532,6 +538,21 @@ func (in *DataStoreList) DeepCopyObject() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DataStoreOverride) DeepCopyInto(out *DataStoreOverride) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataStoreOverride.
|
||||
func (in *DataStoreOverride) DeepCopy() *DataStoreOverride {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DataStoreOverride)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DataStoreSetupStatus) DeepCopyInto(out *DataStoreSetupStatus) {
|
||||
*out = *in
|
||||
@@ -632,19 +653,19 @@ func (in *DeploymentSpec) DeepCopyInto(out *DeploymentSpec) {
|
||||
in.Strategy.DeepCopyInto(&out.Strategy)
|
||||
if in.Tolerations != nil {
|
||||
in, out := &in.Tolerations, &out.Tolerations
|
||||
*out = make([]v1.Toleration, len(*in))
|
||||
*out = make([]corev1.Toleration, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Affinity != nil {
|
||||
in, out := &in.Affinity, &out.Affinity
|
||||
*out = new(v1.Affinity)
|
||||
*out = new(corev1.Affinity)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.TopologySpreadConstraints != nil {
|
||||
in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints
|
||||
*out = make([]v1.TopologySpreadConstraint, len(*in))
|
||||
*out = make([]corev1.TopologySpreadConstraint, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
@@ -663,21 +684,21 @@ func (in *DeploymentSpec) DeepCopyInto(out *DeploymentSpec) {
|
||||
in.PodAdditionalMetadata.DeepCopyInto(&out.PodAdditionalMetadata)
|
||||
if in.AdditionalInitContainers != nil {
|
||||
in, out := &in.AdditionalInitContainers, &out.AdditionalInitContainers
|
||||
*out = make([]v1.Container, len(*in))
|
||||
*out = make([]corev1.Container, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.AdditionalContainers != nil {
|
||||
in, out := &in.AdditionalContainers, &out.AdditionalContainers
|
||||
*out = make([]v1.Container, len(*in))
|
||||
*out = make([]corev1.Container, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.AdditionalVolumes != nil {
|
||||
in, out := &in.AdditionalVolumes, &out.AdditionalVolumes
|
||||
*out = make([]v1.Volume, len(*in))
|
||||
*out = make([]corev1.Volume, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
@@ -786,6 +807,69 @@ func (in ExtraArgs) DeepCopy() ExtraArgs {
|
||||
return *out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GatewayAccessPoint) DeepCopyInto(out *GatewayAccessPoint) {
|
||||
*out = *in
|
||||
if in.Type != nil {
|
||||
in, out := &in.Type, &out.Type
|
||||
*out = new(v1.AddressType)
|
||||
**out = **in
|
||||
}
|
||||
if in.URLs != nil {
|
||||
in, out := &in.URLs, &out.URLs
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayAccessPoint.
|
||||
func (in *GatewayAccessPoint) DeepCopy() *GatewayAccessPoint {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(GatewayAccessPoint)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GatewayListener) DeepCopyInto(out *GatewayListener) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayListener.
|
||||
func (in *GatewayListener) DeepCopy() *GatewayListener {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(GatewayListener)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GatewaySpec) DeepCopyInto(out *GatewaySpec) {
|
||||
*out = *in
|
||||
in.AdditionalMetadata.DeepCopyInto(&out.AdditionalMetadata)
|
||||
if in.GatewayParentRefs != nil {
|
||||
in, out := &in.GatewayParentRefs, &out.GatewayParentRefs
|
||||
*out = make([]v1.ParentReference, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewaySpec.
|
||||
func (in *GatewaySpec) DeepCopy() *GatewaySpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(GatewaySpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ImageOverrideTrait) DeepCopyInto(out *ImageOverrideTrait) {
|
||||
*out = *in
|
||||
@@ -822,7 +906,7 @@ func (in *KonnectivityAgentSpec) DeepCopyInto(out *KonnectivityAgentSpec) {
|
||||
*out = *in
|
||||
if in.Tolerations != nil {
|
||||
in, out := &in.Tolerations, &out.Tolerations
|
||||
*out = make([]v1.Toleration, len(*in))
|
||||
*out = make([]corev1.Toleration, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
@@ -880,7 +964,7 @@ func (in *KonnectivityServerSpec) DeepCopyInto(out *KonnectivityServerSpec) {
|
||||
*out = *in
|
||||
if in.Resources != nil {
|
||||
in, out := &in.Resources, &out.Resources
|
||||
*out = new(v1.ResourceRequirements)
|
||||
*out = new(corev1.ResourceRequirements)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ExtraArgs != nil {
|
||||
@@ -1175,6 +1259,30 @@ func (in *KubernetesDeploymentStatus) DeepCopy() *KubernetesDeploymentStatus {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *KubernetesGatewayStatus) DeepCopyInto(out *KubernetesGatewayStatus) {
|
||||
*out = *in
|
||||
in.RouteStatus.DeepCopyInto(&out.RouteStatus)
|
||||
out.RouteRef = in.RouteRef
|
||||
if in.AccessPoints != nil {
|
||||
in, out := &in.AccessPoints, &out.AccessPoints
|
||||
*out = make([]GatewayAccessPoint, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesGatewayStatus.
|
||||
func (in *KubernetesGatewayStatus) DeepCopy() *KubernetesGatewayStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(KubernetesGatewayStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *KubernetesIngressStatus) DeepCopyInto(out *KubernetesIngressStatus) {
|
||||
*out = *in
|
||||
@@ -1239,6 +1347,11 @@ func (in *KubernetesStatus) DeepCopyInto(out *KubernetesStatus) {
|
||||
*out = new(KubernetesIngressStatus)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Gateway != nil {
|
||||
in, out := &in.Gateway, &out.Gateway
|
||||
*out = new(KubernetesGatewayStatus)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesStatus.
|
||||
@@ -1493,6 +1606,11 @@ func (in *TenantControlPlaneList) DeepCopyObject() runtime.Object {
|
||||
func (in *TenantControlPlaneSpec) DeepCopyInto(out *TenantControlPlaneSpec) {
|
||||
*out = *in
|
||||
out.WritePermissions = in.WritePermissions
|
||||
if in.DataStoreOverrides != nil {
|
||||
in, out := &in.DataStoreOverrides, &out.DataStoreOverrides
|
||||
*out = make([]DataStoreOverride, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
in.ControlPlane.DeepCopyInto(&out.ControlPlane)
|
||||
in.Kubernetes.DeepCopyInto(&out.Kubernetes)
|
||||
in.NetworkProfile.DeepCopyInto(&out.NetworkProfile)
|
||||
|
||||
@@ -6700,6 +6700,178 @@ versions:
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
gateway:
|
||||
description: Defining the options for an Optional Gateway which will expose API Server of the Tenant Control Plane
|
||||
properties:
|
||||
additionalMetadata:
|
||||
description: AdditionalMetadata to add Labels and Annotations support.
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
hostname:
|
||||
description: Hostname is an optional field which will be used as a route hostname.
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
|
||||
type: string
|
||||
parentRefs:
|
||||
description: GatewayParentRefs is the class of the Gateway resource to use.
|
||||
items:
|
||||
description: |-
|
||||
ParentReference identifies an API object (usually a Gateway) that can be considered
|
||||
a parent of this resource (usually a route). There are two kinds of parent resources
|
||||
with "Core" support:
|
||||
|
||||
* Gateway (Gateway conformance profile)
|
||||
* Service (Mesh conformance profile, ClusterIP Services only)
|
||||
|
||||
This API may be extended in the future to support additional kinds of parent
|
||||
resources.
|
||||
|
||||
The API object must be valid in the cluster; the Group and Kind must
|
||||
be registered in the cluster for this reference to be valid.
|
||||
properties:
|
||||
group:
|
||||
default: gateway.networking.k8s.io
|
||||
description: |-
|
||||
Group is the group of the referent.
|
||||
When unspecified, "gateway.networking.k8s.io" is inferred.
|
||||
To set the core API group (such as for a "Service" kind referent),
|
||||
Group must be explicitly set to "" (empty string).
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
|
||||
type: string
|
||||
kind:
|
||||
default: Gateway
|
||||
description: |-
|
||||
Kind is kind of the referent.
|
||||
|
||||
There are two kinds of parent resources with "Core" support:
|
||||
|
||||
* Gateway (Gateway conformance profile)
|
||||
* Service (Mesh conformance profile, ClusterIP Services only)
|
||||
|
||||
Support for other resources is Implementation-Specific.
|
||||
maxLength: 63
|
||||
minLength: 1
|
||||
pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
|
||||
type: string
|
||||
name:
|
||||
description: |-
|
||||
Name is the name of the referent.
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
type: string
|
||||
namespace:
|
||||
description: |-
|
||||
Namespace is the namespace of the referent. When unspecified, this refers
|
||||
to the local namespace of the Route.
|
||||
|
||||
Note that there are specific rules for ParentRefs which cross namespace
|
||||
boundaries. Cross-namespace references are only valid if they are explicitly
|
||||
allowed by something in the namespace they are referring to. For example:
|
||||
Gateway has the AllowedRoutes field, and ReferenceGrant provides a
|
||||
generic way to enable any other kind of cross-namespace reference.
|
||||
|
||||
<gateway:experimental:description>
|
||||
ParentRefs from a Route to a Service in the same namespace are "producer"
|
||||
routes, which apply default routing rules to inbound connections from
|
||||
any namespace to the Service.
|
||||
|
||||
ParentRefs from a Route to a Service in a different namespace are
|
||||
"consumer" routes, and these routing rules are only applied to outbound
|
||||
connections originating from the same namespace as the Route, for which
|
||||
the intended destination of the connections are a Service targeted as a
|
||||
ParentRef of the Route.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Support: Core
|
||||
maxLength: 63
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
|
||||
type: string
|
||||
port:
|
||||
description: |-
|
||||
Port is the network port this Route targets. It can be interpreted
|
||||
differently based on the type of parent resource.
|
||||
|
||||
When the parent resource is a Gateway, this targets all listeners
|
||||
listening on the specified port that also support this kind of Route(and
|
||||
select this Route). It's not recommended to set `Port` unless the
|
||||
networking behaviors specified in a Route must apply to a specific port
|
||||
as opposed to a listener(s) whose port(s) may be changed. When both Port
|
||||
and SectionName are specified, the name and port of the selected listener
|
||||
must match both specified values.
|
||||
|
||||
<gateway:experimental:description>
|
||||
When the parent resource is a Service, this targets a specific port in the
|
||||
Service spec. When both Port (experimental) and SectionName are specified,
|
||||
the name and port of the selected port must match both specified values.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Implementations MAY choose to support other parent resources.
|
||||
Implementations supporting other types of parent resources MUST clearly
|
||||
document how/if Port is interpreted.
|
||||
|
||||
For the purpose of status, an attachment is considered successful as
|
||||
long as the parent resource accepts it partially. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
|
||||
from the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route,
|
||||
the Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Extended
|
||||
format: int32
|
||||
maximum: 65535
|
||||
minimum: 1
|
||||
type: integer
|
||||
sectionName:
|
||||
description: |-
|
||||
SectionName is the name of a section within the target resource. In the
|
||||
following resources, SectionName is interpreted as the following:
|
||||
|
||||
* Gateway: Listener name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
* Service: Port name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
|
||||
Implementations MAY choose to support attaching Routes to other resources.
|
||||
If that is the case, they MUST clearly document how SectionName is
|
||||
interpreted.
|
||||
|
||||
When unspecified (empty string), this will reference the entire resource.
|
||||
For the purpose of status, an attachment is considered successful if at
|
||||
least one section in the parent resource accepts it. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
|
||||
the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route, the
|
||||
Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
ingress:
|
||||
description: Defining the options for an Optional Ingress which will expose API Server of the Tenant Control Plane
|
||||
properties:
|
||||
@@ -6801,6 +6973,9 @@ versions:
|
||||
required:
|
||||
- service
|
||||
type: object
|
||||
x-kubernetes-validations:
|
||||
- message: using both ingress and gateway is not supported
|
||||
rule: '!(has(self.ingress) && has(self.gateway))'
|
||||
dataStore:
|
||||
description: |-
|
||||
DataStore specifies the DataStore that should be used to store the Kubernetes data for the given Tenant Control Plane.
|
||||
@@ -6810,6 +6985,19 @@ versions:
|
||||
Migration from one DataStore to another backed by the same Driver is possible. See: https://kamaji.clastix.io/guides/datastore-migration/
|
||||
Migration from one DataStore to another backed by a different Driver is not supported.
|
||||
type: string
|
||||
dataStoreOverrides:
|
||||
description: DataStoreOverride defines which kubernetes resources will be stored in dedicated datastores.
|
||||
items:
|
||||
description: DataStoreOverride defines which kubernetes resource will be stored in a dedicated datastore.
|
||||
properties:
|
||||
dataStore:
|
||||
description: DataStore specifies the DataStore that should be used to store the Kubernetes data for the given Resource.
|
||||
type: string
|
||||
resource:
|
||||
description: Resource specifies which kubernetes resource to target.
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
dataStoreSchema:
|
||||
description: |-
|
||||
DataStoreSchema allows to specify the name of the database (for relational DataStores) or the key prefix (for etcd). This
|
||||
@@ -7551,6 +7739,383 @@ versions:
|
||||
- namespace
|
||||
- selector
|
||||
type: object
|
||||
gateway:
|
||||
description: KubernetesGatewayStatus defines the status for the Tenant Control Plane Gateway in the management cluster.
|
||||
properties:
|
||||
accessPoints:
|
||||
description: A list of valid access points that the route exposes.
|
||||
items:
|
||||
properties:
|
||||
port:
|
||||
format: int32
|
||||
type: integer
|
||||
type:
|
||||
description: |-
|
||||
AddressType defines how a network address is represented as a text string.
|
||||
This may take two possible forms:
|
||||
|
||||
* A predefined CamelCase string identifier (currently limited to `IPAddress` or `Hostname`)
|
||||
* A domain-prefixed string identifier (like `acme.io/CustomAddressType`)
|
||||
|
||||
Values `IPAddress` and `Hostname` have Extended support.
|
||||
|
||||
The `NamedAddress` value has been deprecated in favor of implementation
|
||||
specific domain-prefixed strings.
|
||||
|
||||
All other values, including domain-prefixed values have Implementation-specific support,
|
||||
which are used in implementation-specific behaviors. Support for additional
|
||||
predefined CamelCase identifiers may be added in future releases.
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^Hostname|IPAddress|NamedAddress|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
|
||||
type: string
|
||||
urls:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
value:
|
||||
type: string
|
||||
required:
|
||||
- port
|
||||
- type
|
||||
- value
|
||||
type: object
|
||||
type: array
|
||||
parents:
|
||||
description: |-
|
||||
Parents is a list of parent resources (usually Gateways) that are
|
||||
associated with the route, and the status of the route with respect to
|
||||
each parent. When this route attaches to a parent, the controller that
|
||||
manages the parent must add an entry to this list when the controller
|
||||
first sees the route and should update the entry as appropriate when the
|
||||
route or gateway is modified.
|
||||
|
||||
Note that parent references that cannot be resolved by an implementation
|
||||
of this API will not be added to this list. Implementations of this API
|
||||
can only populate Route status for the Gateways/parent resources they are
|
||||
responsible for.
|
||||
|
||||
A maximum of 32 Gateways will be represented in this list. An empty list
|
||||
means the route has not been attached to any Gateway.
|
||||
|
||||
<gateway:util:excludeFromCRD>
|
||||
Notes for implementors:
|
||||
|
||||
While parents is not a listType `map`, this is due to the fact that the
|
||||
list key is not scalar, and Kubernetes is unable to represent this.
|
||||
|
||||
Parent status MUST be considered to be namespaced by the combination of
|
||||
the parentRef and controllerName fields, and implementations should keep
|
||||
the following rules in mind when updating this status:
|
||||
|
||||
* Implementations MUST update only entries that have a matching value of
|
||||
`controllerName` for that implementation.
|
||||
* Implementations MUST NOT update entries with non-matching `controllerName`
|
||||
fields.
|
||||
* Implementations MUST treat each `parentRef`` in the Route separately and
|
||||
update its status based on the relationship with that parent.
|
||||
* Implementations MUST perform a read-modify-write cycle on this field
|
||||
before modifying it. That is, when modifying this field, implementations
|
||||
must be confident they have fetched the most recent version of this field,
|
||||
and ensure that changes they make are on that recent version.
|
||||
|
||||
</gateway:util:excludeFromCRD>
|
||||
items:
|
||||
description: |-
|
||||
RouteParentStatus describes the status of a route with respect to an
|
||||
associated Parent.
|
||||
properties:
|
||||
conditions:
|
||||
description: |-
|
||||
Conditions describes the status of the route with respect to the Gateway.
|
||||
Note that the route's availability is also subject to the Gateway's own
|
||||
status conditions and listener status.
|
||||
|
||||
If the Route's ParentRef specifies an existing Gateway that supports
|
||||
Routes of this kind AND that Gateway's controller has sufficient access,
|
||||
then that Gateway's controller MUST set the "Accepted" condition on the
|
||||
Route, to indicate whether the route has been accepted or rejected by the
|
||||
Gateway, and why.
|
||||
|
||||
A Route MUST be considered "Accepted" if at least one of the Route's
|
||||
rules is implemented by the Gateway.
|
||||
|
||||
There are a number of cases where the "Accepted" condition may not be set
|
||||
due to lack of controller visibility, that includes when:
|
||||
|
||||
* The Route refers to a nonexistent parent.
|
||||
* The Route is of a type that the controller does not support.
|
||||
* The Route is in a namespace the controller does not have access to.
|
||||
|
||||
<gateway:util:excludeFromCRD>
|
||||
|
||||
Notes for implementors:
|
||||
|
||||
Conditions are a listType `map`, which means that they function like a
|
||||
map with a key of the `type` field _in the k8s apiserver_.
|
||||
|
||||
This means that implementations must obey some rules when updating this
|
||||
section.
|
||||
|
||||
* Implementations MUST perform a read-modify-write cycle on this field
|
||||
before modifying it. That is, when modifying this field, implementations
|
||||
must be confident they have fetched the most recent version of this field,
|
||||
and ensure that changes they make are on that recent version.
|
||||
* Implementations MUST NOT remove or reorder Conditions that they are not
|
||||
directly responsible for. For example, if an implementation sees a Condition
|
||||
with type `special.io/SomeField`, it MUST NOT remove, change or update that
|
||||
Condition.
|
||||
* Implementations MUST always _merge_ changes into Conditions of the same Type,
|
||||
rather than creating more than one Condition of the same Type.
|
||||
* Implementations MUST always update the `observedGeneration` field of the
|
||||
Condition to the `metadata.generation` of the Gateway at the time of update creation.
|
||||
* If the `observedGeneration` of a Condition is _greater than_ the value the
|
||||
implementation knows about, then it MUST NOT perform the update on that Condition,
|
||||
but must wait for a future reconciliation and status update. (The assumption is that
|
||||
the implementation's copy of the object is stale and an update will be re-triggered
|
||||
if relevant.)
|
||||
|
||||
</gateway:util:excludeFromCRD>
|
||||
items:
|
||||
description: Condition contains details for one aspect of the current state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False, Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
maxItems: 8
|
||||
minItems: 1
|
||||
type: array
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
controllerName:
|
||||
description: |-
|
||||
ControllerName is a domain/path string that indicates the name of the
|
||||
controller that wrote this status. This corresponds with the
|
||||
controllerName field on GatewayClass.
|
||||
|
||||
Example: "example.net/gateway-controller".
|
||||
|
||||
The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are
|
||||
valid Kubernetes names
|
||||
(https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
|
||||
|
||||
Controllers MUST populate this field when writing status. Controllers should ensure that
|
||||
entries to status populated with their ControllerName are cleaned up when they are no
|
||||
longer necessary.
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
|
||||
type: string
|
||||
parentRef:
|
||||
description: |-
|
||||
ParentRef corresponds with a ParentRef in the spec that this
|
||||
RouteParentStatus struct describes the status of.
|
||||
properties:
|
||||
group:
|
||||
default: gateway.networking.k8s.io
|
||||
description: |-
|
||||
Group is the group of the referent.
|
||||
When unspecified, "gateway.networking.k8s.io" is inferred.
|
||||
To set the core API group (such as for a "Service" kind referent),
|
||||
Group must be explicitly set to "" (empty string).
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
|
||||
type: string
|
||||
kind:
|
||||
default: Gateway
|
||||
description: |-
|
||||
Kind is kind of the referent.
|
||||
|
||||
There are two kinds of parent resources with "Core" support:
|
||||
|
||||
* Gateway (Gateway conformance profile)
|
||||
* Service (Mesh conformance profile, ClusterIP Services only)
|
||||
|
||||
Support for other resources is Implementation-Specific.
|
||||
maxLength: 63
|
||||
minLength: 1
|
||||
pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
|
||||
type: string
|
||||
name:
|
||||
description: |-
|
||||
Name is the name of the referent.
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
type: string
|
||||
namespace:
|
||||
description: |-
|
||||
Namespace is the namespace of the referent. When unspecified, this refers
|
||||
to the local namespace of the Route.
|
||||
|
||||
Note that there are specific rules for ParentRefs which cross namespace
|
||||
boundaries. Cross-namespace references are only valid if they are explicitly
|
||||
allowed by something in the namespace they are referring to. For example:
|
||||
Gateway has the AllowedRoutes field, and ReferenceGrant provides a
|
||||
generic way to enable any other kind of cross-namespace reference.
|
||||
|
||||
<gateway:experimental:description>
|
||||
ParentRefs from a Route to a Service in the same namespace are "producer"
|
||||
routes, which apply default routing rules to inbound connections from
|
||||
any namespace to the Service.
|
||||
|
||||
ParentRefs from a Route to a Service in a different namespace are
|
||||
"consumer" routes, and these routing rules are only applied to outbound
|
||||
connections originating from the same namespace as the Route, for which
|
||||
the intended destination of the connections are a Service targeted as a
|
||||
ParentRef of the Route.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Support: Core
|
||||
maxLength: 63
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
|
||||
type: string
|
||||
port:
|
||||
description: |-
|
||||
Port is the network port this Route targets. It can be interpreted
|
||||
differently based on the type of parent resource.
|
||||
|
||||
When the parent resource is a Gateway, this targets all listeners
|
||||
listening on the specified port that also support this kind of Route(and
|
||||
select this Route). It's not recommended to set `Port` unless the
|
||||
networking behaviors specified in a Route must apply to a specific port
|
||||
as opposed to a listener(s) whose port(s) may be changed. When both Port
|
||||
and SectionName are specified, the name and port of the selected listener
|
||||
must match both specified values.
|
||||
|
||||
<gateway:experimental:description>
|
||||
When the parent resource is a Service, this targets a specific port in the
|
||||
Service spec. When both Port (experimental) and SectionName are specified,
|
||||
the name and port of the selected port must match both specified values.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Implementations MAY choose to support other parent resources.
|
||||
Implementations supporting other types of parent resources MUST clearly
|
||||
document how/if Port is interpreted.
|
||||
|
||||
For the purpose of status, an attachment is considered successful as
|
||||
long as the parent resource accepts it partially. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
|
||||
from the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route,
|
||||
the Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Extended
|
||||
format: int32
|
||||
maximum: 65535
|
||||
minimum: 1
|
||||
type: integer
|
||||
sectionName:
|
||||
description: |-
|
||||
SectionName is the name of a section within the target resource. In the
|
||||
following resources, SectionName is interpreted as the following:
|
||||
|
||||
* Gateway: Listener name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
* Service: Port name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
|
||||
Implementations MAY choose to support attaching Routes to other resources.
|
||||
If that is the case, they MUST clearly document how SectionName is
|
||||
interpreted.
|
||||
|
||||
When unspecified (empty string), this will reference the entire resource.
|
||||
For the purpose of status, an attachment is considered successful if at
|
||||
least one section in the parent resource accepts it. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
|
||||
the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route, the
|
||||
Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
required:
|
||||
- conditions
|
||||
- controllerName
|
||||
- parentRef
|
||||
type: object
|
||||
maxItems: 32
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
routeRef:
|
||||
description: Reference to the route created for this tenant.
|
||||
properties:
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
required:
|
||||
- parents
|
||||
type: object
|
||||
ingress:
|
||||
description: KubernetesIngressStatus defines the status for the Tenant Control Plane Ingress in the management cluster.
|
||||
properties:
|
||||
|
||||
@@ -42,6 +42,28 @@
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- gateways
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- grpcroutes
|
||||
- httproutes
|
||||
- tlsroutes
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- kamaji.clastix.io
|
||||
resources:
|
||||
|
||||
@@ -6708,6 +6708,178 @@ spec:
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
gateway:
|
||||
description: Defining the options for an Optional Gateway which will expose API Server of the Tenant Control Plane
|
||||
properties:
|
||||
additionalMetadata:
|
||||
description: AdditionalMetadata to add Labels and Annotations support.
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
hostname:
|
||||
description: Hostname is an optional field which will be used as a route hostname.
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
|
||||
type: string
|
||||
parentRefs:
|
||||
description: GatewayParentRefs is the class of the Gateway resource to use.
|
||||
items:
|
||||
description: |-
|
||||
ParentReference identifies an API object (usually a Gateway) that can be considered
|
||||
a parent of this resource (usually a route). There are two kinds of parent resources
|
||||
with "Core" support:
|
||||
|
||||
* Gateway (Gateway conformance profile)
|
||||
* Service (Mesh conformance profile, ClusterIP Services only)
|
||||
|
||||
This API may be extended in the future to support additional kinds of parent
|
||||
resources.
|
||||
|
||||
The API object must be valid in the cluster; the Group and Kind must
|
||||
be registered in the cluster for this reference to be valid.
|
||||
properties:
|
||||
group:
|
||||
default: gateway.networking.k8s.io
|
||||
description: |-
|
||||
Group is the group of the referent.
|
||||
When unspecified, "gateway.networking.k8s.io" is inferred.
|
||||
To set the core API group (such as for a "Service" kind referent),
|
||||
Group must be explicitly set to "" (empty string).
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
|
||||
type: string
|
||||
kind:
|
||||
default: Gateway
|
||||
description: |-
|
||||
Kind is kind of the referent.
|
||||
|
||||
There are two kinds of parent resources with "Core" support:
|
||||
|
||||
* Gateway (Gateway conformance profile)
|
||||
* Service (Mesh conformance profile, ClusterIP Services only)
|
||||
|
||||
Support for other resources is Implementation-Specific.
|
||||
maxLength: 63
|
||||
minLength: 1
|
||||
pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
|
||||
type: string
|
||||
name:
|
||||
description: |-
|
||||
Name is the name of the referent.
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
type: string
|
||||
namespace:
|
||||
description: |-
|
||||
Namespace is the namespace of the referent. When unspecified, this refers
|
||||
to the local namespace of the Route.
|
||||
|
||||
Note that there are specific rules for ParentRefs which cross namespace
|
||||
boundaries. Cross-namespace references are only valid if they are explicitly
|
||||
allowed by something in the namespace they are referring to. For example:
|
||||
Gateway has the AllowedRoutes field, and ReferenceGrant provides a
|
||||
generic way to enable any other kind of cross-namespace reference.
|
||||
|
||||
<gateway:experimental:description>
|
||||
ParentRefs from a Route to a Service in the same namespace are "producer"
|
||||
routes, which apply default routing rules to inbound connections from
|
||||
any namespace to the Service.
|
||||
|
||||
ParentRefs from a Route to a Service in a different namespace are
|
||||
"consumer" routes, and these routing rules are only applied to outbound
|
||||
connections originating from the same namespace as the Route, for which
|
||||
the intended destination of the connections are a Service targeted as a
|
||||
ParentRef of the Route.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Support: Core
|
||||
maxLength: 63
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
|
||||
type: string
|
||||
port:
|
||||
description: |-
|
||||
Port is the network port this Route targets. It can be interpreted
|
||||
differently based on the type of parent resource.
|
||||
|
||||
When the parent resource is a Gateway, this targets all listeners
|
||||
listening on the specified port that also support this kind of Route(and
|
||||
select this Route). It's not recommended to set `Port` unless the
|
||||
networking behaviors specified in a Route must apply to a specific port
|
||||
as opposed to a listener(s) whose port(s) may be changed. When both Port
|
||||
and SectionName are specified, the name and port of the selected listener
|
||||
must match both specified values.
|
||||
|
||||
<gateway:experimental:description>
|
||||
When the parent resource is a Service, this targets a specific port in the
|
||||
Service spec. When both Port (experimental) and SectionName are specified,
|
||||
the name and port of the selected port must match both specified values.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Implementations MAY choose to support other parent resources.
|
||||
Implementations supporting other types of parent resources MUST clearly
|
||||
document how/if Port is interpreted.
|
||||
|
||||
For the purpose of status, an attachment is considered successful as
|
||||
long as the parent resource accepts it partially. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
|
||||
from the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route,
|
||||
the Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Extended
|
||||
format: int32
|
||||
maximum: 65535
|
||||
minimum: 1
|
||||
type: integer
|
||||
sectionName:
|
||||
description: |-
|
||||
SectionName is the name of a section within the target resource. In the
|
||||
following resources, SectionName is interpreted as the following:
|
||||
|
||||
* Gateway: Listener name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
* Service: Port name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
|
||||
Implementations MAY choose to support attaching Routes to other resources.
|
||||
If that is the case, they MUST clearly document how SectionName is
|
||||
interpreted.
|
||||
|
||||
When unspecified (empty string), this will reference the entire resource.
|
||||
For the purpose of status, an attachment is considered successful if at
|
||||
least one section in the parent resource accepts it. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
|
||||
the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route, the
|
||||
Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
ingress:
|
||||
description: Defining the options for an Optional Ingress which will expose API Server of the Tenant Control Plane
|
||||
properties:
|
||||
@@ -6809,6 +6981,9 @@ spec:
|
||||
required:
|
||||
- service
|
||||
type: object
|
||||
x-kubernetes-validations:
|
||||
- message: using both ingress and gateway is not supported
|
||||
rule: '!(has(self.ingress) && has(self.gateway))'
|
||||
dataStore:
|
||||
description: |-
|
||||
DataStore specifies the DataStore that should be used to store the Kubernetes data for the given Tenant Control Plane.
|
||||
@@ -6818,6 +6993,19 @@ spec:
|
||||
Migration from one DataStore to another backed by the same Driver is possible. See: https://kamaji.clastix.io/guides/datastore-migration/
|
||||
Migration from one DataStore to another backed by a different Driver is not supported.
|
||||
type: string
|
||||
dataStoreOverrides:
|
||||
description: DataStoreOverride defines which kubernetes resources will be stored in dedicated datastores.
|
||||
items:
|
||||
description: DataStoreOverride defines which kubernetes resource will be stored in a dedicated datastore.
|
||||
properties:
|
||||
dataStore:
|
||||
description: DataStore specifies the DataStore that should be used to store the Kubernetes data for the given Resource.
|
||||
type: string
|
||||
resource:
|
||||
description: Resource specifies which kubernetes resource to target.
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
dataStoreSchema:
|
||||
description: |-
|
||||
DataStoreSchema allows to specify the name of the database (for relational DataStores) or the key prefix (for etcd). This
|
||||
@@ -7559,6 +7747,383 @@ spec:
|
||||
- namespace
|
||||
- selector
|
||||
type: object
|
||||
gateway:
|
||||
description: KubernetesGatewayStatus defines the status for the Tenant Control Plane Gateway in the management cluster.
|
||||
properties:
|
||||
accessPoints:
|
||||
description: A list of valid access points that the route exposes.
|
||||
items:
|
||||
properties:
|
||||
port:
|
||||
format: int32
|
||||
type: integer
|
||||
type:
|
||||
description: |-
|
||||
AddressType defines how a network address is represented as a text string.
|
||||
This may take two possible forms:
|
||||
|
||||
* A predefined CamelCase string identifier (currently limited to `IPAddress` or `Hostname`)
|
||||
* A domain-prefixed string identifier (like `acme.io/CustomAddressType`)
|
||||
|
||||
Values `IPAddress` and `Hostname` have Extended support.
|
||||
|
||||
The `NamedAddress` value has been deprecated in favor of implementation
|
||||
specific domain-prefixed strings.
|
||||
|
||||
All other values, including domain-prefixed values have Implementation-specific support,
|
||||
which are used in implementation-specific behaviors. Support for additional
|
||||
predefined CamelCase identifiers may be added in future releases.
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^Hostname|IPAddress|NamedAddress|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
|
||||
type: string
|
||||
urls:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
value:
|
||||
type: string
|
||||
required:
|
||||
- port
|
||||
- type
|
||||
- value
|
||||
type: object
|
||||
type: array
|
||||
parents:
|
||||
description: |-
|
||||
Parents is a list of parent resources (usually Gateways) that are
|
||||
associated with the route, and the status of the route with respect to
|
||||
each parent. When this route attaches to a parent, the controller that
|
||||
manages the parent must add an entry to this list when the controller
|
||||
first sees the route and should update the entry as appropriate when the
|
||||
route or gateway is modified.
|
||||
|
||||
Note that parent references that cannot be resolved by an implementation
|
||||
of this API will not be added to this list. Implementations of this API
|
||||
can only populate Route status for the Gateways/parent resources they are
|
||||
responsible for.
|
||||
|
||||
A maximum of 32 Gateways will be represented in this list. An empty list
|
||||
means the route has not been attached to any Gateway.
|
||||
|
||||
<gateway:util:excludeFromCRD>
|
||||
Notes for implementors:
|
||||
|
||||
While parents is not a listType `map`, this is due to the fact that the
|
||||
list key is not scalar, and Kubernetes is unable to represent this.
|
||||
|
||||
Parent status MUST be considered to be namespaced by the combination of
|
||||
the parentRef and controllerName fields, and implementations should keep
|
||||
the following rules in mind when updating this status:
|
||||
|
||||
* Implementations MUST update only entries that have a matching value of
|
||||
`controllerName` for that implementation.
|
||||
* Implementations MUST NOT update entries with non-matching `controllerName`
|
||||
fields.
|
||||
* Implementations MUST treat each `parentRef`` in the Route separately and
|
||||
update its status based on the relationship with that parent.
|
||||
* Implementations MUST perform a read-modify-write cycle on this field
|
||||
before modifying it. That is, when modifying this field, implementations
|
||||
must be confident they have fetched the most recent version of this field,
|
||||
and ensure that changes they make are on that recent version.
|
||||
|
||||
</gateway:util:excludeFromCRD>
|
||||
items:
|
||||
description: |-
|
||||
RouteParentStatus describes the status of a route with respect to an
|
||||
associated Parent.
|
||||
properties:
|
||||
conditions:
|
||||
description: |-
|
||||
Conditions describes the status of the route with respect to the Gateway.
|
||||
Note that the route's availability is also subject to the Gateway's own
|
||||
status conditions and listener status.
|
||||
|
||||
If the Route's ParentRef specifies an existing Gateway that supports
|
||||
Routes of this kind AND that Gateway's controller has sufficient access,
|
||||
then that Gateway's controller MUST set the "Accepted" condition on the
|
||||
Route, to indicate whether the route has been accepted or rejected by the
|
||||
Gateway, and why.
|
||||
|
||||
A Route MUST be considered "Accepted" if at least one of the Route's
|
||||
rules is implemented by the Gateway.
|
||||
|
||||
There are a number of cases where the "Accepted" condition may not be set
|
||||
due to lack of controller visibility, that includes when:
|
||||
|
||||
* The Route refers to a nonexistent parent.
|
||||
* The Route is of a type that the controller does not support.
|
||||
* The Route is in a namespace the controller does not have access to.
|
||||
|
||||
<gateway:util:excludeFromCRD>
|
||||
|
||||
Notes for implementors:
|
||||
|
||||
Conditions are a listType `map`, which means that they function like a
|
||||
map with a key of the `type` field _in the k8s apiserver_.
|
||||
|
||||
This means that implementations must obey some rules when updating this
|
||||
section.
|
||||
|
||||
* Implementations MUST perform a read-modify-write cycle on this field
|
||||
before modifying it. That is, when modifying this field, implementations
|
||||
must be confident they have fetched the most recent version of this field,
|
||||
and ensure that changes they make are on that recent version.
|
||||
* Implementations MUST NOT remove or reorder Conditions that they are not
|
||||
directly responsible for. For example, if an implementation sees a Condition
|
||||
with type `special.io/SomeField`, it MUST NOT remove, change or update that
|
||||
Condition.
|
||||
* Implementations MUST always _merge_ changes into Conditions of the same Type,
|
||||
rather than creating more than one Condition of the same Type.
|
||||
* Implementations MUST always update the `observedGeneration` field of the
|
||||
Condition to the `metadata.generation` of the Gateway at the time of update creation.
|
||||
* If the `observedGeneration` of a Condition is _greater than_ the value the
|
||||
implementation knows about, then it MUST NOT perform the update on that Condition,
|
||||
but must wait for a future reconciliation and status update. (The assumption is that
|
||||
the implementation's copy of the object is stale and an update will be re-triggered
|
||||
if relevant.)
|
||||
|
||||
</gateway:util:excludeFromCRD>
|
||||
items:
|
||||
description: Condition contains details for one aspect of the current state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False, Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
maxItems: 8
|
||||
minItems: 1
|
||||
type: array
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
controllerName:
|
||||
description: |-
|
||||
ControllerName is a domain/path string that indicates the name of the
|
||||
controller that wrote this status. This corresponds with the
|
||||
controllerName field on GatewayClass.
|
||||
|
||||
Example: "example.net/gateway-controller".
|
||||
|
||||
The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are
|
||||
valid Kubernetes names
|
||||
(https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
|
||||
|
||||
Controllers MUST populate this field when writing status. Controllers should ensure that
|
||||
entries to status populated with their ControllerName are cleaned up when they are no
|
||||
longer necessary.
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
|
||||
type: string
|
||||
parentRef:
|
||||
description: |-
|
||||
ParentRef corresponds with a ParentRef in the spec that this
|
||||
RouteParentStatus struct describes the status of.
|
||||
properties:
|
||||
group:
|
||||
default: gateway.networking.k8s.io
|
||||
description: |-
|
||||
Group is the group of the referent.
|
||||
When unspecified, "gateway.networking.k8s.io" is inferred.
|
||||
To set the core API group (such as for a "Service" kind referent),
|
||||
Group must be explicitly set to "" (empty string).
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
|
||||
type: string
|
||||
kind:
|
||||
default: Gateway
|
||||
description: |-
|
||||
Kind is kind of the referent.
|
||||
|
||||
There are two kinds of parent resources with "Core" support:
|
||||
|
||||
* Gateway (Gateway conformance profile)
|
||||
* Service (Mesh conformance profile, ClusterIP Services only)
|
||||
|
||||
Support for other resources is Implementation-Specific.
|
||||
maxLength: 63
|
||||
minLength: 1
|
||||
pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
|
||||
type: string
|
||||
name:
|
||||
description: |-
|
||||
Name is the name of the referent.
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
type: string
|
||||
namespace:
|
||||
description: |-
|
||||
Namespace is the namespace of the referent. When unspecified, this refers
|
||||
to the local namespace of the Route.
|
||||
|
||||
Note that there are specific rules for ParentRefs which cross namespace
|
||||
boundaries. Cross-namespace references are only valid if they are explicitly
|
||||
allowed by something in the namespace they are referring to. For example:
|
||||
Gateway has the AllowedRoutes field, and ReferenceGrant provides a
|
||||
generic way to enable any other kind of cross-namespace reference.
|
||||
|
||||
<gateway:experimental:description>
|
||||
ParentRefs from a Route to a Service in the same namespace are "producer"
|
||||
routes, which apply default routing rules to inbound connections from
|
||||
any namespace to the Service.
|
||||
|
||||
ParentRefs from a Route to a Service in a different namespace are
|
||||
"consumer" routes, and these routing rules are only applied to outbound
|
||||
connections originating from the same namespace as the Route, for which
|
||||
the intended destination of the connections are a Service targeted as a
|
||||
ParentRef of the Route.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Support: Core
|
||||
maxLength: 63
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
|
||||
type: string
|
||||
port:
|
||||
description: |-
|
||||
Port is the network port this Route targets. It can be interpreted
|
||||
differently based on the type of parent resource.
|
||||
|
||||
When the parent resource is a Gateway, this targets all listeners
|
||||
listening on the specified port that also support this kind of Route(and
|
||||
select this Route). It's not recommended to set `Port` unless the
|
||||
networking behaviors specified in a Route must apply to a specific port
|
||||
as opposed to a listener(s) whose port(s) may be changed. When both Port
|
||||
and SectionName are specified, the name and port of the selected listener
|
||||
must match both specified values.
|
||||
|
||||
<gateway:experimental:description>
|
||||
When the parent resource is a Service, this targets a specific port in the
|
||||
Service spec. When both Port (experimental) and SectionName are specified,
|
||||
the name and port of the selected port must match both specified values.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Implementations MAY choose to support other parent resources.
|
||||
Implementations supporting other types of parent resources MUST clearly
|
||||
document how/if Port is interpreted.
|
||||
|
||||
For the purpose of status, an attachment is considered successful as
|
||||
long as the parent resource accepts it partially. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
|
||||
from the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route,
|
||||
the Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Extended
|
||||
format: int32
|
||||
maximum: 65535
|
||||
minimum: 1
|
||||
type: integer
|
||||
sectionName:
|
||||
description: |-
|
||||
SectionName is the name of a section within the target resource. In the
|
||||
following resources, SectionName is interpreted as the following:
|
||||
|
||||
* Gateway: Listener name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
* Service: Port name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
|
||||
Implementations MAY choose to support attaching Routes to other resources.
|
||||
If that is the case, they MUST clearly document how SectionName is
|
||||
interpreted.
|
||||
|
||||
When unspecified (empty string), this will reference the entire resource.
|
||||
For the purpose of status, an attachment is considered successful if at
|
||||
least one section in the parent resource accepts it. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
|
||||
the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route, the
|
||||
Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Core
|
||||
maxLength: 253
|
||||
minLength: 1
|
||||
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
required:
|
||||
- conditions
|
||||
- controllerName
|
||||
- parentRef
|
||||
type: object
|
||||
maxItems: 32
|
||||
type: array
|
||||
x-kubernetes-list-type: atomic
|
||||
routeRef:
|
||||
description: Reference to the route created for this tenant.
|
||||
properties:
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
required:
|
||||
- parents
|
||||
type: object
|
||||
ingress:
|
||||
description: KubernetesIngressStatus defines the status for the Tenant Control Plane Ingress in the management cluster.
|
||||
properties:
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/klog/v2"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
@@ -34,6 +35,7 @@ import (
|
||||
"github.com/clastix/kamaji/internal"
|
||||
"github.com/clastix/kamaji/internal/builders/controlplane"
|
||||
datastoreutils "github.com/clastix/kamaji/internal/datastore/utils"
|
||||
"github.com/clastix/kamaji/internal/utilities"
|
||||
"github.com/clastix/kamaji/internal/webhook"
|
||||
"github.com/clastix/kamaji/internal/webhook/handlers"
|
||||
"github.com/clastix/kamaji/internal/webhook/routes"
|
||||
@@ -146,6 +148,13 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
discoveryClient, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig())
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to create discovery client")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
reconciler := &controllers.TenantControlPlaneReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
APIReader: mgr.GetAPIReader(),
|
||||
@@ -163,9 +172,10 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
|
||||
KamajiService: managerServiceName,
|
||||
KamajiMigrateImage: migrateJobImage,
|
||||
MaxConcurrentReconciles: maxConcurrentReconciles,
|
||||
DiscoveryClient: discoveryClient,
|
||||
}
|
||||
|
||||
if err = reconciler.SetupWithManager(mgr); err != nil {
|
||||
if err = reconciler.SetupWithManager(ctx, mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "Namespace")
|
||||
|
||||
return err
|
||||
@@ -215,6 +225,15 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only requires to look for the core api group.
|
||||
if utilities.AreGatewayResourcesAvailable(ctx, mgr.GetClient(), discoveryClient) {
|
||||
if err = (&kamajiv1alpha1.GatewayListener{}).SetupWithManager(ctx, mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create indexer", "indexer", "GatewayListener")
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = webhook.Register(mgr, map[routes.Route][]handlers.Handler{
|
||||
routes.TenantControlPlaneMigrate{}: {
|
||||
handlers.Freeze{},
|
||||
@@ -244,6 +263,10 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
|
||||
},
|
||||
handlers.TenantControlPlaneServiceCIDR{},
|
||||
handlers.TenantControlPlaneLoadBalancerSourceRanges{},
|
||||
handlers.TenantControlPlaneGatewayValidation{
|
||||
Client: mgr.GetClient(),
|
||||
DiscoveryClient: discoveryClient,
|
||||
},
|
||||
},
|
||||
routes.TenantControlPlaneTelemetry{}: {
|
||||
handlers.TenantControlPlaneTelemetry{
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
appsv1 "k8s.io/kubernetes/pkg/apis/apps/v1"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
)
|
||||
@@ -22,6 +24,10 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
utilruntime.Must(kamajiv1alpha1.AddToScheme(scheme))
|
||||
utilruntime.Must(appsv1.RegisterDefaults(scheme))
|
||||
// NOTE: This will succeed even if Gateway API is not installed in the cluster.
|
||||
// Only registers the go types.
|
||||
utilruntime.Must(gatewayv1.Install(scheme))
|
||||
utilruntime.Must(gatewayv1alpha2.Install(scheme))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/google/uuid"
|
||||
k8stypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/discovery"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
|
||||
@@ -20,20 +22,24 @@ import (
|
||||
"github.com/clastix/kamaji/internal/resources"
|
||||
ds "github.com/clastix/kamaji/internal/resources/datastore"
|
||||
"github.com/clastix/kamaji/internal/resources/konnectivity"
|
||||
"github.com/clastix/kamaji/internal/utilities"
|
||||
)
|
||||
|
||||
type GroupResourceBuilderConfiguration struct {
|
||||
client client.Client
|
||||
log logr.Logger
|
||||
tcpReconcilerConfig TenantControlPlaneReconcilerConfig
|
||||
tenantControlPlane kamajiv1alpha1.TenantControlPlane
|
||||
ExpirationThreshold time.Duration
|
||||
Connection datastore.Connection
|
||||
DataStore kamajiv1alpha1.DataStore
|
||||
KamajiNamespace string
|
||||
KamajiServiceAccount string
|
||||
KamajiService string
|
||||
KamajiMigrateImage string
|
||||
client client.Client
|
||||
log logr.Logger
|
||||
tcpReconcilerConfig TenantControlPlaneReconcilerConfig
|
||||
tenantControlPlane kamajiv1alpha1.TenantControlPlane
|
||||
ExpirationThreshold time.Duration
|
||||
Connection datastore.Connection
|
||||
DataStore kamajiv1alpha1.DataStore
|
||||
DataStoreOverrides []builder.DataStoreOverrides
|
||||
DataStoreOverriedsConnections map[string]datastore.Connection
|
||||
KamajiNamespace string
|
||||
KamajiServiceAccount string
|
||||
KamajiService string
|
||||
KamajiMigrateImage string
|
||||
DiscoveryClient discovery.DiscoveryInterface
|
||||
}
|
||||
|
||||
type GroupDeletableResourceBuilderConfiguration struct {
|
||||
@@ -48,8 +54,29 @@ type GroupDeletableResourceBuilderConfiguration struct {
|
||||
// GetResources returns a list of resources that will be used to provide tenant control planes
|
||||
// Currently there is only a default approach
|
||||
// TODO: the idea of this function is to become a factory to return the group of resources according to the given configuration.
|
||||
func GetResources(config GroupResourceBuilderConfiguration) []resources.Resource {
|
||||
return getDefaultResources(config)
|
||||
func GetResources(ctx context.Context, config GroupResourceBuilderConfiguration) []resources.Resource {
|
||||
resources := []resources.Resource{}
|
||||
|
||||
resources = append(resources, getDataStoreMigratingResources(config.client, config.KamajiNamespace, config.KamajiMigrateImage, config.KamajiServiceAccount, config.KamajiService)...)
|
||||
resources = append(resources, getUpgradeResources(config.client)...)
|
||||
resources = append(resources, getKubernetesServiceResources(config.client)...)
|
||||
resources = append(resources, getKubeadmConfigResources(config.client, getTmpDirectory(config.tcpReconcilerConfig.TmpBaseDirectory, config.tenantControlPlane), config.DataStore)...)
|
||||
resources = append(resources, getKubernetesCertificatesResources(config.client, config.tcpReconcilerConfig, config.tenantControlPlane)...)
|
||||
resources = append(resources, getKubeconfigResources(config.client, config.tcpReconcilerConfig, config.tenantControlPlane)...)
|
||||
resources = append(resources, getKubernetesStorageResources(config.client, config.Connection, config.DataStore, config.ExpirationThreshold)...)
|
||||
resources = append(resources, getKubernetesAdditionalStorageResources(config.client, config.DataStoreOverriedsConnections, config.DataStoreOverrides, config.ExpirationThreshold)...)
|
||||
resources = append(resources, getKonnectivityServerRequirementsResources(config.client, config.ExpirationThreshold)...)
|
||||
resources = append(resources, getKubernetesDeploymentResources(config.client, config.tcpReconcilerConfig, config.DataStore, config.DataStoreOverrides)...)
|
||||
resources = append(resources, getKonnectivityServerPatchResources(config.client)...)
|
||||
resources = append(resources, getDataStoreMigratingCleanup(config.client, config.KamajiNamespace)...)
|
||||
resources = append(resources, getKubernetesIngressResources(config.client)...)
|
||||
|
||||
// Conditionally add Gateway resources
|
||||
if utilities.AreGatewayResourcesAvailable(ctx, config.client, config.DiscoveryClient) {
|
||||
resources = append(resources, getKubernetesGatewayResources(config.client)...)
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
// GetDeletableResources returns a list of resources that have to be deleted when tenant control planes are deleted
|
||||
@@ -73,23 +100,6 @@ func GetDeletableResources(tcp *kamajiv1alpha1.TenantControlPlane, config GroupD
|
||||
return res
|
||||
}
|
||||
|
||||
func getDefaultResources(config GroupResourceBuilderConfiguration) []resources.Resource {
|
||||
resources := getDataStoreMigratingResources(config.client, config.KamajiNamespace, config.KamajiMigrateImage, config.KamajiServiceAccount, config.KamajiService)
|
||||
resources = append(resources, getUpgradeResources(config.client)...)
|
||||
resources = append(resources, getKubernetesServiceResources(config.client)...)
|
||||
resources = append(resources, getKubeadmConfigResources(config.client, getTmpDirectory(config.tcpReconcilerConfig.TmpBaseDirectory, config.tenantControlPlane), config.DataStore)...)
|
||||
resources = append(resources, getKubernetesCertificatesResources(config.client, config.tcpReconcilerConfig, config.tenantControlPlane)...)
|
||||
resources = append(resources, getKubeconfigResources(config.client, config.tcpReconcilerConfig, config.tenantControlPlane)...)
|
||||
resources = append(resources, getKubernetesStorageResources(config.client, config.Connection, config.DataStore, config.ExpirationThreshold)...)
|
||||
resources = append(resources, getKonnectivityServerRequirementsResources(config.client, config.ExpirationThreshold)...)
|
||||
resources = append(resources, getKubernetesDeploymentResources(config.client, config.tcpReconcilerConfig, config.DataStore)...)
|
||||
resources = append(resources, getKonnectivityServerPatchResources(config.client)...)
|
||||
resources = append(resources, getDataStoreMigratingCleanup(config.client, config.KamajiNamespace)...)
|
||||
resources = append(resources, getKubernetesIngressResources(config.client)...)
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
func getDataStoreMigratingCleanup(c client.Client, kamajiNamespace string) []resources.Resource {
|
||||
return []resources.Resource{
|
||||
&ds.Migrate{
|
||||
@@ -128,6 +138,14 @@ func getKubernetesServiceResources(c client.Client) []resources.Resource {
|
||||
}
|
||||
}
|
||||
|
||||
func getKubernetesGatewayResources(c client.Client) []resources.Resource {
|
||||
return []resources.Resource{
|
||||
&resources.KubernetesGatewayResource{
|
||||
Client: c,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getKubeadmConfigResources(c client.Client, tmpDirectory string, dataStore kamajiv1alpha1.DataStore) []resources.Resource {
|
||||
var endpoints []string
|
||||
|
||||
@@ -237,12 +255,42 @@ func getKubernetesStorageResources(c client.Client, dbConnection datastore.Conne
|
||||
}
|
||||
}
|
||||
|
||||
func getKubernetesDeploymentResources(c client.Client, tcpReconcilerConfig TenantControlPlaneReconcilerConfig, dataStore kamajiv1alpha1.DataStore) []resources.Resource {
|
||||
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,
|
||||
},
|
||||
&ds.Config{
|
||||
Client: c,
|
||||
ConnString: dbConnections[dso.Resource].GetConnectionString(),
|
||||
DataStore: datastore,
|
||||
IsOverride: true,
|
||||
},
|
||||
&ds.Setup{
|
||||
Client: c,
|
||||
Connection: dbConnections[dso.Resource],
|
||||
DataStore: datastore,
|
||||
},
|
||||
&ds.Certificate{
|
||||
Client: c,
|
||||
DataStore: datastore,
|
||||
CertExpirationThreshold: threshold,
|
||||
})
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func getKubernetesDeploymentResources(c client.Client, tcpReconcilerConfig TenantControlPlaneReconcilerConfig, dataStore kamajiv1alpha1.DataStore, dataStoreOverrides []builder.DataStoreOverrides) []resources.Resource {
|
||||
return []resources.Resource{
|
||||
&resources.KubernetesDeploymentResource{
|
||||
Client: c,
|
||||
DataStore: dataStore,
|
||||
KineContainerImage: tcpReconcilerConfig.KineContainerImage,
|
||||
DataStoreOverrides: dataStoreOverrides,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
k8stypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"k8s.io/utils/clock"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
@@ -30,13 +31,17 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
"github.com/clastix/kamaji/controllers/finalizers"
|
||||
"github.com/clastix/kamaji/controllers/utils"
|
||||
controlplanebuilder "github.com/clastix/kamaji/internal/builders/controlplane"
|
||||
"github.com/clastix/kamaji/internal/datastore"
|
||||
kamajierrors "github.com/clastix/kamaji/internal/errors"
|
||||
"github.com/clastix/kamaji/internal/resources"
|
||||
"github.com/clastix/kamaji/internal/utilities"
|
||||
)
|
||||
|
||||
// TenantControlPlaneReconciler reconciles a TenantControlPlane object.
|
||||
@@ -51,6 +56,7 @@ type TenantControlPlaneReconciler struct {
|
||||
KamajiMigrateImage string
|
||||
MaxConcurrentReconciles int
|
||||
ReconcileTimeout time.Duration
|
||||
DiscoveryClient discovery.DiscoveryInterface
|
||||
// CertificateChan is the channel used by the CertificateLifecycleController that is checking for
|
||||
// certificates and kubeconfig user certs validity: a generic event for the given TCP will be triggered
|
||||
// once the validity threshold for the given certificate is reached.
|
||||
@@ -76,6 +82,10 @@ type TenantControlPlaneReconcilerConfig struct {
|
||||
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;delete
|
||||
//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=httproutes,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=grpcroutes,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=tlsroutes,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways,verbs=get;list;watch
|
||||
|
||||
func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
log := log.FromContext(ctx)
|
||||
@@ -148,6 +158,25 @@ func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R
|
||||
}
|
||||
defer dsConnection.Close()
|
||||
|
||||
dso, err := r.dataStoreOverride(ctx, tenantControlPlane)
|
||||
if err != nil {
|
||||
log.Error(err, "cannot retrieve the DataStoreOverrides for the given instance")
|
||||
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
dsoConnections := make(map[string]datastore.Connection, len(dso))
|
||||
for _, ds := range dso {
|
||||
dsoConnection, err := datastore.NewStorageConnection(ctx, r.Client, ds.DataStore)
|
||||
if err != nil {
|
||||
log.Error(err, "cannot generate the DataStoreOverride connection for the given instance")
|
||||
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
defer dsoConnection.Close()
|
||||
|
||||
dsoConnections[ds.Resource] = dsoConnection
|
||||
}
|
||||
|
||||
if markedToBeDeleted && controllerutil.ContainsFinalizer(tenantControlPlane, finalizers.DatastoreFinalizer) {
|
||||
log.Info("marked for deletion, performing clean-up")
|
||||
|
||||
@@ -174,18 +203,21 @@ func (r *TenantControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R
|
||||
}
|
||||
|
||||
groupResourceBuilderConfiguration := GroupResourceBuilderConfiguration{
|
||||
client: r.Client,
|
||||
log: log,
|
||||
tcpReconcilerConfig: r.Config,
|
||||
tenantControlPlane: *tenantControlPlane,
|
||||
Connection: dsConnection,
|
||||
DataStore: *ds,
|
||||
KamajiNamespace: r.KamajiNamespace,
|
||||
KamajiServiceAccount: r.KamajiServiceAccount,
|
||||
KamajiService: r.KamajiService,
|
||||
KamajiMigrateImage: r.KamajiMigrateImage,
|
||||
client: r.Client,
|
||||
log: log,
|
||||
tcpReconcilerConfig: r.Config,
|
||||
tenantControlPlane: *tenantControlPlane,
|
||||
Connection: dsConnection,
|
||||
DataStore: *ds,
|
||||
DataStoreOverrides: dso,
|
||||
DataStoreOverriedsConnections: dsoConnections,
|
||||
KamajiNamespace: r.KamajiNamespace,
|
||||
KamajiServiceAccount: r.KamajiServiceAccount,
|
||||
KamajiService: r.KamajiService,
|
||||
KamajiMigrateImage: r.KamajiMigrateImage,
|
||||
DiscoveryClient: r.DiscoveryClient,
|
||||
}
|
||||
registeredResources := GetResources(groupResourceBuilderConfiguration)
|
||||
registeredResources := GetResources(ctx, groupResourceBuilderConfiguration)
|
||||
|
||||
for _, resource := range registeredResources {
|
||||
result, err := resources.Handle(ctx, resource, tenantControlPlane)
|
||||
@@ -242,10 +274,10 @@ func (r *TenantControlPlaneReconciler) mutexSpec(obj client.Object) mutex.Spec {
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *TenantControlPlaneReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
func (r *TenantControlPlaneReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
|
||||
r.clock = clock.RealClock{}
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
controllerBuilder := ctrl.NewControllerManagedBy(mgr).
|
||||
WatchesRawSource(source.Channel(r.CertificateChan, handler.Funcs{GenericFunc: func(_ context.Context, genericEvent event.TypedGenericEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
w.AddRateLimited(ctrl.Request{
|
||||
NamespacedName: k8stypes.NamespacedName{
|
||||
@@ -295,7 +327,20 @@ func (r *TenantControlPlaneReconciler) SetupWithManager(mgr ctrl.Manager) error
|
||||
v, ok := labels["kamaji.clastix.io/component"]
|
||||
|
||||
return ok && v == "migrate"
|
||||
}))).
|
||||
})))
|
||||
|
||||
// Conditionally add Gateway API ownership if available
|
||||
if utilities.AreGatewayResourcesAvailable(ctx, r.Client, r.DiscoveryClient) {
|
||||
controllerBuilder = controllerBuilder.
|
||||
Owns(&gatewayv1.HTTPRoute{}).
|
||||
Owns(&gatewayv1.GRPCRoute{}).
|
||||
Owns(&gatewayv1alpha2.TLSRoute{}).
|
||||
Watches(&gatewayv1.Gateway{}, handler.EnqueueRequestsFromMapFunc(func(_ context.Context, object client.Object) []reconcile.Request {
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
return controllerBuilder.
|
||||
WithOptions(controller.Options{
|
||||
MaxConcurrentReconciles: r.MaxConcurrentReconciles,
|
||||
}).
|
||||
@@ -339,3 +384,21 @@ func (r *TenantControlPlaneReconciler) dataStore(ctx context.Context, tenantCont
|
||||
|
||||
return &ds, nil
|
||||
}
|
||||
|
||||
func (r *TenantControlPlaneReconciler) dataStoreOverride(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) ([]controlplanebuilder.DataStoreOverrides, error) {
|
||||
datastores := make([]controlplanebuilder.DataStoreOverrides, 0, len(tenantControlPlane.Spec.DataStoreOverrides))
|
||||
|
||||
for _, dso := range tenantControlPlane.Spec.DataStoreOverrides {
|
||||
var ds kamajiv1alpha1.DataStore
|
||||
if err := r.Client.Get(ctx, k8stypes.NamespacedName{Name: dso.DataStore}, &ds); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot retrieve *kamajiv1alpha.DataStore object")
|
||||
}
|
||||
if ds.Spec.Driver != kamajiv1alpha1.EtcdDriver {
|
||||
return nil, errors.New("DataStoreOverrides can only use ETCD driver")
|
||||
}
|
||||
|
||||
datastores = append(datastores, controlplanebuilder.DataStoreOverrides{Resource: dso.Resource, DataStore: ds})
|
||||
}
|
||||
|
||||
return datastores, nil
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
# Cluster Autoscaler
|
||||
|
||||
The [Cluster Autoscaler](https://github.com/kubernetes/autoscaler) is a tool that automatically adjusts the size of a Kubernetes cluster so that all pods have a place to run and there are no unneeded nodes.
|
||||
The [Cluster Autoscaler](https://github.com/kubernetes/autoscaler) is a tool that automatically adjusts the size of a Kubernetes cluster so that all pods have a place to run and no unneeded nodes remain.
|
||||
|
||||
When pods are unschedulable because there are not enough resources, Cluster Autoscaler scales up the cluster. When nodes are underutilized, Cluster Autoscaler scales down the cluster.
|
||||
When pods are unschedulable because there are not enough resources, the Cluster Autoscaler scales up the cluster. When nodes are underutilized, the Cluster Autoscaler scales the cluster down.
|
||||
|
||||
Cluster API supports the Cluster Autoscaler. See the [Cluster Autoscaler on Cluster API](https://cluster-api.sigs.k8s.io/tasks/automated-machine-management/autoscaling) for more information.
|
||||
|
||||
## Getting started with the Cluster Autoscaler on Kamaji
|
||||
|
||||
Kamaji supports the Cluster Autoscaler through Cluster API. There are several way to run the Cluster autoscaler with Cluster API. In this guide, we're leveraging the unique features of Kamaji to run the Cluster Autoscaler as part of Hosted Control Plane.
|
||||
Kamaji supports the Cluster Autoscaler through Cluster API. There are several ways to run the Cluster Autoscaler with Cluster API. In this guide, we leverage the unique features of Kamaji to run the Cluster Autoscaler as part of the Hosted Control Plane.
|
||||
|
||||
In other words, the Cluster Autoscaler is running as a pod in the Kamaji Management Cluster, side by side with the Tenant Control Plane pods, and connecting directly to the apiserver of the workload cluster, hiding sensitive data and information from the tenant: this can be done by mounting the kubeconfig of the tenant cluster in the Cluster Autoscaler pod.
|
||||
In other words, the Cluster Autoscaler runs as a pod in the Kamaji Management Cluster, alongside the Tenant Control Plane pods, and connects directly to the API server of the workload cluster. This approach hides sensitive data from the tenant. It works by mounting the kubeconfig of the tenant cluster into the Cluster Autoscaler pod.
|
||||
|
||||
### Create the workload cluster
|
||||
|
||||
Create a workload cluster using the Kamaji Control Plane Provider and the Infrastructure Provider of choice. The following example creates a workload cluster using the vSphere Infrastructure Provider:
|
||||
Create a workload cluster using the Kamaji Control Plane Provider and the Infrastructure Provider of your choice. The following example creates a workload cluster using the vSphere Infrastructure Provider.
|
||||
|
||||
The template file [`capi-kamaji-vsphere-autoscaler-template.yaml`](https://raw.githubusercontent.com/clastix/cluster-api-control-plane-provider-kamaji/master/templates/vsphere/capi-kamaji-vsphere-autoscaler-template.yaml) provides a full example of a cluster with autoscaler enabled. You can generate the cluster manifest using `clusterctl`.
|
||||
The template file [`capi-kamaji-vsphere-autoscaler-template.yaml`](https://raw.githubusercontent.com/clastix/cluster-api-control-plane-provider-kamaji/master/templates/vsphere/capi-kamaji-vsphere-autoscaler-template.yaml) provides a full example of a cluster with the autoscaler enabled. You can generate the cluster manifest using `clusterctl`.
|
||||
|
||||
Before you need to list all the variables in the template file:
|
||||
Before doing so, list all the variables in the template file:
|
||||
|
||||
```bash
|
||||
cat capi-kamaji-vsphere-autoscaler-template.yaml | clusterctl generate yaml --list-variables
|
||||
@@ -40,10 +40,10 @@ kubectl apply -f capi-kamaji-vsphere-cluster.yaml
|
||||
|
||||
### Install the Cluster Autoscaler
|
||||
|
||||
Install the Cluster Autoscaler via Helm in the Management Cluster, in the same namespace where workload cluster is deployed.
|
||||
Install the Cluster Autoscaler via Helm in the Management Cluster, in the same namespace where the workload cluster is deployed.
|
||||
|
||||
!!! info "Options for install Cluster Autoscaler"
|
||||
Cluster Autoscaler works on a single cluster: it means every cluster must have its own Cluster Autoscaler instance. This could be solved by leveraging on Project Sveltos automations, by deploying a Cluster Autoscaler instance for each Kamaji Cluster API instance.
|
||||
!!! info "Options for installing the Cluster Autoscaler"
|
||||
The Cluster Autoscaler works on a single cluster, meaning every cluster must have its own Cluster Autoscaler instance. This can be addressed by leveraging Project Sveltos automations to deploy a Cluster Autoscaler instance for each Kamaji Cluster API instance.
|
||||
|
||||
```bash
|
||||
helm repo add autoscaler https://kubernetes.github.io/autoscaler
|
||||
@@ -56,9 +56,9 @@ helm upgrade --install ${CLUSTER_NAME}-autoscaler autoscaler/cluster-autoscaler
|
||||
--set clusterAPIMode=kubeconfig-incluster
|
||||
```
|
||||
|
||||
The `autoDiscovery.labels` values are used to pick dynamically clusters to autoscale.
|
||||
The `autoDiscovery.labels` values are used to dynamically select clusters to autoscale.
|
||||
|
||||
Such labels must be set on the workload cluster, in the `Cluster` and `MachineDeployment` resources.
|
||||
These labels must be set on the workload cluster, specifically in the `Cluster` and `MachineDeployment` resources.
|
||||
|
||||
```yaml
|
||||
apiVersion: cluster.x-k8s.io/v1beta1
|
||||
@@ -79,6 +79,9 @@ metadata:
|
||||
# Cluster Autoscaler annotations
|
||||
cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "0"
|
||||
cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size: "6"
|
||||
capacity.cluster-autoscaler.kubernetes.io/cpu: "2" # YMMV
|
||||
capacity.cluster-autoscaler.kubernetes.io/memory: 4Gi # YMMV
|
||||
capacity.cluster-autoscaler.kubernetes.io/maxPods: "110" # YMMV
|
||||
labels:
|
||||
cluster.x-k8s.io/cluster-name: sample
|
||||
# Cluster Autoscaler labels
|
||||
@@ -90,10 +93,9 @@ metadata:
|
||||
# other Cluster API resources omitted for brevity
|
||||
```
|
||||
|
||||
|
||||
### Verify the Cluster Autoscaler
|
||||
|
||||
To verify the Cluster Autoscaler is working as expected, you can deploy a workload in the Tenant cluster with some CPU requirements in order to simulate workload requiring resources.
|
||||
To verify that the Cluster Autoscaler is working as expected, deploy a workload in the Tenant cluster with specific CPU requirements to simulate resource demand.
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
@@ -122,7 +124,51 @@ spec:
|
||||
cpu: 500m
|
||||
```
|
||||
|
||||
Apply the workload to the Tenant cluster and simulate the load spike by increasing the replicas. The Cluster Autoscaler should scale up the cluster to accommodate the workload. Cooldown time must be configured properly on a cluster basis.
|
||||
Apply the workload to the Tenant cluster and simulate a load spike by increasing the number of replicas. The Cluster Autoscaler should scale up the cluster to accommodate the workload. Cooldown times must be configured correctly on a per-cluster basis.
|
||||
|
||||
!!! warning "Possible Resource Wasting"
|
||||
With Cluster Autoscaler, new machines are automatically created in a very short time, ending up with some up-provisioning and potentially wasting resources. The official Cluster Autosclaler documentation must be understood to provide correct values according to the infrastructure and provisioning times.
|
||||
!!! warning "Possible Resource Wastage"
|
||||
With the Cluster Autoscaler, new machines may be created very quickly, which can lead to over-provisioning and potentially wasted resources. The official Cluster Autoscaler documentation should be consulted to configure appropriate values based on your infrastructure and provisioning times.
|
||||
|
||||
## `ProvisioningRequest` support
|
||||
|
||||
The [ProvisioningRequest](https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler-1.34.1/cluster-autoscaler/proposals/provisioning-request.md) introduces a Kubernetes-native way for Cluster Autoscaler to request new capacity without talking directly to cloud provider APIs.
|
||||
Instead of embedding provider-specific logic, the autoscaler simply describes the capacity it needs, and an external provisioner decides how to create the required nodes.
|
||||
This makes scaling portable across clouds, on-prem platforms, and custom provisioning systems, while greatly reducing complexity inside the autoscaler.
|
||||
|
||||
Once the cluster has been provisioned, install the `ProvisioningRequest` definition.
|
||||
|
||||
```
|
||||
kubectl kamaji kubeconfig get capi-quickstart-kubevirt > /tmp/capi-quickstart-kubevirt
|
||||
KUBECONFIG=/tmp/capi-quickstart-kubevirt kubectl apply -f https://raw.githubusercontent.com/kubernetes/autoscaler/refs/tags/cluster-autoscaler-1.34.1/cluster-autoscaler/apis/config/crd/autoscaling.x-k8s.io_provisioningrequests.yaml
|
||||
```
|
||||
|
||||
Proceed with the installation of Cluster Autoscaler by enabling some additional parameters: YMMV.
|
||||
|
||||
```yaml
|
||||
cloudProvider: clusterapi
|
||||
autoDiscovery:
|
||||
namespace: default
|
||||
labels:
|
||||
- autoscaling.x-k8s.io: enabled
|
||||
|
||||
clusterAPIKubeconfigSecret: capi-quickstart-kubeconfig
|
||||
clusterAPIMode: kubeconfig-incluster
|
||||
|
||||
extraArgs:
|
||||
enable-provisioning-requests: true
|
||||
kube-api-content-type: "application/json"
|
||||
cloud-config: /etc/kubernetes/management/kubeconfig
|
||||
|
||||
extraVolumeSecrets:
|
||||
# Mount the management kubeconfig to talk with the management cluster:
|
||||
# the in-rest configuration doesn't work
|
||||
management-kubeconfig:
|
||||
name: management-kubeconfig
|
||||
mountPath: /etc/kubernetes/management
|
||||
items:
|
||||
- key: kubeconfig
|
||||
path: kubeconfig
|
||||
```
|
||||
|
||||
The Cluster Autoscaler should be up and running, enabled to connect to the management and tenant cluster API Server:
|
||||
follow the [official example](https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler-1.34.1/cluster-autoscaler/FAQ.md#example-usage) from the repository to assess the `ProvisioningRequest` feature.
|
||||
|
||||
78
docs/content/guides/datastore-overrides.md
Normal file
78
docs/content/guides/datastore-overrides.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Datastore Overrides
|
||||
|
||||
Kamaji offers the possibility of having multiple ETCD clusters backing different resources of the k8s api server by configuring the [`--etcd-servers-overrides`](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/#:~:text=%2D%2Detcd%2Dservers%2Doverrides%20strings) flag. This feature can be useful for massive clusters to store resources with high churn in a dedicated ETCD cluster.
|
||||
|
||||
## Install Datastores
|
||||
|
||||
Create a self-signed cert-manager `ClusterIssuer`.
|
||||
```bash
|
||||
echo 'apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: self-signed
|
||||
spec:
|
||||
selfSigned: {}
|
||||
' | kubectl apply -f -
|
||||
```
|
||||
|
||||
Install two Datastores, a primary and a secondary that will be used for `/events` resources.
|
||||
```bash
|
||||
helm install etcd-primary clastix/kamaji-etcd -n kamaji-etcd --create-namespace \
|
||||
--set selfSignedCertificates.enabled=false \
|
||||
--set certManager.enabled=true \
|
||||
--set certManager.issuerRef.kind=ClusterIssuer \
|
||||
--set certManager.issuerRef.name=self-signed
|
||||
```
|
||||
|
||||
For the secondary Datastore, use the cert-manager CA created by the `etcd-primary` helm release.
|
||||
```bash
|
||||
helm install etcd-secondary clastix/kamaji-etcd -n kamaji-etcd --create-namespace \
|
||||
--set selfSignedCertificates.enabled=false \
|
||||
--set certManager.enabled=true \
|
||||
--set certManager.ca.create=false \
|
||||
--set certManager.ca.nameOverride=etcd-primary-kamaji-etcd-ca \
|
||||
--set certManager.issuerRef.kind=ClusterIssuer \
|
||||
--set certManager.issuerRef.name=self-signed
|
||||
```
|
||||
|
||||
## Create a Tenant Control Plane
|
||||
|
||||
Using the `spec.dataStoreOverrides` field, Datastores different from the one used in `spec.dataStore` can be used to store specific resources.
|
||||
|
||||
```bash
|
||||
echo 'apiVersion: kamaji.clastix.io/v1alpha1
|
||||
kind: TenantControlPlane
|
||||
metadata:
|
||||
name: k8s-133
|
||||
labels:
|
||||
tenant.clastix.io: k8s-133
|
||||
spec:
|
||||
controlPlane:
|
||||
deployment:
|
||||
replicas: 2
|
||||
service:
|
||||
serviceType: LoadBalancer
|
||||
kubernetes:
|
||||
version: "v1.33.1"
|
||||
kubelet:
|
||||
cgroupfs: systemd
|
||||
dataStore: etcd-primary-kamaji-etcd
|
||||
dataStoreOverrides:
|
||||
- resource: "/events" # Store events in the secondary ETCD
|
||||
dataStore: etcd-secondary-kamaji-etcd
|
||||
networkProfile:
|
||||
port: 6443
|
||||
addons:
|
||||
coreDNS: {}
|
||||
kubeProxy: {}
|
||||
konnectivity:
|
||||
server:
|
||||
port: 8132
|
||||
agent:
|
||||
mode: DaemonSet
|
||||
' | k apply -f -
|
||||
```
|
||||
|
||||
## Considerations
|
||||
|
||||
Only built-in resources can be tagetted by `--etcd-servers-overrides`, it is currently not possible to target Custom Resources.
|
||||
@@ -326,7 +326,7 @@ Enables the Konnectivity addon in the Tenant Cluster, required if the worker nod
|
||||
<td>
|
||||
<br/>
|
||||
<br/>
|
||||
<i>Default</i>: map[image:registry.k8s.io/kas-network-proxy/proxy-agent mode:DaemonSet version:v0.28.6]<br/>
|
||||
<i>Default</i>: map[image:registry.k8s.io/kas-network-proxy/proxy-agent mode:DaemonSet]<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
@@ -335,7 +335,7 @@ Enables the Konnectivity addon in the Tenant Cluster, required if the worker nod
|
||||
<td>
|
||||
<br/>
|
||||
<br/>
|
||||
<i>Default</i>: map[image:registry.k8s.io/kas-network-proxy/proxy-server port:8132 version:v0.28.6]<br/>
|
||||
<i>Default</i>: map[image:registry.k8s.io/kas-network-proxy/proxy-server port:8132]<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
@@ -421,9 +421,10 @@ Can be customized to start the konnectivity-agent even if the nodes are not read
|
||||
<td><b>version</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Version for Konnectivity agent.<br/>
|
||||
<br/>
|
||||
<i>Default</i>: v0.28.6<br/>
|
||||
Version for Konnectivity agent.
|
||||
If left empty, Kamaji will automatically inflect the version from the deployed Tenant Control Plane.
|
||||
|
||||
WARNING: for last cut-off releases, the container image could be not available.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
@@ -548,9 +549,10 @@ unxpected ways. Only modify if you know what you are doing.<br/>
|
||||
<td><b>version</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Container image version of the Konnectivity server.<br/>
|
||||
<br/>
|
||||
<i>Default</i>: v0.28.6<br/>
|
||||
Container image version of the Konnectivity server.
|
||||
If left empty, Kamaji will automatically inflect the version from the deployed Tenant Control Plane.
|
||||
|
||||
WARNING: for last cut-off releases, the container image could be not available.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
@@ -14054,7 +14056,7 @@ Enables the Konnectivity addon in the Tenant Cluster, required if the worker nod
|
||||
<td>
|
||||
<br/>
|
||||
<br/>
|
||||
<i>Default</i>: map[image:registry.k8s.io/kas-network-proxy/proxy-agent mode:DaemonSet version:v0.28.6]<br/>
|
||||
<i>Default</i>: map[image:registry.k8s.io/kas-network-proxy/proxy-agent mode:DaemonSet]<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
@@ -14063,7 +14065,7 @@ Enables the Konnectivity addon in the Tenant Cluster, required if the worker nod
|
||||
<td>
|
||||
<br/>
|
||||
<br/>
|
||||
<i>Default</i>: map[image:registry.k8s.io/kas-network-proxy/proxy-server port:8132 version:v0.28.6]<br/>
|
||||
<i>Default</i>: map[image:registry.k8s.io/kas-network-proxy/proxy-server port:8132]<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
@@ -14149,9 +14151,10 @@ Can be customized to start the konnectivity-agent even if the nodes are not read
|
||||
<td><b>version</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Version for Konnectivity agent.<br/>
|
||||
<br/>
|
||||
<i>Default</i>: v0.28.6<br/>
|
||||
Version for Konnectivity agent.
|
||||
If left empty, Kamaji will automatically inflect the version from the deployed Tenant Control Plane.
|
||||
|
||||
WARNING: for last cut-off releases, the container image could be not available.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
@@ -14276,9 +14279,10 @@ unxpected ways. Only modify if you know what you are doing.<br/>
|
||||
<td><b>version</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Container image version of the Konnectivity server.<br/>
|
||||
<br/>
|
||||
<i>Default</i>: v0.28.6<br/>
|
||||
Container image version of the Konnectivity server.
|
||||
If left empty, Kamaji will automatically inflect the version from the deployed Tenant Control Plane.
|
||||
|
||||
WARNING: for last cut-off releases, the container image could be not available.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
@@ -28496,6 +28500,13 @@ Migration from one DataStore to another backed by the same Driver is possible. S
|
||||
Migration from one DataStore to another backed by a different Driver is not supported.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantcontrolplanespecdatastoreoverridesindex">dataStoreOverrides</a></b></td>
|
||||
<td>[]object</td>
|
||||
<td>
|
||||
DataStoreOverride defines which kubernetes resources will be stored in dedicated datastores.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>dataStoreSchema</b></td>
|
||||
<td>string</td>
|
||||
@@ -28568,6 +28579,13 @@ such as the number of Pod replicas, the Service resource, or the Ingress.
|
||||
Defining the options for the deployed Tenant Control Plane as Deployment resource.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantcontrolplanespeccontrolplanegateway">gateway</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
Defining the options for an Optional Gateway which will expose API Server of the Tenant Control Plane<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantcontrolplanespeccontrolplaneingress">ingress</a></b></td>
|
||||
<td>object</td>
|
||||
@@ -41363,6 +41381,243 @@ merge patch.<br/>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanespeccontrolplanegateway">`TenantControlPlane.spec.controlPlane.gateway`</span>
|
||||
|
||||
|
||||
Defining the options for an Optional Gateway which will expose API Server of the Tenant Control Plane
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b><a href="#tenantcontrolplanespeccontrolplanegatewayadditionalmetadata">additionalMetadata</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
AdditionalMetadata to add Labels and Annotations support.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>hostname</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Hostname is an optional field which will be used as a route hostname.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantcontrolplanespeccontrolplanegatewayparentrefsindex">parentRefs</a></b></td>
|
||||
<td>[]object</td>
|
||||
<td>
|
||||
GatewayParentRefs is the class of the Gateway resource to use.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanespeccontrolplanegatewayadditionalmetadata">`TenantControlPlane.spec.controlPlane.gateway.additionalMetadata`</span>
|
||||
|
||||
|
||||
AdditionalMetadata to add Labels and Annotations support.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>annotations</b></td>
|
||||
<td>map[string]string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>labels</b></td>
|
||||
<td>map[string]string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanespeccontrolplanegatewayparentrefsindex">`TenantControlPlane.spec.controlPlane.gateway.parentRefs[index]`</span>
|
||||
|
||||
|
||||
ParentReference identifies an API object (usually a Gateway) that can be considered
|
||||
a parent of this resource (usually a route). There are two kinds of parent resources
|
||||
with "Core" support:
|
||||
|
||||
* Gateway (Gateway conformance profile)
|
||||
* Service (Mesh conformance profile, ClusterIP Services only)
|
||||
|
||||
This API may be extended in the future to support additional kinds of parent
|
||||
resources.
|
||||
|
||||
The API object must be valid in the cluster; the Group and Kind must
|
||||
be registered in the cluster for this reference to be valid.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>name</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Name is the name of the referent.
|
||||
|
||||
Support: Core<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>group</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Group is the group of the referent.
|
||||
When unspecified, "gateway.networking.k8s.io" is inferred.
|
||||
To set the core API group (such as for a "Service" kind referent),
|
||||
Group must be explicitly set to "" (empty string).
|
||||
|
||||
Support: Core<br/>
|
||||
<br/>
|
||||
<i>Default</i>: gateway.networking.k8s.io<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>kind</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Kind is kind of the referent.
|
||||
|
||||
There are two kinds of parent resources with "Core" support:
|
||||
|
||||
* Gateway (Gateway conformance profile)
|
||||
* Service (Mesh conformance profile, ClusterIP Services only)
|
||||
|
||||
Support for other resources is Implementation-Specific.<br/>
|
||||
<br/>
|
||||
<i>Default</i>: Gateway<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>namespace</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Namespace is the namespace of the referent. When unspecified, this refers
|
||||
to the local namespace of the Route.
|
||||
|
||||
Note that there are specific rules for ParentRefs which cross namespace
|
||||
boundaries. Cross-namespace references are only valid if they are explicitly
|
||||
allowed by something in the namespace they are referring to. For example:
|
||||
Gateway has the AllowedRoutes field, and ReferenceGrant provides a
|
||||
generic way to enable any other kind of cross-namespace reference.
|
||||
|
||||
<gateway:experimental:description>
|
||||
ParentRefs from a Route to a Service in the same namespace are "producer"
|
||||
routes, which apply default routing rules to inbound connections from
|
||||
any namespace to the Service.
|
||||
|
||||
ParentRefs from a Route to a Service in a different namespace are
|
||||
"consumer" routes, and these routing rules are only applied to outbound
|
||||
connections originating from the same namespace as the Route, for which
|
||||
the intended destination of the connections are a Service targeted as a
|
||||
ParentRef of the Route.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Support: Core<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>port</b></td>
|
||||
<td>integer</td>
|
||||
<td>
|
||||
Port is the network port this Route targets. It can be interpreted
|
||||
differently based on the type of parent resource.
|
||||
|
||||
When the parent resource is a Gateway, this targets all listeners
|
||||
listening on the specified port that also support this kind of Route(and
|
||||
select this Route). It's not recommended to set `Port` unless the
|
||||
networking behaviors specified in a Route must apply to a specific port
|
||||
as opposed to a listener(s) whose port(s) may be changed. When both Port
|
||||
and SectionName are specified, the name and port of the selected listener
|
||||
must match both specified values.
|
||||
|
||||
<gateway:experimental:description>
|
||||
When the parent resource is a Service, this targets a specific port in the
|
||||
Service spec. When both Port (experimental) and SectionName are specified,
|
||||
the name and port of the selected port must match both specified values.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Implementations MAY choose to support other parent resources.
|
||||
Implementations supporting other types of parent resources MUST clearly
|
||||
document how/if Port is interpreted.
|
||||
|
||||
For the purpose of status, an attachment is considered successful as
|
||||
long as the parent resource accepts it partially. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
|
||||
from the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route,
|
||||
the Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Extended<br/>
|
||||
<br/>
|
||||
<i>Format</i>: int32<br/>
|
||||
<i>Minimum</i>: 1<br/>
|
||||
<i>Maximum</i>: 65535<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>sectionName</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
SectionName is the name of a section within the target resource. In the
|
||||
following resources, SectionName is interpreted as the following:
|
||||
|
||||
* Gateway: Listener name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
* Service: Port name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
|
||||
Implementations MAY choose to support attaching Routes to other resources.
|
||||
If that is the case, they MUST clearly document how SectionName is
|
||||
interpreted.
|
||||
|
||||
When unspecified (empty string), this will reference the entire resource.
|
||||
For the purpose of status, an attachment is considered successful if at
|
||||
least one section in the parent resource accepts it. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
|
||||
the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route, the
|
||||
Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Core<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanespeccontrolplaneingress">`TenantControlPlane.spec.controlPlane.ingress`</span>
|
||||
|
||||
|
||||
@@ -41966,6 +42221,38 @@ In case this value is set, kubeadm does not change automatically the version of
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanespecdatastoreoverridesindex">`TenantControlPlane.spec.dataStoreOverrides[index]`</span>
|
||||
|
||||
|
||||
DataStoreOverride defines which kubernetes resource will be stored in a dedicated datastore.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>dataStore</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
DataStore specifies the DataStore that should be used to store the Kubernetes data for the given Resource.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>resource</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Resource specifies which kubernetes resource to target.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanespecnetworkprofile">`TenantControlPlane.spec.networkProfile`</span>
|
||||
|
||||
|
||||
@@ -43591,6 +43878,13 @@ Kubernetes contains information about the reconciliation of the required Kuberne
|
||||
KubernetesDeploymentStatus defines the status for the Tenant Control Plane Deployment in the management cluster.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantcontrolplanestatuskubernetesresourcesgateway">gateway</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
KubernetesGatewayStatus defines the status for the Tenant Control Plane Gateway in the management cluster.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantcontrolplanestatuskubernetesresourcesingress">ingress</a></b></td>
|
||||
<td>object</td>
|
||||
@@ -43814,6 +44108,505 @@ DeploymentCondition describes the state of a deployment at a certain point.
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanestatuskubernetesresourcesgateway">`TenantControlPlane.status.kubernetesResources.gateway`</span>
|
||||
|
||||
|
||||
KubernetesGatewayStatus defines the status for the Tenant Control Plane Gateway in the management cluster.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b><a href="#tenantcontrolplanestatuskubernetesresourcesgatewayparentsindex">parents</a></b></td>
|
||||
<td>[]object</td>
|
||||
<td>
|
||||
Parents is a list of parent resources (usually Gateways) that are
|
||||
associated with the route, and the status of the route with respect to
|
||||
each parent. When this route attaches to a parent, the controller that
|
||||
manages the parent must add an entry to this list when the controller
|
||||
first sees the route and should update the entry as appropriate when the
|
||||
route or gateway is modified.
|
||||
|
||||
Note that parent references that cannot be resolved by an implementation
|
||||
of this API will not be added to this list. Implementations of this API
|
||||
can only populate Route status for the Gateways/parent resources they are
|
||||
responsible for.
|
||||
|
||||
A maximum of 32 Gateways will be represented in this list. An empty list
|
||||
means the route has not been attached to any Gateway.
|
||||
|
||||
<gateway:util:excludeFromCRD>
|
||||
Notes for implementors:
|
||||
|
||||
While parents is not a listType `map`, this is due to the fact that the
|
||||
list key is not scalar, and Kubernetes is unable to represent this.
|
||||
|
||||
Parent status MUST be considered to be namespaced by the combination of
|
||||
the parentRef and controllerName fields, and implementations should keep
|
||||
the following rules in mind when updating this status:
|
||||
|
||||
* Implementations MUST update only entries that have a matching value of
|
||||
`controllerName` for that implementation.
|
||||
* Implementations MUST NOT update entries with non-matching `controllerName`
|
||||
fields.
|
||||
* Implementations MUST treat each `parentRef`` in the Route separately and
|
||||
update its status based on the relationship with that parent.
|
||||
* Implementations MUST perform a read-modify-write cycle on this field
|
||||
before modifying it. That is, when modifying this field, implementations
|
||||
must be confident they have fetched the most recent version of this field,
|
||||
and ensure that changes they make are on that recent version.
|
||||
|
||||
</gateway:util:excludeFromCRD><br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantcontrolplanestatuskubernetesresourcesgatewayaccesspointsindex">accessPoints</a></b></td>
|
||||
<td>[]object</td>
|
||||
<td>
|
||||
A list of valid access points that the route exposes.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantcontrolplanestatuskubernetesresourcesgatewayrouteref">routeRef</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
Reference to the route created for this tenant.<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanestatuskubernetesresourcesgatewayparentsindex">`TenantControlPlane.status.kubernetesResources.gateway.parents[index]`</span>
|
||||
|
||||
|
||||
RouteParentStatus describes the status of a route with respect to an
|
||||
associated Parent.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b><a href="#tenantcontrolplanestatuskubernetesresourcesgatewayparentsindexconditionsindex">conditions</a></b></td>
|
||||
<td>[]object</td>
|
||||
<td>
|
||||
Conditions describes the status of the route with respect to the Gateway.
|
||||
Note that the route's availability is also subject to the Gateway's own
|
||||
status conditions and listener status.
|
||||
|
||||
If the Route's ParentRef specifies an existing Gateway that supports
|
||||
Routes of this kind AND that Gateway's controller has sufficient access,
|
||||
then that Gateway's controller MUST set the "Accepted" condition on the
|
||||
Route, to indicate whether the route has been accepted or rejected by the
|
||||
Gateway, and why.
|
||||
|
||||
A Route MUST be considered "Accepted" if at least one of the Route's
|
||||
rules is implemented by the Gateway.
|
||||
|
||||
There are a number of cases where the "Accepted" condition may not be set
|
||||
due to lack of controller visibility, that includes when:
|
||||
|
||||
* The Route refers to a nonexistent parent.
|
||||
* The Route is of a type that the controller does not support.
|
||||
* The Route is in a namespace the controller does not have access to.
|
||||
|
||||
<gateway:util:excludeFromCRD>
|
||||
|
||||
Notes for implementors:
|
||||
|
||||
Conditions are a listType `map`, which means that they function like a
|
||||
map with a key of the `type` field _in the k8s apiserver_.
|
||||
|
||||
This means that implementations must obey some rules when updating this
|
||||
section.
|
||||
|
||||
* Implementations MUST perform a read-modify-write cycle on this field
|
||||
before modifying it. That is, when modifying this field, implementations
|
||||
must be confident they have fetched the most recent version of this field,
|
||||
and ensure that changes they make are on that recent version.
|
||||
* Implementations MUST NOT remove or reorder Conditions that they are not
|
||||
directly responsible for. For example, if an implementation sees a Condition
|
||||
with type `special.io/SomeField`, it MUST NOT remove, change or update that
|
||||
Condition.
|
||||
* Implementations MUST always _merge_ changes into Conditions of the same Type,
|
||||
rather than creating more than one Condition of the same Type.
|
||||
* Implementations MUST always update the `observedGeneration` field of the
|
||||
Condition to the `metadata.generation` of the Gateway at the time of update creation.
|
||||
* If the `observedGeneration` of a Condition is _greater than_ the value the
|
||||
implementation knows about, then it MUST NOT perform the update on that Condition,
|
||||
but must wait for a future reconciliation and status update. (The assumption is that
|
||||
the implementation's copy of the object is stale and an update will be re-triggered
|
||||
if relevant.)
|
||||
|
||||
</gateway:util:excludeFromCRD><br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>controllerName</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
ControllerName is a domain/path string that indicates the name of the
|
||||
controller that wrote this status. This corresponds with the
|
||||
controllerName field on GatewayClass.
|
||||
|
||||
Example: "example.net/gateway-controller".
|
||||
|
||||
The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are
|
||||
valid Kubernetes names
|
||||
(https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
|
||||
|
||||
Controllers MUST populate this field when writing status. Controllers should ensure that
|
||||
entries to status populated with their ControllerName are cleaned up when they are no
|
||||
longer necessary.<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b><a href="#tenantcontrolplanestatuskubernetesresourcesgatewayparentsindexparentref">parentRef</a></b></td>
|
||||
<td>object</td>
|
||||
<td>
|
||||
ParentRef corresponds with a ParentRef in the spec that this
|
||||
RouteParentStatus struct describes the status of.<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanestatuskubernetesresourcesgatewayparentsindexconditionsindex">`TenantControlPlane.status.kubernetesResources.gateway.parents[index].conditions[index]`</span>
|
||||
|
||||
|
||||
Condition contains details for one aspect of the current state of this API Resource.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>lastTransitionTime</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.<br/>
|
||||
<br/>
|
||||
<i>Format</i>: date-time<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>message</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>reason</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>status</b></td>
|
||||
<td>enum</td>
|
||||
<td>
|
||||
status of the condition, one of True, False, Unknown.<br/>
|
||||
<br/>
|
||||
<i>Enum</i>: True, False, Unknown<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>type</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
type of condition in CamelCase or in foo.example.com/CamelCase.<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>observedGeneration</b></td>
|
||||
<td>integer</td>
|
||||
<td>
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.<br/>
|
||||
<br/>
|
||||
<i>Format</i>: int64<br/>
|
||||
<i>Minimum</i>: 0<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanestatuskubernetesresourcesgatewayparentsindexparentref">`TenantControlPlane.status.kubernetesResources.gateway.parents[index].parentRef`</span>
|
||||
|
||||
|
||||
ParentRef corresponds with a ParentRef in the spec that this
|
||||
RouteParentStatus struct describes the status of.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>name</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Name is the name of the referent.
|
||||
|
||||
Support: Core<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>group</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Group is the group of the referent.
|
||||
When unspecified, "gateway.networking.k8s.io" is inferred.
|
||||
To set the core API group (such as for a "Service" kind referent),
|
||||
Group must be explicitly set to "" (empty string).
|
||||
|
||||
Support: Core<br/>
|
||||
<br/>
|
||||
<i>Default</i>: gateway.networking.k8s.io<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>kind</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Kind is kind of the referent.
|
||||
|
||||
There are two kinds of parent resources with "Core" support:
|
||||
|
||||
* Gateway (Gateway conformance profile)
|
||||
* Service (Mesh conformance profile, ClusterIP Services only)
|
||||
|
||||
Support for other resources is Implementation-Specific.<br/>
|
||||
<br/>
|
||||
<i>Default</i>: Gateway<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>namespace</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Namespace is the namespace of the referent. When unspecified, this refers
|
||||
to the local namespace of the Route.
|
||||
|
||||
Note that there are specific rules for ParentRefs which cross namespace
|
||||
boundaries. Cross-namespace references are only valid if they are explicitly
|
||||
allowed by something in the namespace they are referring to. For example:
|
||||
Gateway has the AllowedRoutes field, and ReferenceGrant provides a
|
||||
generic way to enable any other kind of cross-namespace reference.
|
||||
|
||||
<gateway:experimental:description>
|
||||
ParentRefs from a Route to a Service in the same namespace are "producer"
|
||||
routes, which apply default routing rules to inbound connections from
|
||||
any namespace to the Service.
|
||||
|
||||
ParentRefs from a Route to a Service in a different namespace are
|
||||
"consumer" routes, and these routing rules are only applied to outbound
|
||||
connections originating from the same namespace as the Route, for which
|
||||
the intended destination of the connections are a Service targeted as a
|
||||
ParentRef of the Route.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Support: Core<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>port</b></td>
|
||||
<td>integer</td>
|
||||
<td>
|
||||
Port is the network port this Route targets. It can be interpreted
|
||||
differently based on the type of parent resource.
|
||||
|
||||
When the parent resource is a Gateway, this targets all listeners
|
||||
listening on the specified port that also support this kind of Route(and
|
||||
select this Route). It's not recommended to set `Port` unless the
|
||||
networking behaviors specified in a Route must apply to a specific port
|
||||
as opposed to a listener(s) whose port(s) may be changed. When both Port
|
||||
and SectionName are specified, the name and port of the selected listener
|
||||
must match both specified values.
|
||||
|
||||
<gateway:experimental:description>
|
||||
When the parent resource is a Service, this targets a specific port in the
|
||||
Service spec. When both Port (experimental) and SectionName are specified,
|
||||
the name and port of the selected port must match both specified values.
|
||||
</gateway:experimental:description>
|
||||
|
||||
Implementations MAY choose to support other parent resources.
|
||||
Implementations supporting other types of parent resources MUST clearly
|
||||
document how/if Port is interpreted.
|
||||
|
||||
For the purpose of status, an attachment is considered successful as
|
||||
long as the parent resource accepts it partially. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
|
||||
from the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route,
|
||||
the Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Extended<br/>
|
||||
<br/>
|
||||
<i>Format</i>: int32<br/>
|
||||
<i>Minimum</i>: 1<br/>
|
||||
<i>Maximum</i>: 65535<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr><tr>
|
||||
<td><b>sectionName</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
SectionName is the name of a section within the target resource. In the
|
||||
following resources, SectionName is interpreted as the following:
|
||||
|
||||
* Gateway: Listener name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
* Service: Port name. When both Port (experimental) and SectionName
|
||||
are specified, the name and port of the selected listener must match
|
||||
both specified values.
|
||||
|
||||
Implementations MAY choose to support attaching Routes to other resources.
|
||||
If that is the case, they MUST clearly document how SectionName is
|
||||
interpreted.
|
||||
|
||||
When unspecified (empty string), this will reference the entire resource.
|
||||
For the purpose of status, an attachment is considered successful if at
|
||||
least one section in the parent resource accepts it. For example, Gateway
|
||||
listeners can restrict which Routes can attach to them by Route kind,
|
||||
namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
|
||||
the referencing Route, the Route MUST be considered successfully
|
||||
attached. If no Gateway listeners accept attachment from this Route, the
|
||||
Route MUST be considered detached from the Gateway.
|
||||
|
||||
Support: Core<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanestatuskubernetesresourcesgatewayaccesspointsindex">`TenantControlPlane.status.kubernetesResources.gateway.accessPoints[index]`</span>
|
||||
|
||||
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>port</b></td>
|
||||
<td>integer</td>
|
||||
<td>
|
||||
<br/>
|
||||
<br/>
|
||||
<i>Format</i>: int32<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>type</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
AddressType defines how a network address is represented as a text string.
|
||||
This may take two possible forms:
|
||||
|
||||
* A predefined CamelCase string identifier (currently limited to `IPAddress` or `Hostname`)
|
||||
* A domain-prefixed string identifier (like `acme.io/CustomAddressType`)
|
||||
|
||||
Values `IPAddress` and `Hostname` have Extended support.
|
||||
|
||||
The `NamedAddress` value has been deprecated in favor of implementation
|
||||
specific domain-prefixed strings.
|
||||
|
||||
All other values, including domain-prefixed values have Implementation-specific support,
|
||||
which are used in implementation-specific behaviors. Support for additional
|
||||
predefined CamelCase identifiers may be added in future releases.<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>value</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>true</td>
|
||||
</tr><tr>
|
||||
<td><b>urls</b></td>
|
||||
<td>[]string</td>
|
||||
<td>
|
||||
<br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanestatuskubernetesresourcesgatewayrouteref">`TenantControlPlane.status.kubernetesResources.gateway.routeRef`</span>
|
||||
|
||||
|
||||
Reference to the route created for this tenant.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><b>name</b></td>
|
||||
<td>string</td>
|
||||
<td>
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names<br/>
|
||||
<br/>
|
||||
<i>Default</i>: <br/>
|
||||
</td>
|
||||
<td>false</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<span id="tenantcontrolplanestatuskubernetesresourcesingress">`TenantControlPlane.status.kubernetesResources.ingress`</span>
|
||||
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ nav:
|
||||
- guides/pausing.md
|
||||
- guides/write-permissions.md
|
||||
- guides/datastore-migration.md
|
||||
- guides/datastore-overrides.md
|
||||
- guides/gitops.md
|
||||
- guides/console.md
|
||||
- guides/kubeconfig-generator.md
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
)
|
||||
@@ -56,6 +58,12 @@ var _ = BeforeSuite(func() {
|
||||
err = kamajiv1alpha1.AddToScheme(scheme.Scheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = gatewayv1.Install(scheme.Scheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = gatewayv1alpha2.Install(scheme.Scheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
//+kubebuilder:scaffold:scheme
|
||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
68
e2e/tcp_datastore_overrides_test.go
Normal file
68
e2e/tcp_datastore_overrides_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
pointer "k8s.io/utils/ptr"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
)
|
||||
|
||||
var _ = Describe("Deploy a TenantControlPlane resource with DataStoreOverrides", func() {
|
||||
// Fill TenantControlPlane object
|
||||
tcp := &kamajiv1alpha1.TenantControlPlane{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tcp-clusterip",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: kamajiv1alpha1.TenantControlPlaneSpec{
|
||||
DataStore: "etcd-primary",
|
||||
DataStoreOverrides: []kamajiv1alpha1.DataStoreOverride{{
|
||||
Resource: "/events",
|
||||
DataStore: "etcd-secondary",
|
||||
}},
|
||||
ControlPlane: kamajiv1alpha1.ControlPlane{
|
||||
Deployment: kamajiv1alpha1.DeploymentSpec{
|
||||
Replicas: pointer.To(int32(1)),
|
||||
},
|
||||
Service: kamajiv1alpha1.ServiceSpec{
|
||||
ServiceType: "ClusterIP",
|
||||
},
|
||||
},
|
||||
NetworkProfile: kamajiv1alpha1.NetworkProfileSpec{
|
||||
Address: "172.18.0.2",
|
||||
},
|
||||
Kubernetes: kamajiv1alpha1.KubernetesSpec{
|
||||
Version: "v1.23.6",
|
||||
Kubelet: kamajiv1alpha1.KubeletSpec{
|
||||
CGroupFS: "cgroupfs",
|
||||
},
|
||||
AdmissionControllers: kamajiv1alpha1.AdmissionControllers{
|
||||
"LimitRanger",
|
||||
"ResourceQuota",
|
||||
},
|
||||
},
|
||||
Addons: kamajiv1alpha1.AddonsSpec{},
|
||||
},
|
||||
}
|
||||
|
||||
// Create a TenantControlPlane resource into the cluster
|
||||
JustBeforeEach(func() {
|
||||
Expect(k8sClient.Create(context.Background(), tcp)).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
// Delete the TenantControlPlane resource after test is finished
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.Background(), tcp)).Should(Succeed())
|
||||
})
|
||||
// Check if TenantControlPlane resource has been created
|
||||
It("Should be Ready", func() {
|
||||
StatusMustEqualTo(tcp, kamajiv1alpha1.VersionReady)
|
||||
})
|
||||
})
|
||||
102
e2e/tcp_gateway_ready_test.go
Normal file
102
e2e/tcp_gateway_ready_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
pointer "k8s.io/utils/ptr"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
)
|
||||
|
||||
var _ = Describe("Deploy a TenantControlPlane with Gateway API", func() {
|
||||
var tcp *kamajiv1alpha1.TenantControlPlane
|
||||
|
||||
JustBeforeEach(func() {
|
||||
tcp = &kamajiv1alpha1.TenantControlPlane{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tcp-gateway",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: kamajiv1alpha1.TenantControlPlaneSpec{
|
||||
ControlPlane: kamajiv1alpha1.ControlPlane{
|
||||
Deployment: kamajiv1alpha1.DeploymentSpec{
|
||||
Replicas: pointer.To(int32(1)),
|
||||
},
|
||||
Service: kamajiv1alpha1.ServiceSpec{
|
||||
ServiceType: "ClusterIP",
|
||||
},
|
||||
Gateway: &kamajiv1alpha1.GatewaySpec{
|
||||
Hostname: gatewayv1.Hostname("tcp-gateway.example.com"),
|
||||
AdditionalMetadata: kamajiv1alpha1.AdditionalMetadata{
|
||||
Labels: map[string]string{
|
||||
"test.kamaji.io/gateway": "true",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"test.kamaji.io/created-by": "e2e-test",
|
||||
},
|
||||
},
|
||||
GatewayParentRefs: []gatewayv1.ParentReference{
|
||||
{
|
||||
Name: "test-gateway",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
NetworkProfile: kamajiv1alpha1.NetworkProfileSpec{
|
||||
Address: "172.18.0.3",
|
||||
},
|
||||
Kubernetes: kamajiv1alpha1.KubernetesSpec{
|
||||
Version: "v1.23.6",
|
||||
Kubelet: kamajiv1alpha1.KubeletSpec{
|
||||
CGroupFS: "cgroupfs",
|
||||
},
|
||||
AdmissionControllers: kamajiv1alpha1.AdmissionControllers{
|
||||
"LimitRanger",
|
||||
"ResourceQuota",
|
||||
},
|
||||
},
|
||||
Addons: kamajiv1alpha1.AddonsSpec{},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(context.Background(), tcp)).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.Background(), tcp)).Should(Succeed())
|
||||
|
||||
// Wait for the object to be completely deleted
|
||||
Eventually(func() bool {
|
||||
err := k8sClient.Get(context.Background(), types.NamespacedName{
|
||||
Name: tcp.Name,
|
||||
Namespace: tcp.Namespace,
|
||||
}, &kamajiv1alpha1.TenantControlPlane{})
|
||||
|
||||
return err != nil // Returns true when object is not found (deleted)
|
||||
}).WithTimeout(time.Minute).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("Should be Ready", func() {
|
||||
StatusMustEqualTo(tcp, kamajiv1alpha1.VersionReady)
|
||||
})
|
||||
|
||||
It("Should create TLSRoute resource", func() {
|
||||
Eventually(func() error {
|
||||
route := &gatewayv1alpha2.TLSRoute{}
|
||||
// TODO: Check ownership.
|
||||
return k8sClient.Get(context.Background(), types.NamespacedName{
|
||||
Name: tcp.Name,
|
||||
Namespace: tcp.Namespace,
|
||||
}, route)
|
||||
}).WithTimeout(time.Minute).Should(Succeed())
|
||||
})
|
||||
})
|
||||
@@ -23,14 +23,14 @@ var _ = Describe("using an unsupported TenantControlPlane Kubernetes version", f
|
||||
v, err := semver.Make(upgrade.KubeadmVersion[1:])
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
unsupported, err := semver.Make(fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch+1))
|
||||
unsupported, err := semver.Make(fmt.Sprintf("%d.%d.%d", v.Major, v.Minor+1, 0))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
It("should be blocked on creation", func() {
|
||||
Consistently(func() error {
|
||||
tcp := kamajiv1alpha1.TenantControlPlane{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "non-linear-update",
|
||||
Name: "unsupported-version",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: kamajiv1alpha1.TenantControlPlaneSpec{
|
||||
|
||||
47
go.mod
47
go.mod
@@ -15,11 +15,11 @@ require (
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/juju/mutex/v2 v2.0.0
|
||||
github.com/nats-io/nats.go v1.47.0
|
||||
github.com/onsi/ginkgo/v2 v2.27.2
|
||||
github.com/onsi/gomega v1.38.2
|
||||
github.com/onsi/ginkgo/v2 v2.27.3
|
||||
github.com/onsi/gomega v1.38.3
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/testcontainers/testcontainers-go v0.40.0
|
||||
@@ -34,9 +34,10 @@ 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.34.2
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
|
||||
k8s.io/kubernetes v1.34.3
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
|
||||
sigs.k8s.io/controller-runtime v0.22.4
|
||||
sigs.k8s.io/gateway-api v1.4.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -67,8 +68,8 @@ require (
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
@@ -77,9 +78,9 @@ require (
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-logr/zapr v1.3.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.2 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/go-pg/zerochecker v0.2.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
@@ -103,7 +104,7 @@ require (
|
||||
github.com/lithammer/dedent v1.1.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.1.0 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
@@ -126,7 +127,7 @@ require (
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
@@ -150,12 +151,12 @@ require (
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
@@ -170,13 +171,13 @@ require (
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/term v0.36.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/grpc v1.72.1 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
|
||||
google.golang.org/grpc v1.75.1 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
@@ -190,12 +191,12 @@ require (
|
||||
k8s.io/cri-api v0.34.0 // indirect
|
||||
k8s.io/cri-client v0.0.0 // indirect
|
||||
k8s.io/kms v0.34.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 // indirect
|
||||
k8s.io/kube-proxy v0.0.0 // indirect
|
||||
k8s.io/system-validators v1.10.2 // indirect
|
||||
mellium.im/sasl v0.3.1 // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.20.1 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
|
||||
104
go.sum
104
go.sum
@@ -49,7 +49,6 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -68,10 +67,10 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI=
|
||||
github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
@@ -100,14 +99,12 @@ github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA=
|
||||
github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||
github.com/go-pg/pg/v10 v10.15.0 h1:6DQwbaxJz/e4wvgzbxBkBLiL/Uuk87MGgHhkURtzx24=
|
||||
github.com/go-pg/pg/v10 v10.15.0/go.mod h1:FIn/x04hahOf9ywQ1p68rXqaDVbTRLYlu4MQR0lhoB8=
|
||||
github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
|
||||
@@ -196,7 +193,6 @@ github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCy
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@@ -213,8 +209,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
|
||||
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
|
||||
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
|
||||
@@ -257,10 +253,10 @@ github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
|
||||
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8=
|
||||
github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
|
||||
github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
@@ -284,8 +280,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
@@ -305,8 +301,8 @@ github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
@@ -389,22 +385,22 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
@@ -468,8 +464,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
@@ -483,20 +479,22 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
|
||||
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs=
|
||||
gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
@@ -543,26 +541,28 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kms v0.34.0 h1:u+/rcxQ3Jr7gC9AY5nXuEnBcGEB7ZOIJ9cdLdyHyEjQ=
|
||||
k8s.io/kms v0.34.0/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 h1:liMHz39T5dJO1aOKHLvwaCjDbf07wVh6yaUlTpunnkE=
|
||||
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/kube-proxy v0.34.0 h1:gU7MVbJHiXyPX8bXnod4bANtSC7rZSKkkLmM8gUqwT4=
|
||||
k8s.io/kube-proxy v0.34.0/go.mod h1:tfwI8dCKm5Q0r+aVIbrq/aC36Kk936w2LZu8/rvJzWI=
|
||||
k8s.io/kubelet v0.34.0 h1:1nZt1Q6Kfx7xCaTS9vnqR9sjZDxf3cRSQkAFCczULmc=
|
||||
k8s.io/kubelet v0.34.0/go.mod h1:NqbF8ViVettlZbf9hw9DJhubaWn7rGvDDTcLMDm6tQ0=
|
||||
k8s.io/kubernetes v1.34.2 h1:WQdDvYJazkmkwSncgNwGvVtaCt4TYXIU3wSMRgvp3MI=
|
||||
k8s.io/kubernetes v1.34.2/go.mod h1:m6pZk6a179pRo2wsTiCPORJ86iOEQmfIzUvtyEF8BwA=
|
||||
k8s.io/kubernetes v1.34.3 h1:0TfljWbhEF5DBks+WFMSrvKfxBLo4vnZuqORjLMiyT4=
|
||||
k8s.io/kubernetes v1.34.3/go.mod h1:m6pZk6a179pRo2wsTiCPORJ86iOEQmfIzUvtyEF8BwA=
|
||||
k8s.io/system-validators v1.10.2 h1:7rC7VdrQCaM55E08Pw3I1v1Op9ObLxdKAu5Ff5hIPwY=
|
||||
k8s.io/system-validators v1.10.2/go.mod h1:awfSS706v9R12VC7u7K89FKfqVy44G+E0L1A0FX9Wmw=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0=
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo=
|
||||
mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
|
||||
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/gateway-api v1.4.1 h1:NPxFutNkKNa8UfLd2CMlEuhIPMQgDQ6DXNKG9sHbJU8=
|
||||
sigs.k8s.io/gateway-api v1.4.1/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=
|
||||
sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM=
|
||||
sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78=
|
||||
|
||||
@@ -52,9 +52,15 @@ const (
|
||||
kineInitContainerName = "chmod"
|
||||
)
|
||||
|
||||
type DataStoreOverrides struct {
|
||||
Resource string
|
||||
DataStore kamajiv1alpha1.DataStore
|
||||
}
|
||||
|
||||
type Deployment struct {
|
||||
KineContainerImage string
|
||||
DataStore kamajiv1alpha1.DataStore
|
||||
DataStoreOverrides []DataStoreOverrides
|
||||
Client client.Client
|
||||
}
|
||||
|
||||
@@ -711,11 +717,29 @@ func (d Deployment) buildKubeAPIServerCommand(tenantControlPlane kamajiv1alpha1.
|
||||
desiredArgs["--etcd-keyfile"] = "/etc/kubernetes/pki/etcd/server.key"
|
||||
}
|
||||
|
||||
if len(d.DataStoreOverrides) != 0 {
|
||||
desiredArgs["--etcd-servers-overrides"] = d.etcdServersOverrides()
|
||||
}
|
||||
|
||||
// Order matters, here: extraArgs could try to overwrite some arguments managed by Kamaji and that would be crucial.
|
||||
// Adding as first element of the array of maps, we're sure that these overrides will be sanitized by our configuration.
|
||||
return utilities.MergeMaps(current, desiredArgs, extraArgs)
|
||||
}
|
||||
|
||||
func (d Deployment) etcdServersOverrides() string {
|
||||
dataStoreOverridesEndpoints := make([]string, 0, len(d.DataStoreOverrides))
|
||||
for _, dso := range d.DataStoreOverrides {
|
||||
httpsEndpoints := make([]string, 0, len(dso.DataStore.Spec.Endpoints))
|
||||
|
||||
for _, ep := range dso.DataStore.Spec.Endpoints {
|
||||
httpsEndpoints = append(httpsEndpoints, fmt.Sprintf("https://%s", ep))
|
||||
}
|
||||
dataStoreOverridesEndpoints = append(dataStoreOverridesEndpoints, fmt.Sprintf("%s#%s", dso.Resource, strings.Join(httpsEndpoints, ";")))
|
||||
}
|
||||
|
||||
return strings.Join(dataStoreOverridesEndpoints, ",")
|
||||
}
|
||||
|
||||
func (d Deployment) secretProjection(secretName, certKeyName, keyName string) *corev1.SecretProjection {
|
||||
return &corev1.SecretProjection{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
|
||||
46
internal/builders/controlplane/deployment_test.go
Normal file
46
internal/builders/controlplane/deployment_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package controlplane
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
)
|
||||
|
||||
var _ = Describe("Controlplane Deployment", func() {
|
||||
var d Deployment
|
||||
BeforeEach(func() {
|
||||
d = Deployment{
|
||||
DataStoreOverrides: []DataStoreOverrides{{
|
||||
Resource: "/events",
|
||||
DataStore: kamajiv1alpha1.DataStore{
|
||||
Spec: kamajiv1alpha1.DataStoreSpec{
|
||||
Endpoints: kamajiv1alpha1.Endpoints{"etcd-0", "etcd-1", "etcd-2"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
})
|
||||
|
||||
Describe("DataStoreOverrides flag generation", func() {
|
||||
It("should generate valid --etcd-servers-overrides value", func() {
|
||||
etcdSerVersOverrides := d.etcdServersOverrides()
|
||||
Expect(etcdSerVersOverrides).To(Equal("/events#https://etcd-0;https://etcd-1;https://etcd-2"))
|
||||
})
|
||||
It("should generate valid --etcd-servers-overrides value with 2 DataStoreOverrides", func() {
|
||||
d.DataStoreOverrides = append(d.DataStoreOverrides, DataStoreOverrides{
|
||||
Resource: "/pods",
|
||||
DataStore: kamajiv1alpha1.DataStore{
|
||||
Spec: kamajiv1alpha1.DataStoreSpec{
|
||||
Endpoints: kamajiv1alpha1.Endpoints{"etcd-3", "etcd-4", "etcd-5"},
|
||||
},
|
||||
},
|
||||
})
|
||||
etcdSerVersOverrides := d.etcdServersOverrides()
|
||||
Expect(etcdSerVersOverrides).To(Equal("/events#https://etcd-0;https://etcd-1;https://etcd-2,/pods#https://etcd-3;https://etcd-4;https://etcd-5"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -29,7 +29,7 @@ const (
|
||||
mysqlShowGrantsStatement = "SHOW GRANTS FOR `%s`@`%%`"
|
||||
mysqlCreateDBStatement = "CREATE DATABASE IF NOT EXISTS %s"
|
||||
mysqlCreateUserStatement = "CREATE USER `%s`@`%%` IDENTIFIED BY '%s'"
|
||||
mysqlGrantPrivilegesStatement = "GRANT ALL PRIVILEGES ON `%s`.* TO `%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`"
|
||||
mysqlRevokePrivilegesStatement = "REVOKE ALL PRIVILEGES ON `%s`.* FROM `%s`"
|
||||
@@ -167,7 +167,7 @@ func (c *MySQLConnection) CreateDB(ctx context.Context, dbName string) error {
|
||||
}
|
||||
|
||||
func (c *MySQLConnection) GrantPrivileges(ctx context.Context, user, dbName string) error {
|
||||
if err := c.mutate(ctx, mysqlGrantPrivilegesStatement, user, dbName); err != nil {
|
||||
if err := c.mutate(ctx, mysqlGrantPrivilegesStatement, dbName, user); err != nil {
|
||||
return errors.NewGrantPrivilegesError(err)
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ func (c *MySQLConnection) GrantPrivilegesExists(_ context.Context, user, dbName
|
||||
return false, errors.NewGrantPrivilegesError(err)
|
||||
}
|
||||
|
||||
expected := fmt.Sprintf(mysqlGrantPrivilegesStatement, user, dbName)
|
||||
expected := fmt.Sprintf(mysqlGrantPrivilegesStatement, dbName, user)
|
||||
var grant string
|
||||
|
||||
for rows.Next() {
|
||||
@@ -262,7 +262,7 @@ func (c *MySQLConnection) DeleteDB(ctx context.Context, dbName string) error {
|
||||
}
|
||||
|
||||
func (c *MySQLConnection) RevokePrivileges(ctx context.Context, user, dbName string) error {
|
||||
if err := c.mutate(ctx, mysqlRevokePrivilegesStatement, user, dbName); err != nil {
|
||||
if err := c.mutate(ctx, mysqlRevokePrivilegesStatement, dbName, user); err != nil {
|
||||
return errors.NewRevokePrivilegesError(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const (
|
||||
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 = ?"
|
||||
postgresqlKineTableExistsStatement = "SELECT 't' FROM pg_tables WHERE schemaname = ? AND tablename = ?"
|
||||
postgresqlGrantPrivilegesStatement = "GRANT ALL PRIVILEGES ON DATABASE %s TO %s"
|
||||
postgresqlGrantPrivilegesStatement = "GRANT CONNECT, CREATE ON DATABASE %s TO %s"
|
||||
postgresqlChangeOwnerStatement = "ALTER DATABASE %s OWNER TO %s"
|
||||
postgresqlRevokePrivilegesStatement = "REVOKE ALL PRIVILEGES ON DATABASE %s FROM %s"
|
||||
postgresqlDropRoleStatement = "DROP ROLE %s"
|
||||
|
||||
@@ -30,6 +30,7 @@ type Config struct {
|
||||
Client client.Client
|
||||
ConnString string
|
||||
DataStore kamajiv1alpha1.DataStore
|
||||
IsOverride bool
|
||||
}
|
||||
|
||||
func (r *Config) GetHistogram() prometheus.Histogram {
|
||||
@@ -103,10 +104,12 @@ func (r *Config) GetName() string {
|
||||
}
|
||||
|
||||
func (r *Config) UpdateTenantControlPlaneStatus(_ context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) error {
|
||||
tenantControlPlane.Status.Storage.Driver = string(r.DataStore.Spec.Driver)
|
||||
tenantControlPlane.Status.Storage.DataStoreName = r.DataStore.GetName()
|
||||
tenantControlPlane.Status.Storage.Config.SecretName = r.resource.GetName()
|
||||
tenantControlPlane.Status.Storage.Config.Checksum = utilities.GetObjectChecksum(r.resource)
|
||||
if !r.IsOverride {
|
||||
tenantControlPlane.Status.Storage.Driver = string(r.DataStore.Spec.Driver)
|
||||
tenantControlPlane.Status.Storage.DataStoreName = r.DataStore.GetName()
|
||||
tenantControlPlane.Status.Storage.Config.SecretName = r.resource.GetName()
|
||||
tenantControlPlane.Status.Storage.Config.Checksum = utilities.GetObjectChecksum(r.resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ type KubernetesDeploymentResource struct {
|
||||
resource *appsv1.Deployment
|
||||
Client client.Client
|
||||
DataStore kamajiv1alpha1.DataStore
|
||||
DataStoreOverrides []builder.DataStoreOverrides
|
||||
KineContainerImage string
|
||||
}
|
||||
|
||||
@@ -66,6 +67,7 @@ func (r *KubernetesDeploymentResource) mutate(ctx context.Context, tenantControl
|
||||
Client: r.Client,
|
||||
DataStore: r.DataStore,
|
||||
KineContainerImage: r.KineContainerImage,
|
||||
DataStoreOverrides: r.DataStoreOverrides,
|
||||
}).Build(ctx, r.resource, *tenantControlPlane)
|
||||
|
||||
return controllerutil.SetControllerReference(tenantControlPlane, r.resource, r.Client.Scheme())
|
||||
|
||||
384
internal/resources/k8s_gateway_resource.go
Normal file
384
internal/resources/k8s_gateway_resource.go
Normal file
@@ -0,0 +1,384 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
"github.com/clastix/kamaji/internal/utilities"
|
||||
)
|
||||
|
||||
type KubernetesGatewayResource struct {
|
||||
resource *gatewayv1alpha2.TLSRoute
|
||||
Client client.Client
|
||||
}
|
||||
|
||||
func (r *KubernetesGatewayResource) GetHistogram() prometheus.Histogram {
|
||||
gatewayCollector = LazyLoadHistogramFromResource(gatewayCollector, r)
|
||||
|
||||
return gatewayCollector
|
||||
}
|
||||
|
||||
func (r *KubernetesGatewayResource) ShouldStatusBeUpdated(_ context.Context, tcp *kamajiv1alpha1.TenantControlPlane) bool {
|
||||
switch {
|
||||
case tcp.Spec.ControlPlane.Gateway == nil && tcp.Status.Kubernetes.Gateway == nil:
|
||||
return false
|
||||
case tcp.Spec.ControlPlane.Gateway != nil && tcp.Status.Kubernetes.Gateway == nil:
|
||||
return true
|
||||
case tcp.Spec.ControlPlane.Gateway == nil && tcp.Status.Kubernetes.Gateway != nil:
|
||||
return true
|
||||
// Could be an alias for default here since the other cases are covered.
|
||||
case tcp.Spec.ControlPlane.Gateway != nil && tcp.Status.Kubernetes.Gateway != nil:
|
||||
return r.gatewayStatusNeedsUpdate(tcp)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// gatewayStatusNeedsUpdate compares the current gateway resource status with the stored status.
|
||||
func (r *KubernetesGatewayResource) gatewayStatusNeedsUpdate(tcp *kamajiv1alpha1.TenantControlPlane) bool {
|
||||
currentStatus := tcp.Status.Kubernetes.Gateway
|
||||
|
||||
// Check if route reference has changed
|
||||
if currentStatus.RouteRef.Name != r.resource.Name {
|
||||
return true
|
||||
}
|
||||
|
||||
// Compare RouteStatus - check if number of parents changed
|
||||
if len(currentStatus.RouteStatus.Parents) != len(r.resource.Status.RouteStatus.Parents) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Compare individual parent statuses
|
||||
// NOTE: Multiple Parent References are assumed.
|
||||
for i, currentParent := range currentStatus.RouteStatus.Parents {
|
||||
if i >= len(r.resource.Status.RouteStatus.Parents) {
|
||||
return true
|
||||
}
|
||||
|
||||
resourceParent := r.resource.Status.RouteStatus.Parents[i]
|
||||
|
||||
// Compare parent references
|
||||
if currentParent.ParentRef.Name != resourceParent.ParentRef.Name ||
|
||||
(currentParent.ParentRef.Namespace == nil) != (resourceParent.ParentRef.Namespace == nil) ||
|
||||
(currentParent.ParentRef.Namespace != nil && resourceParent.ParentRef.Namespace != nil &&
|
||||
*currentParent.ParentRef.Namespace != *resourceParent.ParentRef.Namespace) ||
|
||||
(currentParent.ParentRef.SectionName == nil) != (resourceParent.ParentRef.SectionName == nil) ||
|
||||
(currentParent.ParentRef.SectionName != nil && resourceParent.ParentRef.SectionName != nil &&
|
||||
*currentParent.ParentRef.SectionName != *resourceParent.ParentRef.SectionName) {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(currentParent.Conditions) != len(resourceParent.Conditions) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Compare each condition
|
||||
for j, currentCondition := range currentParent.Conditions {
|
||||
if j >= len(resourceParent.Conditions) {
|
||||
return true
|
||||
}
|
||||
|
||||
resourceCondition := resourceParent.Conditions[j]
|
||||
|
||||
if currentCondition.Type != resourceCondition.Type ||
|
||||
currentCondition.Status != resourceCondition.Status ||
|
||||
currentCondition.Reason != resourceCondition.Reason ||
|
||||
currentCondition.Message != resourceCondition.Message ||
|
||||
!currentCondition.LastTransitionTime.Equal(&resourceCondition.LastTransitionTime) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Since access points are derived from route status and gateway conditions,
|
||||
// and we've already compared the route status above, we can assume that
|
||||
// if the route status hasn't changed, the access points calculation
|
||||
// will produce the same result. This avoids the need for complex
|
||||
// gateway fetching in the status comparison.
|
||||
//
|
||||
// If there are edge cases where gateway state changes but route status doesn't,
|
||||
// those will be caught in the next reconciliation cycle anyway.
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *KubernetesGatewayResource) ShouldCleanup(tcp *kamajiv1alpha1.TenantControlPlane) bool {
|
||||
return tcp.Spec.ControlPlane.Gateway == nil && tcp.Status.Kubernetes.Gateway != nil
|
||||
}
|
||||
|
||||
func (r *KubernetesGatewayResource) CleanUp(ctx context.Context, tcp *kamajiv1alpha1.TenantControlPlane) (bool, error) {
|
||||
logger := log.FromContext(ctx, "resource", r.GetName())
|
||||
|
||||
route := gatewayv1alpha2.TLSRoute{}
|
||||
if err := r.Client.Get(ctx, client.ObjectKey{
|
||||
Namespace: r.resource.GetNamespace(),
|
||||
Name: r.resource.GetName(),
|
||||
}, &route); err != nil {
|
||||
if !k8serrors.IsNotFound(err) {
|
||||
logger.Error(err, "failed to get TLSRoute before cleanup")
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if !metav1.IsControlledBy(&route, tcp) {
|
||||
logger.Info("skipping cleanup: route is not managed by Kamaji", "name", route.Name, "namespace", route.Namespace)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := r.Client.Delete(ctx, &route); err != nil {
|
||||
if !k8serrors.IsNotFound(err) {
|
||||
// TODO: Is that an error? Wanted to delete the resource anyways.
|
||||
logger.Error(err, "cannot cleanup tcp route")
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
logger.V(1).Info("tcp route cleaned up successfully")
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// fetchGatewayByListener uses the indexer to efficiently find a gateway with a specific listener.
|
||||
// This avoids the need to iterate through all listeners in a gateway.
|
||||
func (r *KubernetesGatewayResource) fetchGatewayByListener(ctx context.Context, ref gatewayv1.ParentReference) (*gatewayv1.Gateway, error) {
|
||||
if ref.Namespace == nil {
|
||||
return nil, fmt.Errorf("missing namespace")
|
||||
}
|
||||
if ref.SectionName == nil {
|
||||
return nil, fmt.Errorf("missing sectionName")
|
||||
}
|
||||
|
||||
// Build the composite key that matches our indexer format: namespace/gatewayName/listenerName
|
||||
listenerKey := fmt.Sprintf("%s/%s/%s", *ref.Namespace, ref.Name, *ref.SectionName)
|
||||
|
||||
// Query gateways using the indexer
|
||||
gatewayList := &gatewayv1.GatewayList{}
|
||||
if err := r.Client.List(ctx, gatewayList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(kamajiv1alpha1.GatewayListenerNameKey, listenerKey),
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to list gateways by listener: %w", err)
|
||||
}
|
||||
|
||||
if len(gatewayList.Items) == 0 {
|
||||
return nil, fmt.Errorf("no gateway found with listener '%s'", *ref.SectionName)
|
||||
}
|
||||
|
||||
// Since we're using a composite key with namespace/name/listener, we should get exactly one result
|
||||
if len(gatewayList.Items) > 1 {
|
||||
return nil, fmt.Errorf("found multiple gateways with listener '%s', expected exactly one", *ref.SectionName)
|
||||
}
|
||||
|
||||
return &gatewayList.Items[0], nil
|
||||
}
|
||||
|
||||
func FindMatchingListener(listeners []gatewayv1.Listener, ref gatewayv1.ParentReference) (gatewayv1.Listener, error) {
|
||||
if ref.SectionName == nil {
|
||||
return gatewayv1.Listener{}, fmt.Errorf("missing sectionName")
|
||||
}
|
||||
name := *ref.SectionName
|
||||
for _, listener := range listeners {
|
||||
if listener.Name == name {
|
||||
return listener, nil
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle the cases according to the spec:
|
||||
// - When both Port (experimental) and SectionName are
|
||||
// specified, the name and port of the selected listener
|
||||
// must match both specified values.
|
||||
// - When unspecified (empty string) this will reference
|
||||
// the entire resource [...] an attachment is considered
|
||||
// successful if at least one section in the parent resource accepts it
|
||||
|
||||
return gatewayv1.Listener{}, fmt.Errorf("could not find listener '%s'", name)
|
||||
}
|
||||
|
||||
func (r *KubernetesGatewayResource) UpdateTenantControlPlaneStatus(ctx context.Context, tcp *kamajiv1alpha1.TenantControlPlane) error {
|
||||
logger := log.FromContext(ctx, "resource", r.GetName())
|
||||
|
||||
// Clean up status if Gateway routes are no longer configured
|
||||
if tcp.Spec.ControlPlane.Gateway == nil {
|
||||
tcp.Status.Kubernetes.Gateway = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
tcp.Status.Kubernetes.Gateway = &kamajiv1alpha1.KubernetesGatewayStatus{
|
||||
RouteStatus: r.resource.Status.RouteStatus,
|
||||
RouteRef: v1.LocalObjectReference{
|
||||
Name: r.resource.Name,
|
||||
},
|
||||
}
|
||||
|
||||
routeStatuses := tcp.Status.Kubernetes.Gateway.RouteStatus
|
||||
|
||||
// TODO: Investigate the implications of having multiple parents / hostnames
|
||||
// TODO: Use condition to report?
|
||||
if len(routeStatuses.Parents) == 0 {
|
||||
return fmt.Errorf("no gateway attached to the route")
|
||||
}
|
||||
if len(routeStatuses.Parents) > 1 {
|
||||
return fmt.Errorf("too many gateway attached to the route")
|
||||
}
|
||||
if len(r.resource.Spec.Hostnames) == 0 {
|
||||
return fmt.Errorf("no hostname in the route")
|
||||
}
|
||||
if len(r.resource.Spec.Hostnames) > 1 {
|
||||
return fmt.Errorf("too many hostnames in the route")
|
||||
}
|
||||
|
||||
logger.V(1).Info("updating TenantControlPlane status for Gateway routes")
|
||||
accessPoints := []kamajiv1alpha1.GatewayAccessPoint{}
|
||||
for _, routeStatus := range routeStatuses.Parents {
|
||||
routeAccepted := meta.IsStatusConditionTrue(
|
||||
routeStatus.Conditions,
|
||||
string(gatewayv1.RouteConditionAccepted),
|
||||
)
|
||||
if !routeAccepted {
|
||||
continue
|
||||
}
|
||||
|
||||
// Use the indexer to efficiently find the gateway with the specific listener
|
||||
gateway, err := r.fetchGatewayByListener(ctx, routeStatus.ParentRef)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not fetch gateway with listener '%v': %w",
|
||||
routeStatus.ParentRef.SectionName, err)
|
||||
}
|
||||
gatewayProgrammed := meta.IsStatusConditionTrue(
|
||||
gateway.Status.Conditions,
|
||||
string(gatewayv1.GatewayConditionProgrammed),
|
||||
)
|
||||
if !gatewayProgrammed {
|
||||
continue
|
||||
}
|
||||
|
||||
// Since we fetched the gateway using the indexer, we know the listener exists
|
||||
// but we still need to get its details from the gateway spec
|
||||
listener, err := FindMatchingListener(
|
||||
gateway.Spec.Listeners, routeStatus.ParentRef,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to match listener: %w", err)
|
||||
}
|
||||
|
||||
for _, hostname := range r.resource.Spec.Hostnames {
|
||||
rawURL := fmt.Sprintf("https://%s:%d", hostname, listener.Port)
|
||||
url, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid url: %w", err)
|
||||
}
|
||||
|
||||
hostnameAddressType := gatewayv1.HostnameAddressType
|
||||
accessPoints = append(accessPoints, kamajiv1alpha1.GatewayAccessPoint{
|
||||
Type: &hostnameAddressType,
|
||||
Value: url.String(),
|
||||
Port: listener.Port,
|
||||
})
|
||||
}
|
||||
}
|
||||
tcp.Status.Kubernetes.Gateway.AccessPoints = accessPoints
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *KubernetesGatewayResource) Define(_ context.Context, tcp *kamajiv1alpha1.TenantControlPlane) error {
|
||||
r.resource = &gatewayv1alpha2.TLSRoute{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: tcp.GetName(),
|
||||
Namespace: tcp.GetNamespace(),
|
||||
},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *KubernetesGatewayResource) mutate(tcp *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
|
||||
return func() error {
|
||||
labels := utilities.MergeMaps(
|
||||
r.resource.GetLabels(),
|
||||
utilities.KamajiLabels(tcp.GetName(), r.GetName()),
|
||||
tcp.Spec.ControlPlane.Gateway.AdditionalMetadata.Labels,
|
||||
)
|
||||
r.resource.SetLabels(labels)
|
||||
|
||||
annotations := utilities.MergeMaps(
|
||||
r.resource.GetAnnotations(),
|
||||
tcp.Spec.ControlPlane.Gateway.AdditionalMetadata.Annotations)
|
||||
r.resource.SetAnnotations(annotations)
|
||||
|
||||
if tcp.Spec.ControlPlane.Gateway.GatewayParentRefs != nil {
|
||||
r.resource.Spec.ParentRefs = tcp.Spec.ControlPlane.Gateway.GatewayParentRefs
|
||||
}
|
||||
|
||||
serviceName := gatewayv1alpha2.ObjectName(tcp.Status.Kubernetes.Service.Name)
|
||||
servicePort := tcp.Status.Kubernetes.Service.Port
|
||||
|
||||
if serviceName == "" || servicePort == 0 {
|
||||
return fmt.Errorf("service not ready, cannot create TLSRoute")
|
||||
}
|
||||
|
||||
rule := gatewayv1alpha2.TLSRouteRule{
|
||||
BackendRefs: []gatewayv1alpha2.BackendRef{
|
||||
{
|
||||
BackendObjectReference: gatewayv1alpha2.BackendObjectReference{
|
||||
Name: serviceName,
|
||||
Port: &servicePort,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r.resource.Spec.Hostnames = []gatewayv1.Hostname{tcp.Spec.ControlPlane.Gateway.Hostname}
|
||||
r.resource.Spec.Rules = []gatewayv1alpha2.TLSRouteRule{rule}
|
||||
|
||||
return controllerutil.SetControllerReference(tcp, r.resource, r.Client.Scheme())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *KubernetesGatewayResource) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
|
||||
logger := log.FromContext(ctx, "resource", r.GetName())
|
||||
|
||||
if tenantControlPlane.Spec.ControlPlane.Gateway == nil {
|
||||
return controllerutil.OperationResultNone, nil
|
||||
}
|
||||
|
||||
if len(tenantControlPlane.Spec.ControlPlane.Gateway.Hostname) == 0 {
|
||||
return controllerutil.OperationResultNone, fmt.Errorf("missing hostname to expose the Tenant Control Plane using a Gateway resource")
|
||||
}
|
||||
|
||||
logger.V(1).Info("creating or updating resource gateway routes")
|
||||
|
||||
result, err := utilities.CreateOrUpdateWithConflict(ctx, r.Client, r.resource, r.mutate(tenantControlPlane))
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *KubernetesGatewayResource) GetName() string {
|
||||
return "gateway_routes"
|
||||
}
|
||||
238
internal/resources/k8s_gateway_resource_test.go
Normal file
238
internal/resources/k8s_gateway_resource_test.go
Normal file
@@ -0,0 +1,238 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package resources_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
"github.com/clastix/kamaji/internal/resources"
|
||||
)
|
||||
|
||||
func TestGatewayResource(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Gateway Resource Suite")
|
||||
}
|
||||
|
||||
var runtimeScheme *runtime.Scheme
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
runtimeScheme = runtime.NewScheme()
|
||||
Expect(scheme.AddToScheme(runtimeScheme)).To(Succeed())
|
||||
Expect(kamajiv1alpha1.AddToScheme(runtimeScheme)).To(Succeed())
|
||||
Expect(gatewayv1alpha2.Install(runtimeScheme)).To(Succeed())
|
||||
})
|
||||
|
||||
var _ = Describe("KubernetesGatewayResource", func() {
|
||||
var (
|
||||
tcp *kamajiv1alpha1.TenantControlPlane
|
||||
resource *resources.KubernetesGatewayResource
|
||||
ctx context.Context
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(runtimeScheme).
|
||||
Build()
|
||||
|
||||
resource = &resources.KubernetesGatewayResource{
|
||||
Client: fakeClient,
|
||||
}
|
||||
|
||||
tcp = &kamajiv1alpha1.TenantControlPlane{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-tcp",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: kamajiv1alpha1.TenantControlPlaneSpec{
|
||||
ControlPlane: kamajiv1alpha1.ControlPlane{
|
||||
Gateway: &kamajiv1alpha1.GatewaySpec{
|
||||
Hostname: gatewayv1alpha2.Hostname("test.example.com"),
|
||||
AdditionalMetadata: kamajiv1alpha1.AdditionalMetadata{
|
||||
Labels: map[string]string{
|
||||
"test-label": "test-value",
|
||||
},
|
||||
},
|
||||
GatewayParentRefs: []gatewayv1alpha2.ParentReference{
|
||||
{
|
||||
Name: "test-gateway",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: kamajiv1alpha1.TenantControlPlaneStatus{
|
||||
Kubernetes: kamajiv1alpha1.KubernetesStatus{
|
||||
Service: kamajiv1alpha1.KubernetesServiceStatus{
|
||||
Name: "test-service",
|
||||
Port: 6443,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
Context("When GatewayRoutes is configured", func() {
|
||||
It("should not cleanup", func() {
|
||||
Expect(resource.ShouldCleanup(tcp)).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should define route resources", func() {
|
||||
err := resource.Define(ctx, tcp)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should require status update when GatewayRoutes is configured but status is nil", func() {
|
||||
tcp.Status.Kubernetes.Gateway = nil
|
||||
shouldUpdate := resource.ShouldStatusBeUpdated(ctx, tcp)
|
||||
Expect(shouldUpdate).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
Context("When GatewayRoutes is not configured", func() {
|
||||
BeforeEach(func() {
|
||||
tcp.Spec.ControlPlane.Gateway = nil
|
||||
tcp.Status.Kubernetes.Gateway = &kamajiv1alpha1.KubernetesGatewayStatus{
|
||||
AccessPoints: nil,
|
||||
}
|
||||
})
|
||||
|
||||
It("should cleanup", func() {
|
||||
Expect(resource.ShouldCleanup(tcp)).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should not require status update when both spec and status are nil", func() {
|
||||
tcp.Status.Kubernetes.Gateway = nil
|
||||
shouldUpdate := resource.ShouldStatusBeUpdated(ctx, tcp)
|
||||
Expect(shouldUpdate).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
Context("When hostname is missing", func() {
|
||||
BeforeEach(func() {
|
||||
tcp.Spec.ControlPlane.Gateway.Hostname = ""
|
||||
})
|
||||
|
||||
It("should fail to create or update", func() {
|
||||
err := resource.Define(ctx, tcp)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = resource.CreateOrUpdate(ctx, tcp)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("missing hostname"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("When service is not ready", func() {
|
||||
BeforeEach(func() {
|
||||
tcp.Status.Kubernetes.Service.Name = ""
|
||||
tcp.Status.Kubernetes.Service.Port = 0
|
||||
})
|
||||
|
||||
It("should fail to create or update", func() {
|
||||
err := resource.Define(ctx, tcp)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = resource.CreateOrUpdate(ctx, tcp)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("service not ready"))
|
||||
})
|
||||
})
|
||||
|
||||
It("should return correct resource name", func() {
|
||||
Expect(resource.GetName()).To(Equal("gateway_routes"))
|
||||
})
|
||||
|
||||
Describe("findMatchingListener", func() {
|
||||
var (
|
||||
listeners []gatewayv1.Listener
|
||||
ref gatewayv1.ParentReference
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
listeners = []gatewayv1.Listener{
|
||||
{
|
||||
Name: "first",
|
||||
Port: gatewayv1.PortNumber(443),
|
||||
},
|
||||
{
|
||||
Name: "middle",
|
||||
Port: gatewayv1.PortNumber(6443),
|
||||
},
|
||||
{
|
||||
Name: "last",
|
||||
Port: gatewayv1.PortNumber(80),
|
||||
},
|
||||
}
|
||||
ref = gatewayv1.ParentReference{
|
||||
Name: "test-gateway",
|
||||
}
|
||||
})
|
||||
|
||||
It("should return an error when sectionName is nil", func() {
|
||||
listener, err := resources.FindMatchingListener(listeners, ref)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("missing sectionName"))
|
||||
Expect(listener).To(Equal(gatewayv1.Listener{}))
|
||||
})
|
||||
It("should return an error when sectionName is an empty string", func() {
|
||||
sectionName := gatewayv1.SectionName("")
|
||||
ref.SectionName = §ionName
|
||||
|
||||
listener, err := resources.FindMatchingListener(listeners, ref)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("could not find listener ''"))
|
||||
Expect(listener).To(Equal(gatewayv1.Listener{}))
|
||||
})
|
||||
|
||||
It("should return the matching listener when sectionName points to an existing listener", func() {
|
||||
sectionName := gatewayv1.SectionName("middle")
|
||||
ref.SectionName = §ionName
|
||||
|
||||
listener, err := resources.FindMatchingListener(listeners, ref)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(listener.Port).To(Equal(gatewayv1.PortNumber(6443)))
|
||||
})
|
||||
|
||||
It("should return an error when sectionName points to a non-existent listener", func() {
|
||||
sectionName := gatewayv1.SectionName("non-existent")
|
||||
ref.SectionName = §ionName
|
||||
|
||||
listener, err := resources.FindMatchingListener(listeners, ref)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("could not find listener 'non-existent'"))
|
||||
Expect(listener).To(Equal(gatewayv1.Listener{}))
|
||||
})
|
||||
|
||||
It("should return the first listener", func() {
|
||||
sectionName := gatewayv1.SectionName("first")
|
||||
ref.SectionName = §ionName
|
||||
|
||||
listener, err := resources.FindMatchingListener(listeners, ref)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(listener.Port).To(Equal(gatewayv1.PortNumber(443)))
|
||||
})
|
||||
|
||||
It("should return the last listener when matching by name", func() {
|
||||
sectionName := gatewayv1.SectionName("last")
|
||||
ref.SectionName = §ionName
|
||||
|
||||
listener, err := resources.FindMatchingListener(listeners, ref)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(listener.Port).To(Equal(gatewayv1.PortNumber(80)))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -77,14 +77,6 @@ func (r *KubeadmConfigResource) UpdateTenantControlPlaneStatus(_ context.Context
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *KubeadmConfigResource) getControlPlaneEndpoint(ingress *kamajiv1alpha1.IngressSpec, address string, port int32) string {
|
||||
if ingress != nil && len(ingress.Hostname) > 0 {
|
||||
address, port = utilities.GetControlPlaneAddressAndPortFromHostname(ingress.Hostname, port)
|
||||
}
|
||||
|
||||
return net.JoinHostPort(address, strconv.FormatInt(int64(port), 10))
|
||||
}
|
||||
|
||||
func (r *KubeadmConfigResource) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
|
||||
return func() error {
|
||||
logger := log.FromContext(ctx, "resource", r.GetName())
|
||||
@@ -98,12 +90,27 @@ func (r *KubeadmConfigResource) mutate(ctx context.Context, tenantControlPlane *
|
||||
|
||||
r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
|
||||
|
||||
endpoint := net.JoinHostPort(address, strconv.FormatInt(int64(port), 10))
|
||||
spec := tenantControlPlane.Spec.ControlPlane
|
||||
if spec.Gateway != nil {
|
||||
if len(spec.Gateway.Hostname) > 0 {
|
||||
gaddr, gport := utilities.GetControlPlaneAddressAndPortFromHostname(string(spec.Gateway.Hostname), port)
|
||||
endpoint = net.JoinHostPort(gaddr, strconv.FormatInt(int64(gport), 10))
|
||||
}
|
||||
}
|
||||
if spec.Ingress != nil {
|
||||
if len(spec.Ingress.Hostname) > 0 {
|
||||
iaddr, iport := utilities.GetControlPlaneAddressAndPortFromHostname(spec.Ingress.Hostname, port)
|
||||
endpoint = net.JoinHostPort(iaddr, strconv.FormatInt(int64(iport), 10))
|
||||
}
|
||||
}
|
||||
|
||||
params := kubeadm.Parameters{
|
||||
TenantControlPlaneAddress: address,
|
||||
TenantControlPlanePort: port,
|
||||
TenantControlPlaneName: tenantControlPlane.GetName(),
|
||||
TenantControlPlaneNamespace: tenantControlPlane.GetNamespace(),
|
||||
TenantControlPlaneEndpoint: r.getControlPlaneEndpoint(tenantControlPlane.Spec.ControlPlane.Ingress, address, port),
|
||||
TenantControlPlaneEndpoint: endpoint,
|
||||
TenantControlPlaneCertSANs: tenantControlPlane.Spec.NetworkProfile.CertSANs,
|
||||
TenantControlPlaneClusterDomain: tenantControlPlane.Spec.NetworkProfile.ClusterDomain,
|
||||
TenantControlPlanePodCIDR: tenantControlPlane.Spec.NetworkProfile.PodCIDR,
|
||||
|
||||
@@ -16,6 +16,7 @@ var (
|
||||
frontproxycaCollector prometheus.Histogram
|
||||
deploymentCollector prometheus.Histogram
|
||||
ingressCollector prometheus.Histogram
|
||||
gatewayCollector prometheus.Histogram
|
||||
serviceCollector prometheus.Histogram
|
||||
kubeadmconfigCollector prometheus.Histogram
|
||||
kubeadmupgradeCollector prometheus.Histogram
|
||||
|
||||
109
internal/utilities/gateway_discovery.go
Normal file
109
internal/utilities/gateway_discovery.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package utilities
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/discovery"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
)
|
||||
|
||||
// AreGatewayResourcesAvailable checks if Gateway API is available in the cluster through a discovery Client
|
||||
// with fallback to client-based check.
|
||||
func AreGatewayResourcesAvailable(ctx context.Context, c client.Client, discoveryClient discovery.DiscoveryInterface) bool {
|
||||
if discoveryClient == nil {
|
||||
return IsGatewayAPIAvailableViaClient(ctx, c)
|
||||
}
|
||||
|
||||
available, err := GatewayAPIResourcesAvailable(ctx, discoveryClient)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return available
|
||||
}
|
||||
|
||||
// NOTE: These functions are extremely similar, maybe they can be merged and accept a GVK.
|
||||
// Explicit for now.
|
||||
// GatewayAPIResourcesAvailable checks if Gateway API is available in the cluster.
|
||||
func GatewayAPIResourcesAvailable(ctx context.Context, discoveryClient discovery.DiscoveryInterface) (bool, error) {
|
||||
gatewayAPIGroup := gatewayv1.GroupName
|
||||
|
||||
serverGroups, err := discoveryClient.ServerGroups()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, group := range serverGroups.Groups {
|
||||
if group.Name == gatewayAPIGroup {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// TLSRouteAPIAvailable checks specifically for TLSRoute resource availability.
|
||||
func TLSRouteAPIAvailable(ctx context.Context, discoveryClient discovery.DiscoveryInterface) (bool, error) {
|
||||
gv := gatewayv1alpha2.SchemeGroupVersion
|
||||
|
||||
resourceList, err := discoveryClient.ServerResourcesForGroupVersion(gv.String())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, resource := range resourceList.APIResources {
|
||||
if resource.Kind == "TLSRoute" {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// IsTLSRouteAvailable checks if TLSRoute is available with fallback to client-based check.
|
||||
func IsTLSRouteAvailable(ctx context.Context, c client.Client, discoveryClient discovery.DiscoveryInterface) bool {
|
||||
if discoveryClient == nil {
|
||||
return IsTLSRouteAvailableViaClient(ctx, c)
|
||||
}
|
||||
|
||||
available, err := TLSRouteAPIAvailable(ctx, discoveryClient)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return available
|
||||
}
|
||||
|
||||
// IsTLSRouteAvailableViaClient uses client to check TLSRoute availability.
|
||||
func IsTLSRouteAvailableViaClient(ctx context.Context, c client.Client) bool {
|
||||
// Try to check if TLSRoute GVK can be resolved
|
||||
gvk := schema.GroupVersionKind{
|
||||
Group: gatewayv1alpha2.GroupName,
|
||||
Version: "v1alpha2",
|
||||
Kind: "TLSRoute",
|
||||
}
|
||||
|
||||
restMapper := c.RESTMapper()
|
||||
_, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
|
||||
if err != nil {
|
||||
if meta.IsNoMatchError(err) {
|
||||
return false
|
||||
}
|
||||
// Other errors might be transient, assume available
|
||||
return true
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsGatewayAPIAvailableViaClient uses client to check Gateway API availability.
|
||||
func IsGatewayAPIAvailableViaClient(ctx context.Context, c client.Client) bool {
|
||||
return IsTLSRouteAvailableViaClient(ctx, c)
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func (t TenantControlPlaneDataStore) OnCreate(object runtime.Object) AdmissionRe
|
||||
return nil, t.check(ctx, tcp.Spec.DataStore)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return nil, t.checkDataStoreOverrides(ctx, tcp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,3 +61,19 @@ func (t TenantControlPlaneDataStore) check(ctx context.Context, dataStoreName st
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TenantControlPlaneDataStore) checkDataStoreOverrides(ctx context.Context, tcp *kamajiv1alpha1.TenantControlPlane) error {
|
||||
overrideCheck := make(map[string]struct{}, 0)
|
||||
for _, ds := range tcp.Spec.DataStoreOverrides {
|
||||
if _, exists := overrideCheck[ds.Resource]; !exists {
|
||||
overrideCheck[ds.Resource] = struct{}{}
|
||||
} else {
|
||||
return fmt.Errorf("duplicate resource override in Spec.DataStoreOverrides")
|
||||
}
|
||||
if err := t.check(ctx, ds.DataStore); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
94
internal/webhook/handlers/tcp_datastore_test.go
Normal file
94
internal/webhook/handlers/tcp_datastore_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
)
|
||||
|
||||
var _ = Describe("TCP Datastore webhook", func() {
|
||||
var (
|
||||
ctx context.Context
|
||||
t TenantControlPlaneDataStore
|
||||
tcp *kamajiv1alpha1.TenantControlPlane
|
||||
)
|
||||
BeforeEach(func() {
|
||||
scheme := runtime.NewScheme()
|
||||
utilruntime.Must(kamajiv1alpha1.AddToScheme(scheme))
|
||||
|
||||
ctx = context.Background()
|
||||
t = TenantControlPlaneDataStore{
|
||||
Client: fakeclient.NewClientBuilder().WithScheme(scheme).WithObjects(&kamajiv1alpha1.DataStore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
}, &kamajiv1alpha1.DataStore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar",
|
||||
},
|
||||
}).Build(),
|
||||
}
|
||||
tcp = &kamajiv1alpha1.TenantControlPlane{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tcp",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: kamajiv1alpha1.TenantControlPlaneSpec{},
|
||||
}
|
||||
})
|
||||
Describe("validation should succeed without DataStoreOverrides", func() {
|
||||
It("should validate TCP without DataStoreOverrides", func() {
|
||||
err := t.checkDataStoreOverrides(ctx, tcp)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("validation should fail with duplicate resources in DataStoreOverrides", func() {
|
||||
It("should fail to validate TCP with duplicate resources in DataStoreOverrides", func() {
|
||||
tcp.Spec.DataStoreOverrides = []kamajiv1alpha1.DataStoreOverride{{
|
||||
Resource: "/event",
|
||||
DataStore: "foo",
|
||||
}, {
|
||||
Resource: "/event",
|
||||
DataStore: "bar",
|
||||
}}
|
||||
err := t.checkDataStoreOverrides(ctx, tcp)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("validation should succeed with valid DataStoreOverrides", func() {
|
||||
It("should validate TCP with valid DataStoreOverrides", func() {
|
||||
tcp.Spec.DataStoreOverrides = []kamajiv1alpha1.DataStoreOverride{{
|
||||
Resource: "/leases",
|
||||
DataStore: "foo",
|
||||
}, {
|
||||
Resource: "/event",
|
||||
DataStore: "bar",
|
||||
}}
|
||||
err := t.checkDataStoreOverrides(ctx, tcp)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("validation should fail with nonexistent DataStoreOverrides", func() {
|
||||
It("should fail to validate TCP with nonexistent DataStoreOverrides", func() {
|
||||
tcp.Spec.DataStoreOverrides = []kamajiv1alpha1.DataStoreOverride{{
|
||||
Resource: "/leases",
|
||||
DataStore: "baz",
|
||||
}}
|
||||
err := t.checkDataStoreOverrides(ctx, tcp)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -67,6 +67,20 @@ func (t TenantControlPlaneDeployment) OnUpdate(newObject runtime.Object, oldObje
|
||||
}
|
||||
t.DeploymentBuilder.DataStore = ds
|
||||
|
||||
dataStoreOverrides := make([]controlplane.DataStoreOverrides, 0, len(tcp.Spec.DataStoreOverrides))
|
||||
|
||||
for _, dso := range tcp.Spec.DataStoreOverrides {
|
||||
ds := kamajiv1alpha1.DataStore{}
|
||||
if err := t.Client.Get(ctx, types.NamespacedName{Name: dso.DataStore}, &ds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dataStoreOverrides = append(dataStoreOverrides, controlplane.DataStoreOverrides{
|
||||
Resource: dso.Resource,
|
||||
DataStore: ds,
|
||||
})
|
||||
}
|
||||
t.DeploymentBuilder.DataStoreOverrides = dataStoreOverrides
|
||||
|
||||
deployment := appsv1.Deployment{}
|
||||
deployment.Name = tcp.Name
|
||||
deployment.Namespace = tcp.Namespace
|
||||
|
||||
77
internal/webhook/handlers/tcp_gateway_validate.go
Normal file
77
internal/webhook/handlers/tcp_gateway_validate.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright 2022 Clastix Labs
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gomodules.xyz/jsonpatch/v2"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/discovery"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
"github.com/clastix/kamaji/internal/utilities"
|
||||
"github.com/clastix/kamaji/internal/webhook/utils"
|
||||
)
|
||||
|
||||
type TenantControlPlaneGatewayValidation struct {
|
||||
Client client.Client
|
||||
DiscoveryClient discovery.DiscoveryInterface
|
||||
}
|
||||
|
||||
func (t TenantControlPlaneGatewayValidation) OnCreate(object runtime.Object) AdmissionResponse {
|
||||
return func(ctx context.Context, _ admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
|
||||
tcp, ok := object.(*kamajiv1alpha1.TenantControlPlane)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cannot cast object to TenantControlPlane")
|
||||
}
|
||||
|
||||
if tcp.Spec.ControlPlane.Gateway != nil {
|
||||
// NOTE: Do we actually want to deny here if Gateway API is not available or a warning?
|
||||
// Seems sensible to deny to avoid anything.
|
||||
if err := t.validateGatewayAPIAvailability(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (t TenantControlPlaneGatewayValidation) OnUpdate(object runtime.Object, _ runtime.Object) AdmissionResponse {
|
||||
return func(ctx context.Context, _ admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
|
||||
tcp, ok := object.(*kamajiv1alpha1.TenantControlPlane)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cannot cast object to TenantControlPlane")
|
||||
}
|
||||
|
||||
if tcp.Spec.ControlPlane.Gateway != nil {
|
||||
if err := t.validateGatewayAPIAvailability(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (t TenantControlPlaneGatewayValidation) OnDelete(object runtime.Object) AdmissionResponse {
|
||||
return utils.NilOp()
|
||||
}
|
||||
|
||||
func (t TenantControlPlaneGatewayValidation) validateGatewayAPIAvailability(ctx context.Context) error {
|
||||
if !utilities.AreGatewayResourcesAvailable(ctx, t.Client, t.DiscoveryClient) {
|
||||
return fmt.Errorf("the Gateway API is not available in this cluster, cannot use gatewayRoute configuration")
|
||||
}
|
||||
|
||||
// Additional check for TLSRoute specifically
|
||||
if !utilities.IsTLSRouteAvailable(ctx, t.Client, t.DiscoveryClient) {
|
||||
return fmt.Errorf("TLSRoute resource is not available in this cluster")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
253
internal/webhook/handlers/tcp_gateway_validate_test.go
Normal file
253
internal/webhook/handlers/tcp_gateway_validate_test.go
Normal file
@@ -0,0 +1,253 @@
|
||||
// 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"
|
||||
"k8s.io/client-go/discovery"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
|
||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||
"github.com/clastix/kamaji/internal/webhook/handlers"
|
||||
)
|
||||
|
||||
// Mock discovery client for testing.
|
||||
type mockDiscoveryClient struct {
|
||||
discovery.DiscoveryInterface
|
||||
serverGroups *metav1.APIGroupList
|
||||
serverGroupsError error
|
||||
serverResources map[string]*metav1.APIResourceList
|
||||
serverResourcesError map[string]error
|
||||
}
|
||||
|
||||
func (m *mockDiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) {
|
||||
return m.serverGroups, m.serverGroupsError
|
||||
}
|
||||
|
||||
func (m *mockDiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
|
||||
if err, exists := m.serverResourcesError[groupVersion]; exists {
|
||||
return nil, err
|
||||
}
|
||||
if resources, exists := m.serverResources[groupVersion]; exists {
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
return &metav1.APIResourceList{}, nil
|
||||
}
|
||||
|
||||
var _ = Describe("TCP Gateway Validation Webhook", func() {
|
||||
var (
|
||||
ctx context.Context
|
||||
handler handlers.TenantControlPlaneGatewayValidation
|
||||
tcp *kamajiv1alpha1.TenantControlPlane
|
||||
mockClient client.Client
|
||||
mockDiscovery *mockDiscoveryClient
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
mockClient = nil
|
||||
mockDiscovery = &mockDiscoveryClient{
|
||||
serverResources: make(map[string]*metav1.APIResourceList),
|
||||
serverResourcesError: make(map[string]error),
|
||||
}
|
||||
|
||||
handler = handlers.TenantControlPlaneGatewayValidation{
|
||||
Client: mockClient,
|
||||
DiscoveryClient: mockDiscovery,
|
||||
}
|
||||
|
||||
tcp = &kamajiv1alpha1.TenantControlPlane{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-tcp",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: kamajiv1alpha1.TenantControlPlaneSpec{},
|
||||
}
|
||||
})
|
||||
|
||||
Context("when TenantControlPlane has no Gateway configuration", func() {
|
||||
It("should allow creation without Gateway APIs", func() {
|
||||
mockDiscovery.serverGroups = &metav1.APIGroupList{
|
||||
Groups: []metav1.APIGroup{},
|
||||
}
|
||||
|
||||
_, err := handler.OnCreate(tcp)(ctx, admission.Request{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should allow creation with Gateway APIs available", func() {
|
||||
mockDiscovery.serverGroups = &metav1.APIGroupList{
|
||||
Groups: []metav1.APIGroup{
|
||||
{Name: "gateway.networking.k8s.io"},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := handler.OnCreate(tcp)(ctx, admission.Request{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Context("when TenantControlPlane has Gateway configuration", func() {
|
||||
BeforeEach(func() {
|
||||
tcp.Spec.ControlPlane.Gateway = &kamajiv1alpha1.GatewaySpec{
|
||||
Hostname: gatewayv1.Hostname("api.example.com"),
|
||||
}
|
||||
})
|
||||
|
||||
Context("and Gateway APIs are available", func() {
|
||||
BeforeEach(func() {
|
||||
mockDiscovery.serverGroups = &metav1.APIGroupList{
|
||||
Groups: []metav1.APIGroup{
|
||||
{Name: "gateway.networking.k8s.io"},
|
||||
},
|
||||
}
|
||||
mockDiscovery.serverResources["gateway.networking.k8s.io/v1alpha2"] = &metav1.APIResourceList{
|
||||
APIResources: []metav1.APIResource{
|
||||
{Kind: "TLSRoute"},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
It("should allow creation", func() {
|
||||
_, err := handler.OnCreate(tcp)(ctx, admission.Request{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should allow updates", func() {
|
||||
oldTCP := tcp.DeepCopy()
|
||||
_, err := handler.OnUpdate(tcp, oldTCP)(ctx, admission.Request{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Context("and Gateway APIs are not available", func() {
|
||||
BeforeEach(func() {
|
||||
mockDiscovery.serverGroups = &metav1.APIGroupList{
|
||||
Groups: []metav1.APIGroup{},
|
||||
}
|
||||
})
|
||||
|
||||
It("should deny creation with clear error message", func() {
|
||||
_, err := handler.OnCreate(tcp)(ctx, admission.Request{})
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("Gateway API is not available in this cluster"))
|
||||
})
|
||||
|
||||
It("should deny updates with clear error message", func() {
|
||||
oldTCP := tcp.DeepCopy()
|
||||
_, err := handler.OnUpdate(tcp, oldTCP)(ctx, admission.Request{})
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("Gateway API is not available in this cluster"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("and Gateway API group exists but TLSRoute is not available", func() {
|
||||
BeforeEach(func() {
|
||||
mockDiscovery.serverGroups = &metav1.APIGroupList{
|
||||
Groups: []metav1.APIGroup{
|
||||
{Name: "gateway.networking.k8s.io"},
|
||||
},
|
||||
}
|
||||
mockDiscovery.serverResources["gateway.networking.k8s.io/v1alpha2"] = &metav1.APIResourceList{
|
||||
APIResources: []metav1.APIResource{
|
||||
{Kind: "Gateway"},
|
||||
{Kind: "HTTPRoute"},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
It("should deny creation when TLSRoute is missing", func() {
|
||||
_, err := handler.OnCreate(tcp)(ctx, admission.Request{})
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("TLSRoute resource is not available"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("when Gateway configuration is added in update", func() {
|
||||
It("should validate Gateway APIs when adding Gateway configuration", func() {
|
||||
oldTCP := tcp.DeepCopy()
|
||||
|
||||
tcp.Spec.ControlPlane.Gateway = &kamajiv1alpha1.GatewaySpec{
|
||||
Hostname: gatewayv1.Hostname("api.example.com"),
|
||||
}
|
||||
|
||||
mockDiscovery.serverGroups = &metav1.APIGroupList{
|
||||
Groups: []metav1.APIGroup{},
|
||||
}
|
||||
|
||||
_, err := handler.OnUpdate(tcp, oldTCP)(ctx, admission.Request{})
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("Gateway API is not available"))
|
||||
})
|
||||
|
||||
It("should allow removing Gateway configuration", func() {
|
||||
// Start with Gateway configuration
|
||||
oldTCP := tcp.DeepCopy()
|
||||
oldTCP.Spec.ControlPlane.Gateway = &kamajiv1alpha1.GatewaySpec{
|
||||
Hostname: gatewayv1.Hostname("api.example.com"),
|
||||
}
|
||||
|
||||
mockDiscovery.serverGroups = &metav1.APIGroupList{
|
||||
Groups: []metav1.APIGroup{},
|
||||
}
|
||||
|
||||
_, err := handler.OnUpdate(tcp, oldTCP)(ctx, admission.Request{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Context("OnDelete operations", func() {
|
||||
It("should always allow delete operations", func() {
|
||||
tcp.Spec.ControlPlane.Gateway = &kamajiv1alpha1.GatewaySpec{
|
||||
Hostname: gatewayv1.Hostname("api.example.com"),
|
||||
}
|
||||
|
||||
mockDiscovery.serverGroups = &metav1.APIGroupList{
|
||||
Groups: []metav1.APIGroup{},
|
||||
}
|
||||
|
||||
admissionResponse := handler.OnDelete(tcp)
|
||||
_, err := admissionResponse(ctx, admission.Request{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Context("with different Gateway API versions", func() {
|
||||
BeforeEach(func() {
|
||||
tcp.Spec.ControlPlane.Gateway = &kamajiv1alpha1.GatewaySpec{
|
||||
Hostname: gatewayv1.Hostname("api.example.com"),
|
||||
}
|
||||
mockDiscovery.serverGroups = &metav1.APIGroupList{
|
||||
Groups: []metav1.APIGroup{
|
||||
{Name: "gateway.networking.k8s.io"},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
It("should work with v1alpha2 TLSRoute", func() {
|
||||
mockDiscovery.serverResources["gateway.networking.k8s.io/v1alpha2"] = &metav1.APIResourceList{
|
||||
APIResources: []metav1.APIResource{
|
||||
{Kind: "TLSRoute"},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := handler.OnCreate(tcp)(ctx, admission.Request{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should handle missing version gracefully", func() {
|
||||
_, err := handler.OnCreate(tcp)(ctx, admission.Request{})
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("TLSRoute resource is not available"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -29,6 +29,8 @@ func (t TenantControlPlaneVersion) OnCreate(object runtime.Object) AdmissionResp
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to parse the desired Kubernetes version")
|
||||
}
|
||||
// No need to check if the patch version
|
||||
ver.Patch = 0
|
||||
|
||||
supportedVer, supportedErr := semver.Make(t.normalizeKubernetesVersion(upgrade.KubeadmVersion))
|
||||
if supportedErr != nil {
|
||||
@@ -36,7 +38,7 @@ func (t TenantControlPlaneVersion) OnCreate(object runtime.Object) AdmissionResp
|
||||
}
|
||||
|
||||
if ver.GT(supportedVer) {
|
||||
return nil, fmt.Errorf("unable to create a TenantControlPlane with a Kubernetes version greater than the supported one, actually %s", supportedVer.String())
|
||||
return nil, fmt.Errorf("unable to create a TenantControlPlane with a Kubernetes version greater than the supported one, actually v%d.%d", supportedVer.Major, supportedVer.Minor)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
@@ -59,15 +61,23 @@ func (t TenantControlPlaneVersion) OnUpdate(object runtime.Object, oldObject run
|
||||
return func(context.Context, admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
|
||||
newTCP, oldTCP := object.(*kamajiv1alpha1.TenantControlPlane), oldObject.(*kamajiv1alpha1.TenantControlPlane) //nolint:forcetypeassert
|
||||
|
||||
if newTCP.DeletionTimestamp != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
oldVer, oldErr := semver.Make(t.normalizeKubernetesVersion(oldTCP.Spec.Kubernetes.Version))
|
||||
if oldErr != nil {
|
||||
return nil, errors.Wrap(oldErr, "unable to parse the previous Kubernetes version")
|
||||
}
|
||||
// No need to check if the patch version
|
||||
oldVer.Patch = 0
|
||||
|
||||
newVer, newErr := semver.New(t.normalizeKubernetesVersion(newTCP.Spec.Kubernetes.Version))
|
||||
if newErr != nil {
|
||||
return nil, errors.Wrap(newErr, "unable to parse the desired Kubernetes version")
|
||||
}
|
||||
// No need to check if the patch version
|
||||
newVer.Patch = 0
|
||||
|
||||
supportedVer, supportedErr := semver.Make(t.normalizeKubernetesVersion(upgrade.KubeadmVersion))
|
||||
if supportedErr != nil {
|
||||
@@ -76,7 +86,7 @@ func (t TenantControlPlaneVersion) OnUpdate(object runtime.Object, oldObject run
|
||||
|
||||
switch {
|
||||
case newVer.GT(supportedVer):
|
||||
return nil, fmt.Errorf("unable to upgrade to a version greater than the supported one, actually %s", supportedVer.String())
|
||||
return nil, fmt.Errorf("unable to upgrade to a version greater than the supported one (v%d.%d)", supportedVer.Major, supportedVer.Minor)
|
||||
case newVer.LT(oldVer):
|
||||
return nil, fmt.Errorf("unable to downgrade a TenantControlPlane from %s to %s", oldVer.String(), newVer.String())
|
||||
case newVer.Minor-oldVer.Minor > 1:
|
||||
|
||||
Reference in New Issue
Block a user