mirror of
https://github.com/clastix/kamaji.git
synced 2026-02-14 10:00:02 +00:00
feat: extend Gateway API support to Konnectivity addons (#1054)
This change extends Gateway API support to Konnectivity addons. When `spec.controlPlane.gateway` is configured and Konnectivity addon is enabled, Kamaji automatically creates two TLSRoutes: 1. A Control plane TLSRoute (port 6443, sectionName "kube-apiserver") 2. A Konnectivity TLSRoute (port 8132, sectionName "konnectivity-server") Both routes use the hostname specified in `gateway.hostname` and reference the same Gateway resource via `parentRefs`, with `port` and `sectionName` set automatically by Kamaji. This patch also adds CEL validation to prevent users from specifying `port` or `sectionName` in Gateway `parentRefs`, as these fields are now managed automatically by Kamaji. Signed-off-by: Parth Yadav <parth@coredge.io>
This commit is contained in:
6
Makefile
6
Makefile
@@ -248,6 +248,10 @@ gateway-api:
|
|||||||
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/experimental-install.yaml
|
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
|
kubectl wait --for=condition=Established crd/gateways.gateway.networking.k8s.io --timeout=60s
|
||||||
|
|
||||||
|
envoy-gateway: gateway-api helm ## Install Envoy Gateway for Gateway API tests.
|
||||||
|
$(HELM) upgrade --install eg oci://docker.io/envoyproxy/gateway-helm --version v1.6.1 -n envoy-gateway-system --create-namespace
|
||||||
|
kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available
|
||||||
|
|
||||||
load: kind
|
load: kind
|
||||||
$(KIND) load docker-image --name kamaji ${CONTAINER_REPOSITORY}:${VERSION}
|
$(KIND) load docker-image --name kamaji ${CONTAINER_REPOSITORY}:${VERSION}
|
||||||
|
|
||||||
@@ -261,7 +265,7 @@ cleanup: kind
|
|||||||
$(KIND) delete cluster --name kamaji
|
$(KIND) delete cluster --name kamaji
|
||||||
|
|
||||||
.PHONY: e2e
|
.PHONY: e2e
|
||||||
e2e: env build load helm ginkgo cert-manager gateway-api ## Create a KinD cluster, install Kamaji on it and run the test suite.
|
e2e: env build load helm ginkgo cert-manager gateway-api envoy-gateway ## 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) upgrade --debug --install kamaji-crds ./charts/kamaji-crds --create-namespace --namespace kamaji-system
|
||||||
$(HELM) repo add clastix https://clastix.github.io/charts
|
$(HELM) repo add clastix https://clastix.github.io/charts
|
||||||
$(HELM) dependency build ./charts/kamaji
|
$(HELM) dependency build ./charts/kamaji
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ type KonnectivityStatus struct {
|
|||||||
ClusterRoleBinding ExternalKubernetesObjectStatus `json:"clusterrolebinding,omitempty"`
|
ClusterRoleBinding ExternalKubernetesObjectStatus `json:"clusterrolebinding,omitempty"`
|
||||||
Agent KonnectivityAgentStatus `json:"agent,omitempty"`
|
Agent KonnectivityAgentStatus `json:"agent,omitempty"`
|
||||||
Service KubernetesServiceStatus `json:"service,omitempty"`
|
Service KubernetesServiceStatus `json:"service,omitempty"`
|
||||||
|
Gateway *KubernetesGatewayStatus `json:"gateway,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KonnectivityConfigMap struct {
|
type KonnectivityConfigMap struct {
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ type IngressSpec struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GatewaySpec defines the options for the Gateway which will expose API Server of the Tenant Control Plane.
|
// GatewaySpec defines the options for the Gateway which will expose API Server of the Tenant Control Plane.
|
||||||
|
// +kubebuilder:validation:XValidation:rule="!has(self.parentRefs) || size(self.parentRefs) == 0 || self.parentRefs.all(ref, !has(ref.port) && !has(ref.sectionName))",message="parentRefs must not specify port or sectionName, these are set automatically by Kamaji"
|
||||||
type GatewaySpec struct {
|
type GatewaySpec struct {
|
||||||
// AdditionalMetadata to add Labels and Annotations support.
|
// AdditionalMetadata to add Labels and Annotations support.
|
||||||
AdditionalMetadata AdditionalMetadata `json:"additionalMetadata,omitempty"`
|
AdditionalMetadata AdditionalMetadata `json:"additionalMetadata,omitempty"`
|
||||||
|
|||||||
@@ -1058,6 +1058,11 @@ func (in *KonnectivityStatus) DeepCopyInto(out *KonnectivityStatus) {
|
|||||||
in.ClusterRoleBinding.DeepCopyInto(&out.ClusterRoleBinding)
|
in.ClusterRoleBinding.DeepCopyInto(&out.ClusterRoleBinding)
|
||||||
in.Agent.DeepCopyInto(&out.Agent)
|
in.Agent.DeepCopyInto(&out.Agent)
|
||||||
in.Service.DeepCopyInto(&out.Service)
|
in.Service.DeepCopyInto(&out.Service)
|
||||||
|
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 KonnectivityStatus.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KonnectivityStatus.
|
||||||
|
|||||||
@@ -6896,6 +6896,9 @@ versions:
|
|||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
type: object
|
type: object
|
||||||
|
x-kubernetes-validations:
|
||||||
|
- message: parentRefs must not specify port or sectionName, these are set automatically by Kamaji
|
||||||
|
rule: '!has(self.parentRefs) || size(self.parentRefs) == 0 || self.parentRefs.all(ref, !has(ref.port) && !has(ref.sectionName))'
|
||||||
ingress:
|
ingress:
|
||||||
description: Defining the options for an Optional Ingress which will expose API Server of the Tenant Control Plane
|
description: Defining the options for an Optional Ingress which will expose API Server of the Tenant Control Plane
|
||||||
properties:
|
properties:
|
||||||
@@ -7349,6 +7352,383 @@ versions:
|
|||||||
type: object
|
type: object
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
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
|
||||||
kubeconfig:
|
kubeconfig:
|
||||||
description: KubeconfigStatus contains information about the generated kubeconfig.
|
description: KubeconfigStatus contains information about the generated kubeconfig.
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -6904,6 +6904,9 @@ spec:
|
|||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
type: object
|
type: object
|
||||||
|
x-kubernetes-validations:
|
||||||
|
- message: parentRefs must not specify port or sectionName, these are set automatically by Kamaji
|
||||||
|
rule: '!has(self.parentRefs) || size(self.parentRefs) == 0 || self.parentRefs.all(ref, !has(ref.port) && !has(ref.sectionName))'
|
||||||
ingress:
|
ingress:
|
||||||
description: Defining the options for an Optional Ingress which will expose API Server of the Tenant Control Plane
|
description: Defining the options for an Optional Ingress which will expose API Server of the Tenant Control Plane
|
||||||
properties:
|
properties:
|
||||||
@@ -7357,6 +7360,383 @@ spec:
|
|||||||
type: object
|
type: object
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
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
|
||||||
kubeconfig:
|
kubeconfig:
|
||||||
description: KubeconfigStatus contains information about the generated kubeconfig.
|
description: KubeconfigStatus contains information about the generated kubeconfig.
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Copyright 2022 Clastix Labs
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
# This example demonstrates how to configure Gateway API support for a Tenant Control Plane.
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# 1. Gateway API CRDs must be installed (GatewayClass, Gateway, TLSRoute)
|
||||||
|
# 2. A Gateway resource must exist with listeners for ports 6443 and 8132
|
||||||
|
# 3. DNS(or worker nodes hosts entries) must be configured to resolve the hostname to the Gateway's external address
|
||||||
|
#
|
||||||
|
# Example GatewayClass and Gateway configuration:
|
||||||
|
#
|
||||||
|
# apiVersion: gateway.networking.k8s.io/v1
|
||||||
|
# kind: GatewayClass
|
||||||
|
# metadata:
|
||||||
|
# name: envoy-gw-class
|
||||||
|
# spec:
|
||||||
|
# controllerName: gateway.envoyproxy.io/gatewayclass-controller
|
||||||
|
# ---
|
||||||
|
# apiVersion: gateway.networking.k8s.io/v1
|
||||||
|
# kind: Gateway
|
||||||
|
# metadata:
|
||||||
|
# name: gateway
|
||||||
|
# namespace: default
|
||||||
|
# spec:
|
||||||
|
# gatewayClassName: envoy-gw-class
|
||||||
|
# listeners:
|
||||||
|
# - allowedRoutes:
|
||||||
|
# kinds:
|
||||||
|
# - group: gateway.networking.k8s.io
|
||||||
|
# kind: TLSRoute
|
||||||
|
# namespaces:
|
||||||
|
# from: All
|
||||||
|
# hostname: '*.cluster.dev'
|
||||||
|
# name: kube-apiserver
|
||||||
|
# port: 6443
|
||||||
|
# protocol: TLS
|
||||||
|
# tls:
|
||||||
|
# mode: Passthrough
|
||||||
|
# - allowedRoutes:
|
||||||
|
# kinds:
|
||||||
|
# - group: gateway.networking.k8s.io
|
||||||
|
# kind: TLSRoute
|
||||||
|
# namespaces:
|
||||||
|
# from: All
|
||||||
|
# hostname: '*.cluster.dev'
|
||||||
|
# name: konnectivity-server
|
||||||
|
# port: 8132
|
||||||
|
# protocol: TLS
|
||||||
|
# tls:
|
||||||
|
# mode: Passthrough
|
||||||
|
|
||||||
|
apiVersion: kamaji.clastix.io/v1alpha1
|
||||||
|
kind: TenantControlPlane
|
||||||
|
metadata:
|
||||||
|
name: demo-tcp-1
|
||||||
|
spec:
|
||||||
|
addons:
|
||||||
|
coreDNS: {}
|
||||||
|
kubeProxy: {}
|
||||||
|
konnectivity: {}
|
||||||
|
dataStore: default
|
||||||
|
controlPlane:
|
||||||
|
gateway:
|
||||||
|
hostname: "c11.cluster.dev" # worker nodes or kubectl clients must be able to resolve this hostname to the Gateway's external address.
|
||||||
|
parentRefs:
|
||||||
|
- name: gateway
|
||||||
|
namespace: default
|
||||||
|
deployment:
|
||||||
|
replicas: 1
|
||||||
|
service:
|
||||||
|
serviceType: ClusterIP
|
||||||
|
kubernetes:
|
||||||
|
version: v1.32.0
|
||||||
|
kubelet:
|
||||||
|
cgroupfs: systemd
|
||||||
|
networkProfile:
|
||||||
|
port: 6443
|
||||||
|
certSANs:
|
||||||
|
- "c11.cluster.dev"
|
||||||
|
|
||||||
@@ -74,6 +74,7 @@ func GetResources(ctx context.Context, config GroupResourceBuilderConfiguration)
|
|||||||
// Conditionally add Gateway resources
|
// Conditionally add Gateway resources
|
||||||
if utilities.AreGatewayResourcesAvailable(ctx, config.client, config.DiscoveryClient) {
|
if utilities.AreGatewayResourcesAvailable(ctx, config.client, config.DiscoveryClient) {
|
||||||
resources = append(resources, getKubernetesGatewayResources(config.client)...)
|
resources = append(resources, getKubernetesGatewayResources(config.client)...)
|
||||||
|
resources = append(resources, getKonnectivityGatewayResources(config.client)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return resources
|
return resources
|
||||||
@@ -146,6 +147,14 @@ func getKubernetesGatewayResources(c client.Client) []resources.Resource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getKonnectivityGatewayResources(c client.Client) []resources.Resource {
|
||||||
|
return []resources.Resource{
|
||||||
|
&konnectivity.KubernetesKonnectivityGatewayResource{
|
||||||
|
Client: c,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getKubeadmConfigResources(c client.Client, tmpDirectory string, dataStore kamajiv1alpha1.DataStore) []resources.Resource {
|
func getKubeadmConfigResources(c client.Client, tmpDirectory string, dataStore kamajiv1alpha1.DataStore) []resources.Resource {
|
||||||
var endpoints []string
|
var endpoints []string
|
||||||
|
|
||||||
|
|||||||
213
docs/content/guides/gateway-api.md
Normal file
213
docs/content/guides/gateway-api.md
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
# Gateway API Support
|
||||||
|
|
||||||
|
Kamaji provides built-in support for the [Gateway API](https://gateway-api.sigs.k8s.io/), allowing you to expose Tenant Control Planes using TLSRoute resources with SNI-based routing. This enables hostname-based routing to multiple Tenant Control Planes through a single Gateway resource, reducing the need for dedicated LoadBalancer services.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Gateway API support in Kamaji automatically creates and manages TLSRoute resources for your Tenant Control Planes. When you configure a Gateway for a Tenant Control Plane, Kamaji automatically creates TLSRoutes for the Control Plane API Server. If konnectivity is enabled, a separate TLSRoute is created for it. Both TLSRoutes use the same hostname and Gateway resource, but route to different ports(listeners) using port-based routing and semantic `sectionName` values.
|
||||||
|
|
||||||
|
Therefore, the target `Gateway` resource must have right listener configurations (see the Gateway [example section](#gateway-resource-setup) below).
|
||||||
|
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
When you configure `spec.controlPlane.gateway` in a TenantControlPlane resource, Kamaji automatically:
|
||||||
|
|
||||||
|
1. **Creates a TLSRoute for the control plane** that routes for port 6443 (or `spec.networkProfile.port`) with sectionName `"kube-apiserver"`
|
||||||
|
2. **Creates a TLSRoute for Konnectivity** (if konnectivity addon is enabled) that routes for port 8132 (or `spec.addons.konnectivity.server.port`) with sectionName `"konnectivity-server"`
|
||||||
|
|
||||||
|
Both TLSRoutes:
|
||||||
|
|
||||||
|
- Use the same hostname from `spec.controlPlane.gateway.hostname`
|
||||||
|
- Reference the same parent Gateway resource via `parentRefs`
|
||||||
|
- The `port` and `sectionName` fields are set automatically by Kamaji
|
||||||
|
- Route to the appropriate Tenant Control Plane service
|
||||||
|
|
||||||
|
The Gateway resource must have listeners configured for both ports (6443 and 8132) to support both routes.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before using Gateway API support, ensure:
|
||||||
|
|
||||||
|
1. **Gateway API CRDs are installed** in your cluster (Required CRDs: `GatewayClass`, `Gateway`, `TLSRoute`)
|
||||||
|
|
||||||
|
2. **A Gateway resource exists** with appropriate listeners configured:
|
||||||
|
- At minimum, listeners for ports 6443 (control plane) and 8132 (Konnectivity)
|
||||||
|
- TLS protocol with Passthrough mode
|
||||||
|
- Hostname pattern matching your Tenant Control Plane hostnames
|
||||||
|
|
||||||
|
3. **DNS is configured** to resolve your hostnames to the Gateway's external address
|
||||||
|
|
||||||
|
4. **Gateway controller is running** (e.g., Envoy Gateway, Istio Gateway, etc.)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### TenantControlPlane Gateway Configuration
|
||||||
|
|
||||||
|
Enable Gateway API mode by setting the `spec.controlPlane.gateway` field in your TenantControlPlane resource:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: kamaji.clastix.io/v1alpha1
|
||||||
|
kind: TenantControlPlane
|
||||||
|
metadata:
|
||||||
|
name: tcp-1
|
||||||
|
spec:
|
||||||
|
controlPlane:
|
||||||
|
# ... gateway configuration:
|
||||||
|
gateway:
|
||||||
|
hostname: "tcp1.cluster.dev"
|
||||||
|
parentRefs:
|
||||||
|
- name: gateway
|
||||||
|
namespace: default
|
||||||
|
additionalMetadata:
|
||||||
|
labels:
|
||||||
|
environment: production
|
||||||
|
annotations:
|
||||||
|
example.com/custom: "value"
|
||||||
|
# ... rest of the spec
|
||||||
|
deployment:
|
||||||
|
replicas: 1
|
||||||
|
service:
|
||||||
|
serviceType: ClusterIP
|
||||||
|
dataStore: default
|
||||||
|
kubernetes:
|
||||||
|
version: v1.29.0
|
||||||
|
kubelet:
|
||||||
|
cgroupfs: systemd
|
||||||
|
networkProfile:
|
||||||
|
port: 6443
|
||||||
|
certSANs:
|
||||||
|
- "c11.cluster.dev" # make sure to set this.
|
||||||
|
addons:
|
||||||
|
coreDNS: {}
|
||||||
|
kubeProxy: {}
|
||||||
|
konnectivity: {}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required fields:**
|
||||||
|
|
||||||
|
- `hostname`: The hostname that will be used for routing (must match Gateway listener hostname pattern)
|
||||||
|
- `parentRefs`: Array of Gateway references (name and namespace)
|
||||||
|
|
||||||
|
**Optional fields:**
|
||||||
|
|
||||||
|
- `additionalMetadata.labels`: Custom labels to add to TLSRoute resources
|
||||||
|
- `additionalMetadata.annotations`: Custom annotations to add to TLSRoute resources
|
||||||
|
|
||||||
|
!!! warning "Port and sectionName are set automatically"
|
||||||
|
Do not specify `port` or `sectionName` in `parentRefs`. Kamaji automatically sets these fields in TLSRoutes.
|
||||||
|
|
||||||
|
### Gateway Resource Setup
|
||||||
|
|
||||||
|
Your Gateway resource must have listeners configured for both the control plane and Konnectivity ports. Here's an example Gateway configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: gateway.networking.k8s.io/v1
|
||||||
|
kind: GatewayClass
|
||||||
|
metadata:
|
||||||
|
name: envoy-gw-class
|
||||||
|
spec:
|
||||||
|
controllerName: gateway.envoyproxy.io/gatewayclass-controller
|
||||||
|
---
|
||||||
|
apiVersion: gateway.networking.k8s.io/v1
|
||||||
|
kind: Gateway
|
||||||
|
metadata:
|
||||||
|
name: gateway
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
gatewayClassName: envoy-gw-class
|
||||||
|
listeners:
|
||||||
|
- name: kube-apiserver
|
||||||
|
port: 6443
|
||||||
|
protocol: TLS
|
||||||
|
hostname: 'tcp1.cluster.dev'
|
||||||
|
tls:
|
||||||
|
mode: Passthrough
|
||||||
|
allowedRoutes:
|
||||||
|
kinds:
|
||||||
|
- group: gateway.networking.k8s.io
|
||||||
|
kind: TLSRoute
|
||||||
|
namespaces:
|
||||||
|
from: All
|
||||||
|
|
||||||
|
# if konnectivity addon is enabled:
|
||||||
|
- name: konnectivity-server
|
||||||
|
port: 8132
|
||||||
|
protocol: TLS
|
||||||
|
hostname: 'tcp1.cluster.dev'
|
||||||
|
tls:
|
||||||
|
mode: Passthrough
|
||||||
|
allowedRoutes:
|
||||||
|
kinds:
|
||||||
|
- group: gateway.networking.k8s.io
|
||||||
|
kind: TLSRoute
|
||||||
|
namespaces:
|
||||||
|
from: All
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Multiple Tenant Control Planes
|
||||||
|
|
||||||
|
You can use the same Gateway resource for multiple Tenant Control Planes by using different hostnames:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Gateway with wildcard hostname
|
||||||
|
apiVersion: gateway.networking.k8s.io/v1
|
||||||
|
kind: Gateway
|
||||||
|
metadata:
|
||||||
|
name: gateway
|
||||||
|
spec:
|
||||||
|
listeners:
|
||||||
|
- hostname: '*.cluster.dev'
|
||||||
|
name: kube-apiserver
|
||||||
|
port: 6443
|
||||||
|
# ...
|
||||||
|
---
|
||||||
|
# Tenant Control Plane 1
|
||||||
|
apiVersion: kamaji.clastix.io/v1alpha1
|
||||||
|
kind: TenantControlPlane
|
||||||
|
metadata:
|
||||||
|
name: tcp-1
|
||||||
|
spec:
|
||||||
|
controlPlane:
|
||||||
|
gateway:
|
||||||
|
hostname: "tcp1.cluster.dev"
|
||||||
|
parentRefs:
|
||||||
|
- name: gateway
|
||||||
|
namespace: default
|
||||||
|
# ...
|
||||||
|
---
|
||||||
|
# Tenant Control Plane 2
|
||||||
|
apiVersion: kamaji.clastix.io/v1alpha1
|
||||||
|
kind: TenantControlPlane
|
||||||
|
metadata:
|
||||||
|
name: tcp-2
|
||||||
|
spec:
|
||||||
|
controlPlane:
|
||||||
|
gateway:
|
||||||
|
hostname: "tcp2.cluster.dev"
|
||||||
|
parentRefs:
|
||||||
|
- name: gateway
|
||||||
|
namespace: default
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Each Tenant Control Plane will get its own TLSRoutes with the respective hostnames, all routing through the same Gateway resource.
|
||||||
|
|
||||||
|
You can check the Gateway status in the TenantControlPlane:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get tenantcontrolplane tcp-1 -o yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for the `status.kubernetesResources.gateway` and `status.addons.konnectivity.gateway` fields.
|
||||||
|
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [Gateway API Documentation](https://gateway-api.sigs.k8s.io/)
|
||||||
|
- [Quickstart with Envoy Gateway](https://gateway.envoyproxy.io/docs/tasks/quickstart/)
|
||||||
|
|
||||||
|
|
||||||
@@ -42688,6 +42688,13 @@ KonnectivityStatus defines the status of Konnectivity as Addon.
|
|||||||
<br/>
|
<br/>
|
||||||
</td>
|
</td>
|
||||||
<td>false</td>
|
<td>false</td>
|
||||||
|
</tr><tr>
|
||||||
|
<td><b><a href="#tenantcontrolplanestatusaddonskonnectivitygateway">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>
|
</tr><tr>
|
||||||
<td><b><a href="#tenantcontrolplanestatusaddonskonnectivitykubeconfig">kubeconfig</a></b></td>
|
<td><b><a href="#tenantcontrolplanestatusaddonskonnectivitykubeconfig">kubeconfig</a></b></td>
|
||||||
<td>object</td>
|
<td>object</td>
|
||||||
@@ -42875,6 +42882,505 @@ CertificatePrivateKeyPairStatus defines the status.
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
<span id="tenantcontrolplanestatusaddonskonnectivitygateway">`TenantControlPlane.status.addons.konnectivity.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="#tenantcontrolplanestatusaddonskonnectivitygatewayparentsindex">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="#tenantcontrolplanestatusaddonskonnectivitygatewayaccesspointsindex">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="#tenantcontrolplanestatusaddonskonnectivitygatewayrouteref">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="tenantcontrolplanestatusaddonskonnectivitygatewayparentsindex">`TenantControlPlane.status.addons.konnectivity.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="#tenantcontrolplanestatusaddonskonnectivitygatewayparentsindexconditionsindex">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="#tenantcontrolplanestatusaddonskonnectivitygatewayparentsindexparentref">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="tenantcontrolplanestatusaddonskonnectivitygatewayparentsindexconditionsindex">`TenantControlPlane.status.addons.konnectivity.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="tenantcontrolplanestatusaddonskonnectivitygatewayparentsindexparentref">`TenantControlPlane.status.addons.konnectivity.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="tenantcontrolplanestatusaddonskonnectivitygatewayaccesspointsindex">`TenantControlPlane.status.addons.konnectivity.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="tenantcontrolplanestatusaddonskonnectivitygatewayrouteref">`TenantControlPlane.status.addons.konnectivity.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="tenantcontrolplanestatusaddonskonnectivitykubeconfig">`TenantControlPlane.status.addons.konnectivity.kubeconfig`</span>
|
<span id="tenantcontrolplanestatusaddonskonnectivitykubeconfig">`TenantControlPlane.status.addons.konnectivity.kubeconfig`</span>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ nav:
|
|||||||
- guides/gitops.md
|
- guides/gitops.md
|
||||||
- guides/console.md
|
- guides/console.md
|
||||||
- guides/kubeconfig-generator.md
|
- guides/kubeconfig-generator.md
|
||||||
|
- guides/gateway-api.md
|
||||||
- guides/upgrade.md
|
- guides/upgrade.md
|
||||||
- guides/monitoring.md
|
- guides/monitoring.md
|
||||||
- guides/terraform.md
|
- guides/terraform.md
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
package e2e
|
package e2e
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/client-go/kubernetes/scheme"
|
"k8s.io/client-go/kubernetes/scheme"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
pointer "k8s.io/utils/ptr"
|
pointer "k8s.io/utils/ptr"
|
||||||
@@ -68,9 +70,39 @@ var _ = BeforeSuite(func() {
|
|||||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(k8sClient).NotTo(BeNil())
|
Expect(k8sClient).NotTo(BeNil())
|
||||||
|
|
||||||
|
By("creating GatewayClass for Gateway API tests")
|
||||||
|
gatewayClass := &gatewayv1.GatewayClass{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "envoy-gw-class",
|
||||||
|
},
|
||||||
|
Spec: gatewayv1.GatewayClassSpec{
|
||||||
|
ControllerName: "gateway.envoyproxy.io/gatewayclass-controller",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Expect(k8sClient.Create(context.Background(), gatewayClass)).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("creating Gateway with kube-apiserver and konnectivity-server listeners")
|
||||||
|
CreateGatewayWithListeners("test-gateway", "default", "envoy-gw-class", "*.example.com")
|
||||||
})
|
})
|
||||||
|
|
||||||
var _ = AfterSuite(func() {
|
var _ = AfterSuite(func() {
|
||||||
|
By("deleting Gateway resources")
|
||||||
|
gateway := &gatewayv1.Gateway{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-gateway",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_ = k8sClient.Delete(context.Background(), gateway)
|
||||||
|
|
||||||
|
gatewayClass := &gatewayv1.GatewayClass{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "envoy-gw-class",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_ = k8sClient.Delete(context.Background(), gatewayClass)
|
||||||
|
|
||||||
By("tearing down the test environment")
|
By("tearing down the test environment")
|
||||||
err := testEnv.Stop()
|
err := testEnv.Stop()
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|||||||
151
e2e/tcp_gateway_konnectivity_ready_test.go
Normal file
151
e2e/tcp_gateway_konnectivity_ready_test.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
// Copyright 2022 Clastix Labs
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package e2e
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
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 and Konnectivity", func() {
|
||||||
|
var tcp *kamajiv1alpha1.TenantControlPlane
|
||||||
|
|
||||||
|
JustBeforeEach(func() {
|
||||||
|
tcp = &kamajiv1alpha1.TenantControlPlane{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "tcp-konnectivity-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-konnectivity.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.4",
|
||||||
|
},
|
||||||
|
Kubernetes: kamajiv1alpha1.KubernetesSpec{
|
||||||
|
Version: "v1.28.0",
|
||||||
|
Kubelet: kamajiv1alpha1.KubeletSpec{
|
||||||
|
CGroupFS: "cgroupfs",
|
||||||
|
},
|
||||||
|
AdmissionControllers: kamajiv1alpha1.AdmissionControllers{
|
||||||
|
"LimitRanger",
|
||||||
|
"ResourceQuota",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Addons: kamajiv1alpha1.AddonsSpec{
|
||||||
|
Konnectivity: &kamajiv1alpha1.KonnectivitySpec{
|
||||||
|
KonnectivityServerSpec: kamajiv1alpha1.KonnectivityServerSpec{
|
||||||
|
Port: 8132,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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 Konnectivity TLSRoute with correct sectionName", func() {
|
||||||
|
Eventually(func() error {
|
||||||
|
route := &gatewayv1alpha2.TLSRoute{}
|
||||||
|
if err := k8sClient.Get(context.Background(), types.NamespacedName{
|
||||||
|
Name: tcp.Name + "-konnectivity",
|
||||||
|
Namespace: tcp.Namespace,
|
||||||
|
}, route); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(route.Spec.ParentRefs) == 0 {
|
||||||
|
return fmt.Errorf("parentRefs is empty")
|
||||||
|
}
|
||||||
|
if route.Spec.ParentRefs[0].SectionName == nil {
|
||||||
|
return fmt.Errorf("sectionName is nil")
|
||||||
|
}
|
||||||
|
if *route.Spec.ParentRefs[0].SectionName != gatewayv1.SectionName("konnectivity-server") {
|
||||||
|
return fmt.Errorf("expected sectionName 'konnectivity-server', got '%s'", *route.Spec.ParentRefs[0].SectionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}).WithTimeout(time.Minute).Should(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("Should use same hostname for both TLSRoutes", func() {
|
||||||
|
Eventually(func() error {
|
||||||
|
controlPlaneRoute := &gatewayv1alpha2.TLSRoute{}
|
||||||
|
if err := k8sClient.Get(context.Background(), types.NamespacedName{
|
||||||
|
Name: tcp.Name,
|
||||||
|
Namespace: tcp.Namespace,
|
||||||
|
}, controlPlaneRoute); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
konnectivityRoute := &gatewayv1alpha2.TLSRoute{}
|
||||||
|
if err := k8sClient.Get(context.Background(), types.NamespacedName{
|
||||||
|
Name: tcp.Name + "-konnectivity",
|
||||||
|
Namespace: tcp.Namespace,
|
||||||
|
}, konnectivityRoute); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(controlPlaneRoute.Spec.Hostnames) == 0 || len(konnectivityRoute.Spec.Hostnames) == 0 {
|
||||||
|
return fmt.Errorf("hostnames are empty")
|
||||||
|
}
|
||||||
|
if controlPlaneRoute.Spec.Hostnames[0] != konnectivityRoute.Spec.Hostnames[0] {
|
||||||
|
return fmt.Errorf("hostnames do not match: control plane '%s', konnectivity '%s'",
|
||||||
|
controlPlaneRoute.Spec.Hostnames[0], konnectivityRoute.Spec.Hostnames[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}).WithTimeout(time.Minute).Should(Succeed())
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -5,6 +5,7 @@ package e2e
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
@@ -89,14 +90,39 @@ var _ = Describe("Deploy a TenantControlPlane with Gateway API", func() {
|
|||||||
StatusMustEqualTo(tcp, kamajiv1alpha1.VersionReady)
|
StatusMustEqualTo(tcp, kamajiv1alpha1.VersionReady)
|
||||||
})
|
})
|
||||||
|
|
||||||
It("Should create TLSRoute resource", func() {
|
It("Should create control plane TLSRoute with correct sectionName", func() {
|
||||||
Eventually(func() error {
|
Eventually(func() error {
|
||||||
route := &gatewayv1alpha2.TLSRoute{}
|
route := &gatewayv1alpha2.TLSRoute{}
|
||||||
// TODO: Check ownership.
|
// TODO: Check ownership.
|
||||||
return k8sClient.Get(context.Background(), types.NamespacedName{
|
if err := k8sClient.Get(context.Background(), types.NamespacedName{
|
||||||
Name: tcp.Name,
|
Name: tcp.Name,
|
||||||
Namespace: tcp.Namespace,
|
Namespace: tcp.Namespace,
|
||||||
}, route)
|
}, route); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(route.Spec.ParentRefs) == 0 {
|
||||||
|
return fmt.Errorf("parentRefs is empty")
|
||||||
|
}
|
||||||
|
if route.Spec.ParentRefs[0].SectionName == nil {
|
||||||
|
return fmt.Errorf("sectionName is nil")
|
||||||
|
}
|
||||||
|
if *route.Spec.ParentRefs[0].SectionName != gatewayv1.SectionName("kube-apiserver") {
|
||||||
|
return fmt.Errorf("expected sectionName 'kube-apiserver', got '%s'", *route.Spec.ParentRefs[0].SectionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}).WithTimeout(time.Minute).Should(Succeed())
|
}).WithTimeout(time.Minute).Should(Succeed())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("Should not create Konnectivity TLSRoute", func() {
|
||||||
|
// Verify Konnectivity route is not created
|
||||||
|
Consistently(func() error {
|
||||||
|
route := &gatewayv1alpha2.TLSRoute{}
|
||||||
|
|
||||||
|
return k8sClient.Get(context.Background(), types.NamespacedName{
|
||||||
|
Name: tcp.Name + "-konnectivity",
|
||||||
|
Namespace: tcp.Namespace,
|
||||||
|
}, route)
|
||||||
|
}, 10*time.Second, time.Second).Should(HaveOccurred())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/util/retry"
|
"k8s.io/client-go/util/retry"
|
||||||
|
pointer "k8s.io/utils/ptr"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||||
|
|
||||||
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||||
)
|
)
|
||||||
@@ -209,3 +211,60 @@ func ScaleTenantControlPlane(tcp *kamajiv1alpha1.TenantControlPlane, replicas in
|
|||||||
})
|
})
|
||||||
Expect(err).To(Succeed())
|
Expect(err).To(Succeed())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateGatewayWithListeners creates a Gateway with both kube-apiserver and konnectivity-server listeners.
|
||||||
|
func CreateGatewayWithListeners(gatewayName, namespace, gatewayClassName, hostname string) {
|
||||||
|
GinkgoHelper()
|
||||||
|
gateway := &gatewayv1.Gateway{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: gatewayName,
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
Spec: gatewayv1.GatewaySpec{
|
||||||
|
GatewayClassName: gatewayv1.ObjectName(gatewayClassName),
|
||||||
|
Listeners: []gatewayv1.Listener{
|
||||||
|
{
|
||||||
|
Name: "kube-apiserver",
|
||||||
|
Port: 6443,
|
||||||
|
Protocol: gatewayv1.TLSProtocolType,
|
||||||
|
Hostname: pointer.To(gatewayv1.Hostname(hostname)),
|
||||||
|
TLS: &gatewayv1.ListenerTLSConfig{
|
||||||
|
Mode: pointer.To(gatewayv1.TLSModeType("Passthrough")),
|
||||||
|
},
|
||||||
|
AllowedRoutes: &gatewayv1.AllowedRoutes{
|
||||||
|
Namespaces: &gatewayv1.RouteNamespaces{
|
||||||
|
From: pointer.To(gatewayv1.NamespacesFromAll),
|
||||||
|
},
|
||||||
|
Kinds: []gatewayv1.RouteGroupKind{
|
||||||
|
{
|
||||||
|
Group: pointer.To(gatewayv1.Group("gateway.networking.k8s.io")),
|
||||||
|
Kind: "TLSRoute",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "konnectivity-server",
|
||||||
|
Port: 8132,
|
||||||
|
Protocol: gatewayv1.TLSProtocolType,
|
||||||
|
Hostname: pointer.To(gatewayv1.Hostname(hostname)),
|
||||||
|
TLS: &gatewayv1.ListenerTLSConfig{
|
||||||
|
Mode: pointer.To(gatewayv1.TLSModeType("Passthrough")),
|
||||||
|
},
|
||||||
|
AllowedRoutes: &gatewayv1.AllowedRoutes{
|
||||||
|
Namespaces: &gatewayv1.RouteNamespaces{
|
||||||
|
From: pointer.To(gatewayv1.NamespacesFromAll),
|
||||||
|
},
|
||||||
|
Kinds: []gatewayv1.RouteGroupKind{
|
||||||
|
{
|
||||||
|
Group: pointer.To(gatewayv1.Group("gateway.networking.k8s.io")),
|
||||||
|
Kind: "TLSRoute",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Expect(k8sClient.Create(context.Background(), gateway)).NotTo(HaveOccurred())
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,14 +6,10 @@ package resources
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
v1 "k8s.io/api/core/v1"
|
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"
|
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/client"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
@@ -56,66 +52,12 @@ func (r *KubernetesGatewayResource) gatewayStatusNeedsUpdate(tcp *kamajiv1alpha1
|
|||||||
currentStatus := tcp.Status.Kubernetes.Gateway
|
currentStatus := tcp.Status.Kubernetes.Gateway
|
||||||
|
|
||||||
// Check if route reference has changed
|
// Check if route reference has changed
|
||||||
if currentStatus.RouteRef.Name != r.resource.Name {
|
if currentStatus != nil && currentStatus.RouteRef.Name != r.resource.Name {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare RouteStatus - check if number of parents changed
|
// Compare RouteStatus - check if number of parents changed
|
||||||
if len(currentStatus.RouteStatus.Parents) != len(r.resource.Status.RouteStatus.Parents) {
|
return IsGatewayRouteStatusChanged(currentStatus, r.resource.Status.RouteStatus)
|
||||||
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 {
|
func (r *KubernetesGatewayResource) ShouldCleanup(tcp *kamajiv1alpha1.TenantControlPlane) bool {
|
||||||
@@ -125,95 +67,18 @@ func (r *KubernetesGatewayResource) ShouldCleanup(tcp *kamajiv1alpha1.TenantCont
|
|||||||
func (r *KubernetesGatewayResource) CleanUp(ctx context.Context, tcp *kamajiv1alpha1.TenantControlPlane) (bool, error) {
|
func (r *KubernetesGatewayResource) CleanUp(ctx context.Context, tcp *kamajiv1alpha1.TenantControlPlane) (bool, error) {
|
||||||
logger := log.FromContext(ctx, "resource", r.GetName())
|
logger := log.FromContext(ctx, "resource", r.GetName())
|
||||||
|
|
||||||
route := gatewayv1alpha2.TLSRoute{}
|
cleaned, err := CleanupTLSRoute(ctx, r.Client, r.resource.GetName(), r.resource.GetNamespace(), tcp)
|
||||||
if err := r.Client.Get(ctx, client.ObjectKey{
|
if err != nil {
|
||||||
Namespace: r.resource.GetNamespace(),
|
logger.Error(err, "failed to cleanup tcp route")
|
||||||
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, err
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !metav1.IsControlledBy(&route, tcp) {
|
if cleaned {
|
||||||
logger.Info("skipping cleanup: route is not managed by Kamaji", "name", route.Name, "namespace", route.Namespace)
|
logger.V(1).Info("tcp route cleaned up successfully")
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.Client.Delete(ctx, &route); err != nil {
|
return cleaned, 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 {
|
func (r *KubernetesGatewayResource) UpdateTenantControlPlaneStatus(ctx context.Context, tcp *kamajiv1alpha1.TenantControlPlane) error {
|
||||||
@@ -251,53 +116,9 @@ func (r *KubernetesGatewayResource) UpdateTenantControlPlaneStatus(ctx context.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.V(1).Info("updating TenantControlPlane status for Gateway routes")
|
logger.V(1).Info("updating TenantControlPlane status for Gateway routes")
|
||||||
accessPoints := []kamajiv1alpha1.GatewayAccessPoint{}
|
accessPoints, err := BuildGatewayAccessPointsStatus(ctx, r.Client, r.resource, routeStatuses)
|
||||||
for _, routeStatus := range routeStatuses.Parents {
|
if err != nil {
|
||||||
routeAccepted := meta.IsStatusConditionTrue(
|
return err
|
||||||
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
|
tcp.Status.Kubernetes.Gateway.AccessPoints = accessPoints
|
||||||
|
|
||||||
@@ -329,10 +150,6 @@ func (r *KubernetesGatewayResource) mutate(tcp *kamajiv1alpha1.TenantControlPlan
|
|||||||
tcp.Spec.ControlPlane.Gateway.AdditionalMetadata.Annotations)
|
tcp.Spec.ControlPlane.Gateway.AdditionalMetadata.Annotations)
|
||||||
r.resource.SetAnnotations(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)
|
serviceName := gatewayv1alpha2.ObjectName(tcp.Status.Kubernetes.Service.Name)
|
||||||
servicePort := tcp.Status.Kubernetes.Service.Port
|
servicePort := tcp.Status.Kubernetes.Service.Port
|
||||||
|
|
||||||
@@ -340,6 +157,11 @@ func (r *KubernetesGatewayResource) mutate(tcp *kamajiv1alpha1.TenantControlPlan
|
|||||||
return fmt.Errorf("service not ready, cannot create TLSRoute")
|
return fmt.Errorf("service not ready, cannot create TLSRoute")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tcp.Spec.ControlPlane.Gateway.GatewayParentRefs != nil {
|
||||||
|
// Copy parentRefs and explicitly set port and sectionName fields
|
||||||
|
r.resource.Spec.ParentRefs = NewParentRefsSpecWithPortAndSection(tcp.Spec.ControlPlane.Gateway.GatewayParentRefs, servicePort, "kube-apiserver")
|
||||||
|
}
|
||||||
|
|
||||||
rule := gatewayv1alpha2.TLSRouteRule{
|
rule := gatewayv1alpha2.TLSRouteRule{
|
||||||
BackendRefs: []gatewayv1alpha2.BackendRef{
|
BackendRefs: []gatewayv1alpha2.BackendRef{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/client-go/kubernetes/scheme"
|
"k8s.io/client-go/kubernetes/scheme"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||||
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||||
@@ -100,6 +101,62 @@ var _ = Describe("KubernetesGatewayResource", func() {
|
|||||||
shouldUpdate := resource.ShouldStatusBeUpdated(ctx, tcp)
|
shouldUpdate := resource.ShouldStatusBeUpdated(ctx, tcp)
|
||||||
Expect(shouldUpdate).To(BeTrue())
|
Expect(shouldUpdate).To(BeTrue())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("should set port and sectionName in parentRefs, overriding any user-provided values", func() {
|
||||||
|
customPort := gatewayv1.PortNumber(9999)
|
||||||
|
customSectionName := gatewayv1.SectionName("custom")
|
||||||
|
tcp.Spec.ControlPlane.Gateway.GatewayParentRefs[0].Port = &customPort
|
||||||
|
tcp.Spec.ControlPlane.Gateway.GatewayParentRefs[0].SectionName = &customSectionName
|
||||||
|
|
||||||
|
err := resource.Define(ctx, tcp)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
_, err = resource.CreateOrUpdate(ctx, tcp)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
route := &gatewayv1alpha2.TLSRoute{}
|
||||||
|
err = resource.Client.Get(ctx, client.ObjectKey{Name: tcp.Name, Namespace: tcp.Namespace}, route)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(route.Spec.ParentRefs).To(HaveLen(1))
|
||||||
|
Expect(route.Spec.ParentRefs[0].Port).NotTo(BeNil())
|
||||||
|
Expect(*route.Spec.ParentRefs[0].Port).To(Equal(tcp.Status.Kubernetes.Service.Port))
|
||||||
|
Expect(*route.Spec.ParentRefs[0].Port).NotTo(Equal(customPort))
|
||||||
|
Expect(route.Spec.ParentRefs[0].SectionName).NotTo(BeNil())
|
||||||
|
Expect(*route.Spec.ParentRefs[0].SectionName).To(Equal(gatewayv1.SectionName("kube-apiserver")))
|
||||||
|
Expect(*route.Spec.ParentRefs[0].SectionName).NotTo(Equal(customSectionName))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should handle multiple parentRefs correctly", func() {
|
||||||
|
namespace := gatewayv1.Namespace("default")
|
||||||
|
tcp.Spec.ControlPlane.Gateway.GatewayParentRefs = []gatewayv1alpha2.ParentReference{
|
||||||
|
{
|
||||||
|
Name: "test-gateway-1",
|
||||||
|
Namespace: &namespace,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "test-gateway-2",
|
||||||
|
Namespace: &namespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := resource.Define(ctx, tcp)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
_, err = resource.CreateOrUpdate(ctx, tcp)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
route := &gatewayv1alpha2.TLSRoute{}
|
||||||
|
err = resource.Client.Get(ctx, client.ObjectKey{Name: tcp.Name, Namespace: tcp.Namespace}, route)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(route.Spec.ParentRefs).To(HaveLen(2))
|
||||||
|
|
||||||
|
for i := range route.Spec.ParentRefs {
|
||||||
|
Expect(route.Spec.ParentRefs[i].Port).NotTo(BeNil())
|
||||||
|
Expect(*route.Spec.ParentRefs[i].Port).To(Equal(tcp.Status.Kubernetes.Service.Port))
|
||||||
|
Expect(route.Spec.ParentRefs[i].SectionName).NotTo(BeNil())
|
||||||
|
Expect(*route.Spec.ParentRefs[i].SectionName).To(Equal(gatewayv1.SectionName("kube-apiserver")))
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Context("When GatewayRoutes is not configured", func() {
|
Context("When GatewayRoutes is not configured", func() {
|
||||||
@@ -235,4 +292,81 @@ var _ = Describe("KubernetesGatewayResource", func() {
|
|||||||
Expect(listener.Port).To(Equal(gatewayv1.PortNumber(80)))
|
Expect(listener.Port).To(Equal(gatewayv1.PortNumber(80)))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Describe("NewParentRefsSpecWithPortAndSection", func() {
|
||||||
|
var (
|
||||||
|
parentRefs []gatewayv1.ParentReference
|
||||||
|
testPort int32
|
||||||
|
testSectionName string
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
namespace := gatewayv1.Namespace("default")
|
||||||
|
namespace2 := gatewayv1.Namespace("other")
|
||||||
|
testPort = int32(6443)
|
||||||
|
testSectionName = "kube-apiserver"
|
||||||
|
originalPort := gatewayv1.PortNumber(9999)
|
||||||
|
originalSectionName := gatewayv1.SectionName("original")
|
||||||
|
parentRefs = []gatewayv1.ParentReference{
|
||||||
|
{
|
||||||
|
Name: "test-gateway-1",
|
||||||
|
Namespace: &namespace,
|
||||||
|
Port: &originalPort,
|
||||||
|
SectionName: &originalSectionName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "test-gateway-2",
|
||||||
|
Namespace: &namespace2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should create copy of parentRefs with port and sectionName set", func() {
|
||||||
|
result := resources.NewParentRefsSpecWithPortAndSection(parentRefs, testPort, testSectionName)
|
||||||
|
|
||||||
|
Expect(result).To(HaveLen(2))
|
||||||
|
for i := range result {
|
||||||
|
Expect(result[i].Name).To(Equal(parentRefs[i].Name))
|
||||||
|
Expect(result[i].Namespace).To(Equal(parentRefs[i].Namespace))
|
||||||
|
Expect(result[i].Port).NotTo(BeNil())
|
||||||
|
Expect(*result[i].Port).To(Equal(testPort))
|
||||||
|
Expect(result[i].SectionName).NotTo(BeNil())
|
||||||
|
Expect(*result[i].SectionName).To(Equal(gatewayv1.SectionName(testSectionName)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should not modify original parentRefs", func() {
|
||||||
|
// Store original values for verification
|
||||||
|
originalFirstPort := parentRefs[0].Port
|
||||||
|
originalFirstSectionName := parentRefs[0].SectionName
|
||||||
|
originalSecondPort := parentRefs[1].Port
|
||||||
|
originalSecondSectionName := parentRefs[1].SectionName
|
||||||
|
|
||||||
|
result := resources.NewParentRefsSpecWithPortAndSection(parentRefs, testPort, testSectionName)
|
||||||
|
|
||||||
|
// Original should remain unchanged
|
||||||
|
Expect(parentRefs[0].Port).To(Equal(originalFirstPort))
|
||||||
|
Expect(parentRefs[0].SectionName).To(Equal(originalFirstSectionName))
|
||||||
|
Expect(parentRefs[1].Port).To(Equal(originalSecondPort))
|
||||||
|
Expect(parentRefs[1].SectionName).To(Equal(originalSecondSectionName))
|
||||||
|
|
||||||
|
// Result should have new values
|
||||||
|
Expect(result[0].Port).NotTo(BeNil())
|
||||||
|
Expect(*result[0].Port).To(Equal(testPort))
|
||||||
|
Expect(result[0].SectionName).NotTo(BeNil())
|
||||||
|
Expect(*result[0].SectionName).To(Equal(gatewayv1.SectionName(testSectionName)))
|
||||||
|
Expect(result[1].Port).NotTo(BeNil())
|
||||||
|
Expect(*result[1].Port).To(Equal(testPort))
|
||||||
|
Expect(result[1].SectionName).NotTo(BeNil())
|
||||||
|
Expect(*result[1].SectionName).To(Equal(gatewayv1.SectionName(testSectionName)))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should handle empty parentRefs slice", func() {
|
||||||
|
parentRefs = []gatewayv1.ParentReference{}
|
||||||
|
|
||||||
|
result := resources.NewParentRefsSpecWithPortAndSection(parentRefs, testPort, testSectionName)
|
||||||
|
|
||||||
|
Expect(result).To(BeEmpty())
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
241
internal/resources/k8s_gateway_utils.go
Normal file
241
internal/resources/k8s_gateway_utils.go
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
// Copyright 2022 Clastix Labs
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
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"
|
||||||
|
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||||
|
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||||
|
|
||||||
|
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 fetchGatewayByListener(ctx context.Context, c client.Client, ref gatewayv1.ParentReference) (*gatewayv1.Gateway, error) {
|
||||||
|
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 := c.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
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindMatchingListener finds a listener in the given list that matches the parent reference.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsGatewayRouteStatusChanged checks if the gateway route status has changed compared to the stored status.
|
||||||
|
// Returns true if the status has changed (update needed), false if it's the same.
|
||||||
|
func IsGatewayRouteStatusChanged(currentStatus *kamajiv1alpha1.KubernetesGatewayStatus, resourceStatus gatewayv1alpha2.RouteStatus) bool {
|
||||||
|
if currentStatus == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare RouteStatus - check if number of parents changed
|
||||||
|
if len(currentStatus.RouteStatus.Parents) != len(resourceStatus.Parents) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare individual parent statuses
|
||||||
|
// NOTE: Multiple Parent References are assumed.
|
||||||
|
for i, currentParent := range currentStatus.RouteStatus.Parents {
|
||||||
|
if i >= len(resourceStatus.Parents) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceParent := resourceStatus.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
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupTLSRoute cleans up a TLSRoute resource if it's managed by the given TenantControlPlane.
|
||||||
|
func CleanupTLSRoute(ctx context.Context, c client.Client, routeName, routeNamespace string, tcp metav1.Object) (bool, error) {
|
||||||
|
route := gatewayv1alpha2.TLSRoute{}
|
||||||
|
if err := c.Get(ctx, client.ObjectKey{
|
||||||
|
Namespace: routeNamespace,
|
||||||
|
Name: routeName,
|
||||||
|
}, &route); err != nil {
|
||||||
|
if !k8serrors.IsNotFound(err) {
|
||||||
|
return false, fmt.Errorf("failed to get TLSRoute before cleanup: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !metav1.IsControlledBy(&route, tcp) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Delete(ctx, &route); err != nil {
|
||||||
|
if !k8serrors.IsNotFound(err) {
|
||||||
|
return false, fmt.Errorf("cannot delete TLSRoute route: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildGatewayAccessPointsStatus builds access points from route statuses.
|
||||||
|
func BuildGatewayAccessPointsStatus(ctx context.Context, c client.Client, route *gatewayv1alpha2.TLSRoute, routeStatuses gatewayv1alpha2.RouteStatus) ([]kamajiv1alpha1.GatewayAccessPoint, error) {
|
||||||
|
accessPoints := []kamajiv1alpha1.GatewayAccessPoint{}
|
||||||
|
routeNamespace := gatewayv1.Namespace(route.Namespace)
|
||||||
|
|
||||||
|
for _, routeStatus := range routeStatuses.Parents {
|
||||||
|
routeAccepted := meta.IsStatusConditionTrue(
|
||||||
|
routeStatus.Conditions,
|
||||||
|
string(gatewayv1.RouteConditionAccepted),
|
||||||
|
)
|
||||||
|
if !routeAccepted {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if routeStatus.ParentRef.Namespace == nil {
|
||||||
|
// Set the namespace to the route namespace if not set
|
||||||
|
routeStatus.ParentRef.Namespace = &routeNamespace
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the indexer to efficiently find the gateway with the specific listener
|
||||||
|
gateway, err := fetchGatewayByListener(ctx, c, routeStatus.ParentRef)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 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 nil, fmt.Errorf("failed to match listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, hostname := range route.Spec.Hostnames {
|
||||||
|
rawURL := fmt.Sprintf("https://%s:%d", hostname, listener.Port)
|
||||||
|
parsedURL, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid url: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hostnameAddressType := gatewayv1.HostnameAddressType
|
||||||
|
accessPoints = append(accessPoints, kamajiv1alpha1.GatewayAccessPoint{
|
||||||
|
Type: &hostnameAddressType,
|
||||||
|
Value: parsedURL.String(),
|
||||||
|
Port: listener.Port,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessPoints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewParentRefsSpecWithPortAndSection creates a copy of parentRefs with port and sectionName set for each reference.
|
||||||
|
func NewParentRefsSpecWithPortAndSection(parentRefs []gatewayv1.ParentReference, port int32, sectionName string) []gatewayv1.ParentReference {
|
||||||
|
result := make([]gatewayv1.ParentReference, len(parentRefs))
|
||||||
|
sectionNamePtr := gatewayv1.SectionName(sectionName)
|
||||||
|
for i, parentRef := range parentRefs {
|
||||||
|
result[i] = *parentRef.DeepCopy()
|
||||||
|
result[i].Port = &port
|
||||||
|
result[i].SectionName = §ionNamePtr
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -182,6 +182,21 @@ func (r *Agent) mutate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.T
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override address with control plane gateway hostname if configured
|
||||||
|
// Konnectivity TLSRoute uses the same hostname as control plane gateway
|
||||||
|
if tenantControlPlane.Spec.ControlPlane.Gateway != nil &&
|
||||||
|
len(tenantControlPlane.Spec.ControlPlane.Gateway.Hostname) > 0 {
|
||||||
|
hostname := tenantControlPlane.Spec.ControlPlane.Gateway.Hostname
|
||||||
|
|
||||||
|
// Extract hostname
|
||||||
|
if len(hostname) > 0 {
|
||||||
|
konnectivityHostname, _ := utilities.GetControlPlaneAddressAndPortFromHostname(
|
||||||
|
string(hostname),
|
||||||
|
tenantControlPlane.Spec.Addons.Konnectivity.KonnectivityServerSpec.Port)
|
||||||
|
address = konnectivityHostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
|
r.resource.SetLabels(utilities.MergeMaps(r.resource.GetLabels(), utilities.KamajiLabels(tenantControlPlane.GetName(), r.GetName())))
|
||||||
|
|
||||||
specSelector := &metav1.LabelSelector{
|
specSelector := &metav1.LabelSelector{
|
||||||
|
|||||||
232
internal/resources/konnectivity/gateway_resource.go
Normal file
232
internal/resources/konnectivity/gateway_resource.go
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
// Copyright 2022 Clastix Labs
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package konnectivity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"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/resources"
|
||||||
|
"github.com/clastix/kamaji/internal/utilities"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KubernetesKonnectivityGatewayResource struct {
|
||||||
|
resource *gatewayv1alpha2.TLSRoute
|
||||||
|
Client client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *KubernetesKonnectivityGatewayResource) GetHistogram() prometheus.Histogram {
|
||||||
|
gatewayCollector = resources.LazyLoadHistogramFromResource(gatewayCollector, r)
|
||||||
|
|
||||||
|
return gatewayCollector
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *KubernetesKonnectivityGatewayResource) ShouldStatusBeUpdated(_ context.Context, tcp *kamajiv1alpha1.TenantControlPlane) bool {
|
||||||
|
switch {
|
||||||
|
case !r.shouldHaveGateway(tcp) && (tcp.Status.Addons.Konnectivity.Gateway == nil):
|
||||||
|
return false
|
||||||
|
case r.shouldHaveGateway(tcp) && (tcp.Status.Addons.Konnectivity.Gateway == nil):
|
||||||
|
return true
|
||||||
|
case !r.shouldHaveGateway(tcp) && (tcp.Status.Addons.Konnectivity.Gateway != nil):
|
||||||
|
return true
|
||||||
|
case r.shouldHaveGateway(tcp) && (tcp.Status.Addons.Konnectivity.Gateway != nil):
|
||||||
|
return r.gatewayStatusNeedsUpdate(tcp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldHaveGateway checks if Konnectivity gateway should be configured.
|
||||||
|
// Create when Konnectivity addon is enabled and control plane gateway is configured.
|
||||||
|
func (r *KubernetesKonnectivityGatewayResource) shouldHaveGateway(tcp *kamajiv1alpha1.TenantControlPlane) bool {
|
||||||
|
if tcp.Spec.Addons.Konnectivity == nil { // konnectivity addon is disabled
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Create when control plane gateway is configured
|
||||||
|
return tcp.Spec.ControlPlane.Gateway != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// gatewayStatusNeedsUpdate compares the current gateway resource status with the stored status.
|
||||||
|
func (r *KubernetesKonnectivityGatewayResource) gatewayStatusNeedsUpdate(tcp *kamajiv1alpha1.TenantControlPlane) bool {
|
||||||
|
currentStatus := tcp.Status.Addons.Konnectivity.Gateway
|
||||||
|
|
||||||
|
// Check if route reference has changed
|
||||||
|
if currentStatus != nil && currentStatus.RouteRef.Name != r.resource.Name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare RouteStatus - check if number of parents changed
|
||||||
|
return resources.IsGatewayRouteStatusChanged(currentStatus, r.resource.Status.RouteStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *KubernetesKonnectivityGatewayResource) ShouldCleanup(tcp *kamajiv1alpha1.TenantControlPlane) bool {
|
||||||
|
return !r.shouldHaveGateway(tcp) && tcp.Status.Addons.Konnectivity.Gateway != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *KubernetesKonnectivityGatewayResource) CleanUp(ctx context.Context, tcp *kamajiv1alpha1.TenantControlPlane) (bool, error) {
|
||||||
|
logger := log.FromContext(ctx, "resource", r.GetName())
|
||||||
|
|
||||||
|
cleaned, err := resources.CleanupTLSRoute(ctx, r.Client, r.resource.GetName(), r.resource.GetNamespace(), tcp)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err, "failed to cleanup konnectivity route")
|
||||||
|
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cleaned {
|
||||||
|
logger.V(1).Info("konnectivity route cleaned up successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleaned, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *KubernetesKonnectivityGatewayResource) 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 !r.shouldHaveGateway(tcp) {
|
||||||
|
tcp.Status.Addons.Konnectivity.Gateway = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tcp.Status.Addons.Konnectivity.Gateway = &kamajiv1alpha1.KubernetesGatewayStatus{
|
||||||
|
RouteStatus: r.resource.Status.RouteStatus,
|
||||||
|
RouteRef: v1.LocalObjectReference{
|
||||||
|
Name: r.resource.Name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
routeStatuses := tcp.Status.Addons.Konnectivity.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 konnectivity route")
|
||||||
|
}
|
||||||
|
if len(routeStatuses.Parents) > 1 {
|
||||||
|
return fmt.Errorf("too many gateways attached to the konnectivity route")
|
||||||
|
}
|
||||||
|
if len(r.resource.Spec.Hostnames) == 0 {
|
||||||
|
return fmt.Errorf("no hostname in the konnectivity route")
|
||||||
|
}
|
||||||
|
if len(r.resource.Spec.Hostnames) > 1 {
|
||||||
|
return fmt.Errorf("too many hostnames in the konnectivity route")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.V(1).Info("updating TenantControlPlane status for Konnectivity Gateway routes")
|
||||||
|
accessPoints, err := resources.BuildGatewayAccessPointsStatus(ctx, r.Client, r.resource, routeStatuses)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tcp.Status.Addons.Konnectivity.Gateway.AccessPoints = accessPoints
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *KubernetesKonnectivityGatewayResource) Define(_ context.Context, tcp *kamajiv1alpha1.TenantControlPlane) error {
|
||||||
|
r.resource = &gatewayv1alpha2.TLSRoute{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: fmt.Sprintf("%s-konnectivity", tcp.GetName()),
|
||||||
|
Namespace: tcp.GetNamespace(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *KubernetesKonnectivityGatewayResource) mutate(tcp *kamajiv1alpha1.TenantControlPlane) controllerutil.MutateFn {
|
||||||
|
return func() error {
|
||||||
|
// Use control plane gateway configuration
|
||||||
|
if tcp.Spec.ControlPlane.Gateway == nil {
|
||||||
|
return fmt.Errorf("control plane gateway is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Use hostname from control plane gateway
|
||||||
|
if len(tcp.Spec.ControlPlane.Gateway.Hostname) == 0 {
|
||||||
|
return fmt.Errorf("control plane gateway hostname is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceName := gatewayv1alpha2.ObjectName(tcp.Status.Addons.Konnectivity.Service.Name)
|
||||||
|
servicePort := tcp.Status.Addons.Konnectivity.Service.Port
|
||||||
|
|
||||||
|
if serviceName == "" || servicePort == 0 {
|
||||||
|
return fmt.Errorf("konnectivity service not ready, cannot create TLSRoute")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy parentRefs from control plane gateway and explicitly set port and sectionName fields
|
||||||
|
if tcp.Spec.ControlPlane.Gateway.GatewayParentRefs == nil {
|
||||||
|
return fmt.Errorf("control plane gateway parentRefs are not specified")
|
||||||
|
}
|
||||||
|
r.resource.Spec.ParentRefs = resources.NewParentRefsSpecWithPortAndSection(tcp.Spec.ControlPlane.Gateway.GatewayParentRefs, servicePort, "konnectivity-server")
|
||||||
|
|
||||||
|
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 *KubernetesKonnectivityGatewayResource) CreateOrUpdate(ctx context.Context, tenantControlPlane *kamajiv1alpha1.TenantControlPlane) (controllerutil.OperationResult, error) {
|
||||||
|
logger := log.FromContext(ctx, "resource", r.GetName())
|
||||||
|
|
||||||
|
if !r.shouldHaveGateway(tenantControlPlane) {
|
||||||
|
return controllerutil.OperationResultNone, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Konnectivity using a Gateway resource")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.V(1).Info("creating or updating resource konnectivity 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 *KubernetesKonnectivityGatewayResource) GetName() string {
|
||||||
|
return "konnectivity_gateway_routes"
|
||||||
|
}
|
||||||
218
internal/resources/konnectivity/gateway_resource_test.go
Normal file
218
internal/resources/konnectivity/gateway_resource_test.go
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
// Copyright 2022 Clastix Labs
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package konnectivity_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"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
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/konnectivity"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKonnectivityGatewayResource(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "Konnectivity 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("KubernetesKonnectivityGatewayResource", func() {
|
||||||
|
var (
|
||||||
|
tcp *kamajiv1alpha1.TenantControlPlane
|
||||||
|
resource *konnectivity.KubernetesKonnectivityGatewayResource
|
||||||
|
ctx context.Context
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx = context.Background()
|
||||||
|
|
||||||
|
fakeClient := fake.NewClientBuilder().
|
||||||
|
WithScheme(runtimeScheme).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
resource = &konnectivity.KubernetesKonnectivityGatewayResource{
|
||||||
|
Client: fakeClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace := gatewayv1.Namespace("default")
|
||||||
|
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"),
|
||||||
|
GatewayParentRefs: []gatewayv1alpha2.ParentReference{
|
||||||
|
{
|
||||||
|
Name: "test-gateway",
|
||||||
|
Namespace: &namespace,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Addons: kamajiv1alpha1.AddonsSpec{
|
||||||
|
Konnectivity: &kamajiv1alpha1.KonnectivitySpec{
|
||||||
|
KonnectivityServerSpec: kamajiv1alpha1.KonnectivityServerSpec{
|
||||||
|
Port: 8132,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: kamajiv1alpha1.TenantControlPlaneStatus{
|
||||||
|
Addons: kamajiv1alpha1.AddonsStatus{
|
||||||
|
Konnectivity: kamajiv1alpha1.KonnectivityStatus{
|
||||||
|
Service: kamajiv1alpha1.KubernetesServiceStatus{
|
||||||
|
Name: "test-konnectivity-service",
|
||||||
|
Port: 8132,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("shouldHaveGateway logic", func() {
|
||||||
|
It("should return false when Konnectivity addon is disabled", func() {
|
||||||
|
tcp.Spec.Addons.Konnectivity = nil
|
||||||
|
shouldUpdate := resource.ShouldStatusBeUpdated(ctx, tcp)
|
||||||
|
Expect(shouldUpdate).To(BeFalse())
|
||||||
|
Expect(resource.ShouldCleanup(tcp)).To(BeFalse())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should return false when control plane gateway is not configured", func() {
|
||||||
|
tcp.Spec.ControlPlane.Gateway = nil
|
||||||
|
shouldUpdate := resource.ShouldStatusBeUpdated(ctx, tcp)
|
||||||
|
Expect(shouldUpdate).To(BeFalse())
|
||||||
|
Expect(resource.ShouldCleanup(tcp)).To(BeFalse())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should return true when both Konnectivity and gateway are configured", func() {
|
||||||
|
shouldUpdate := resource.ShouldStatusBeUpdated(ctx, tcp)
|
||||||
|
Expect(shouldUpdate).To(BeTrue())
|
||||||
|
Expect(resource.ShouldCleanup(tcp)).To(BeFalse())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("When Konnectivity gateway should be configured", func() {
|
||||||
|
It("should set correct TLSRoute name with -konnectivity suffix", func() {
|
||||||
|
err := resource.Define(ctx, tcp)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
_, err = resource.CreateOrUpdate(ctx, tcp)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
route := &gatewayv1alpha2.TLSRoute{}
|
||||||
|
err = resource.Client.Get(ctx, client.ObjectKey{Name: "test-tcp-konnectivity", Namespace: tcp.Namespace}, route)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(route.Name).To(Equal("test-tcp-konnectivity"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should set sectionName to \"konnectivity-server\" and port from Konnectivity service status", func() {
|
||||||
|
err := resource.Define(ctx, tcp)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
_, err = resource.CreateOrUpdate(ctx, tcp)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
route := &gatewayv1alpha2.TLSRoute{}
|
||||||
|
err = resource.Client.Get(ctx, client.ObjectKey{Name: "test-tcp-konnectivity", Namespace: tcp.Namespace}, route)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(route.Spec.ParentRefs).To(HaveLen(1))
|
||||||
|
Expect(route.Spec.ParentRefs[0].SectionName).NotTo(BeNil())
|
||||||
|
Expect(*route.Spec.ParentRefs[0].SectionName).To(Equal(gatewayv1.SectionName("konnectivity-server")))
|
||||||
|
Expect(route.Spec.ParentRefs[0].Port).NotTo(BeNil())
|
||||||
|
Expect(*route.Spec.ParentRefs[0].Port).To(Equal(tcp.Status.Addons.Konnectivity.Service.Port))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should use control plane gateway hostname", func() {
|
||||||
|
err := resource.Define(ctx, tcp)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
_, err = resource.CreateOrUpdate(ctx, tcp)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
route := &gatewayv1alpha2.TLSRoute{}
|
||||||
|
err = resource.Client.Get(ctx, client.ObjectKey{Name: "test-tcp-konnectivity", Namespace: tcp.Namespace}, route)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(route.Spec.Hostnames).To(HaveLen(1))
|
||||||
|
Expect(route.Spec.Hostnames[0]).To(Equal(tcp.Spec.ControlPlane.Gateway.Hostname))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("Konnectivity-specific error cases", func() {
|
||||||
|
It("should return early without error when control plane gateway is not configured", func() {
|
||||||
|
tcp.Spec.ControlPlane.Gateway = nil
|
||||||
|
|
||||||
|
err := resource.Define(ctx, tcp)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
result, err := resource.CreateOrUpdate(ctx, tcp)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(result).To(Equal(controllerutil.OperationResultNone))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should fail when Konnectivity service is not ready", func() {
|
||||||
|
tcp.Status.Addons.Konnectivity.Service.Name = ""
|
||||||
|
tcp.Status.Addons.Konnectivity.Service.Port = 0
|
||||||
|
|
||||||
|
err := resource.Define(ctx, tcp)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
_, err = resource.CreateOrUpdate(ctx, tcp)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(err.Error()).To(ContainSubstring("konnectivity service not ready"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should fail when control plane gateway parentRefs are not specified", func() {
|
||||||
|
tcp.Spec.ControlPlane.Gateway.GatewayParentRefs = nil
|
||||||
|
|
||||||
|
err := resource.Define(ctx, tcp)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
_, err = resource.CreateOrUpdate(ctx, tcp)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(err.Error()).To(ContainSubstring("control plane gateway parentRefs are not specified"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("When Konnectivity gateway should not be configured", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
tcp.Spec.Addons.Konnectivity = nil
|
||||||
|
tcp.Status.Addons.Konnectivity = kamajiv1alpha1.KonnectivityStatus{
|
||||||
|
Gateway: &kamajiv1alpha1.KubernetesGatewayStatus{
|
||||||
|
AccessPoints: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should cleanup when gateway is removed", func() {
|
||||||
|
Expect(resource.ShouldCleanup(tcp)).To(BeTrue())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should return correct resource name", func() {
|
||||||
|
Expect(resource.GetName()).To(Equal("konnectivity_gateway_routes"))
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -13,6 +13,7 @@ var (
|
|||||||
clusterrolebindingCollector prometheus.Histogram
|
clusterrolebindingCollector prometheus.Histogram
|
||||||
deploymentCollector prometheus.Histogram
|
deploymentCollector prometheus.Histogram
|
||||||
egressCollector prometheus.Histogram
|
egressCollector prometheus.Histogram
|
||||||
|
gatewayCollector prometheus.Histogram
|
||||||
kubeconfigCollector prometheus.Histogram
|
kubeconfigCollector prometheus.Histogram
|
||||||
serviceaccountCollector prometheus.Histogram
|
serviceaccountCollector prometheus.Histogram
|
||||||
serviceCollector prometheus.Histogram
|
serviceCollector prometheus.Histogram
|
||||||
|
|||||||
Reference in New Issue
Block a user