mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-02-14 09:59:57 +00:00
feat(controller): refactor namespace core loop and state management (#1680)
* feat(controller): allow owners to promote serviceaccounts within tenant as owners Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(controller): refactor status handling for tenants and owned namespaces (including metrics) Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> --------- Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
This commit is contained in:
@@ -12,6 +12,7 @@ type NamespaceOptions struct {
|
||||
// Specifies the maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
|
||||
Quota *int32 `json:"quota,omitempty"`
|
||||
// Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
|
||||
// Deprecated: Use additionalMetadataList instead
|
||||
AdditionalMetadata *api.AdditionalMetadataSpec `json:"additionalMetadata,omitempty"`
|
||||
// Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant via a list. Optional.
|
||||
AdditionalMetadataList []api.AdditionalMetadataSelectorSpec `json:"additionalMetadataList,omitempty"`
|
||||
@@ -19,4 +20,7 @@ type NamespaceOptions struct {
|
||||
ForbiddenLabels api.ForbiddenListSpec `json:"forbiddenLabels,omitempty"`
|
||||
// Define the annotations that a Tenant Owner cannot set for their Namespace resources.
|
||||
ForbiddenAnnotations api.ForbiddenListSpec `json:"forbiddenAnnotations,omitempty"`
|
||||
// If enabled only metadata from additionalMetadata is reconciled to the namespaces.
|
||||
//+kubebuilder:default:=false
|
||||
ManagedMetadataOnly bool `json:"managedMetadataOnly,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
|
||||
package v1beta2
|
||||
|
||||
import (
|
||||
k8stypes "k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
)
|
||||
|
||||
// +kubebuilder:validation:Enum=Cordoned;Active
|
||||
type tenantState string
|
||||
|
||||
@@ -18,6 +24,68 @@ type TenantStatus struct {
|
||||
State tenantState `json:"state"`
|
||||
// How many namespaces are assigned to the Tenant.
|
||||
Size uint `json:"size"`
|
||||
// List of namespaces assigned to the Tenant.
|
||||
// List of namespaces assigned to the Tenant. (Deprecated)
|
||||
Namespaces []string `json:"namespaces,omitempty"`
|
||||
// Tracks state for the namespaces associated with this tenant
|
||||
Spaces []*TenantStatusNamespaceItem `json:"spaces,omitempty"`
|
||||
// Tenant Condition
|
||||
Conditions meta.ConditionList `json:"conditions"`
|
||||
}
|
||||
|
||||
type TenantStatusNamespaceItem struct {
|
||||
// Conditions
|
||||
Conditions meta.ConditionList `json:"conditions"`
|
||||
// Namespace Name
|
||||
Name string `json:"name"`
|
||||
// Namespace UID
|
||||
UID k8stypes.UID `json:"uid,omitempty"`
|
||||
// Managed Metadata
|
||||
Metadata *TenantStatusNamespaceMetadata `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type TenantStatusNamespaceMetadata struct {
|
||||
// Managed Labels
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
// Managed Annotations
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
func (ms *TenantStatus) GetInstance(stat *TenantStatusNamespaceItem) *TenantStatusNamespaceItem {
|
||||
for _, source := range ms.Spaces {
|
||||
if ms.instancequal(source, stat) {
|
||||
return source
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ms *TenantStatus) UpdateInstance(stat *TenantStatusNamespaceItem) {
|
||||
// Check if the tenant is already present in the status
|
||||
for i, source := range ms.Spaces {
|
||||
if ms.instancequal(source, stat) {
|
||||
ms.Spaces[i] = stat
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ms.Spaces = append(ms.Spaces, stat)
|
||||
}
|
||||
|
||||
func (ms *TenantStatus) RemoveInstance(stat *TenantStatusNamespaceItem) {
|
||||
// Filter out the datasource with given UID
|
||||
filter := []*TenantStatusNamespaceItem{}
|
||||
|
||||
for _, source := range ms.Spaces {
|
||||
if !ms.instancequal(source, stat) {
|
||||
filter = append(filter, source)
|
||||
}
|
||||
}
|
||||
|
||||
ms.Spaces = filter
|
||||
}
|
||||
|
||||
func (ms *TenantStatus) instancequal(a, b *TenantStatusNamespaceItem) bool {
|
||||
return a.Name == b.Name
|
||||
}
|
||||
|
||||
@@ -32,8 +32,10 @@ type TenantSpec struct {
|
||||
// Specifies the label to control the placement of pods on a given pool of worker nodes. All namespaces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
|
||||
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
|
||||
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
|
||||
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
|
||||
NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitempty"`
|
||||
// Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
|
||||
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
|
||||
LimitRanges api.LimitRangesSpec `json:"limitRanges,omitempty"`
|
||||
// Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional.
|
||||
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
|
||||
@@ -74,12 +76,13 @@ type TenantSpec struct {
|
||||
// +kubebuilder:storageversion
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:scope=Cluster,shortName=tnt
|
||||
// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.state",description="The actual state of the Tenant"
|
||||
// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.conditions[?(@.type==\"Cordoned\")].reason",description="The actual state of the Tenant"
|
||||
// +kubebuilder:printcolumn:name="Namespace quota",type="integer",JSONPath=".spec.namespaceOptions.quota",description="The max amount of Namespaces can be created"
|
||||
// +kubebuilder:printcolumn:name="Namespace count",type="integer",JSONPath=".status.size",description="The total amount of Namespaces in use"
|
||||
// +kubebuilder:printcolumn:name="Node selector",type="string",JSONPath=".spec.nodeSelector",description="Node Selector applied to Pods"
|
||||
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Reconcile Status for the tenant"
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="Reconcile Message for the tenant"
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
|
||||
|
||||
// Tenant is the Schema for the tenants API.
|
||||
type Tenant struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
@@ -9,6 +9,7 @@ package v1beta2
|
||||
|
||||
import (
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -1215,6 +1216,24 @@ func (in *TenantStatus) DeepCopyInto(out *TenantStatus) {
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Spaces != nil {
|
||||
in, out := &in.Spaces, &out.Spaces
|
||||
*out = make([]*TenantStatusNamespaceItem, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = new(TenantStatusNamespaceItem)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
}
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make(meta.ConditionList, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantStatus.
|
||||
@@ -1226,3 +1245,59 @@ func (in *TenantStatus) DeepCopy() *TenantStatus {
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *TenantStatusNamespaceItem) DeepCopyInto(out *TenantStatusNamespaceItem) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make(meta.ConditionList, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Metadata != nil {
|
||||
in, out := &in.Metadata, &out.Metadata
|
||||
*out = new(TenantStatusNamespaceMetadata)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantStatusNamespaceItem.
|
||||
func (in *TenantStatusNamespaceItem) DeepCopy() *TenantStatusNamespaceItem {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(TenantStatusNamespaceItem)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *TenantStatusNamespaceMetadata) DeepCopyInto(out *TenantStatusNamespaceMetadata) {
|
||||
*out = *in
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Annotations != nil {
|
||||
in, out := &in.Annotations, &out.Annotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantStatusNamespaceMetadata.
|
||||
func (in *TenantStatusNamespaceMetadata) DeepCopy() *TenantStatusNamespaceMetadata {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(TenantStatusNamespaceMetadata)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -254,6 +254,7 @@ The following Values have changed key or Value:
|
||||
| webhooks.hooks.tenants.matchPolicy | string | `"Exact"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
| webhooks.hooks.tenants.namespaceSelector | object | `{}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
|
||||
| webhooks.hooks.tenants.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
|
||||
| webhooks.hooks.tenants.reinvocationPolicy | string | `"Never"` | [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) |
|
||||
| webhooks.mutatingWebhooksTimeoutSeconds | int | `30` | Timeout in seconds for mutating webhooks |
|
||||
| webhooks.service.caBundle | string | `""` | CABundle for the webhook service |
|
||||
| webhooks.service.name | string | `""` | Custom service name for the webhook service |
|
||||
|
||||
@@ -1041,7 +1041,7 @@ spec:
|
||||
status: {}
|
||||
- additionalPrinterColumns:
|
||||
- description: The actual state of the Tenant
|
||||
jsonPath: .status.state
|
||||
jsonPath: .status.conditions[?(@.type=="Cordoned")].reason
|
||||
name: State
|
||||
type: string
|
||||
- description: The max amount of Namespaces can be created
|
||||
@@ -1056,6 +1056,14 @@ spec:
|
||||
jsonPath: .spec.nodeSelector
|
||||
name: Node selector
|
||||
type: string
|
||||
- description: Reconcile Status for the tenant
|
||||
jsonPath: .status.conditions[?(@.type=="Ready")].status
|
||||
name: Ready
|
||||
type: string
|
||||
- description: Reconcile Message for the tenant
|
||||
jsonPath: .status.conditions[?(@.type=="Ready")].message
|
||||
name: Status
|
||||
type: string
|
||||
- description: Age
|
||||
jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
@@ -1319,9 +1327,9 @@ spec:
|
||||
type: string
|
||||
type: object
|
||||
limitRanges:
|
||||
description: Specifies the resource min/max usage restrictions to
|
||||
the Tenant. The assigned values are inherited by any namespace created
|
||||
in the Tenant. Optional.
|
||||
description: |-
|
||||
Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
|
||||
Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
@@ -1410,8 +1418,9 @@ spec:
|
||||
the Tenant owner cannot create further namespaces. Optional.
|
||||
properties:
|
||||
additionalMetadata:
|
||||
description: Specifies additional labels and annotations the Capsule
|
||||
operator places on any Namespace resource in the Tenant. Optional.
|
||||
description: |-
|
||||
Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
|
||||
Deprecated: Use additionalMetadataList instead
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
@@ -1509,6 +1518,11 @@ spec:
|
||||
deniedRegex:
|
||||
type: string
|
||||
type: object
|
||||
managedMetadataOnly:
|
||||
default: false
|
||||
description: If enabled only metadata from additionalMetadata
|
||||
is reconciled to the namespaces.
|
||||
type: boolean
|
||||
quota:
|
||||
description: Specifies the maximum number of namespaces allowed
|
||||
for that Tenant. Once the namespace quota assigned to the Tenant
|
||||
@@ -1519,9 +1533,9 @@ spec:
|
||||
type: integer
|
||||
type: object
|
||||
networkPolicies:
|
||||
description: Specifies the NetworkPolicies assigned to the Tenant.
|
||||
The assigned NetworkPolicies are inherited by any namespace created
|
||||
in the Tenant. Optional.
|
||||
description: |-
|
||||
Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
|
||||
Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
@@ -2423,14 +2437,159 @@ spec:
|
||||
status:
|
||||
description: Returns the observed state of the Tenant.
|
||||
properties:
|
||||
conditions:
|
||||
description: Tenant Condition
|
||||
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
|
||||
type: array
|
||||
namespaces:
|
||||
description: List of namespaces assigned to the Tenant.
|
||||
description: List of namespaces assigned to the Tenant. (Deprecated)
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
size:
|
||||
description: How many namespaces are assigned to the Tenant.
|
||||
type: integer
|
||||
spaces:
|
||||
description: Tracks state for the namespaces associated with this
|
||||
tenant
|
||||
items:
|
||||
properties:
|
||||
conditions:
|
||||
description: Conditions
|
||||
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
|
||||
type: array
|
||||
metadata:
|
||||
description: Managed Metadata
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Managed Annotations
|
||||
type: object
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Managed Labels
|
||||
type: object
|
||||
type: object
|
||||
name:
|
||||
description: Namespace Name
|
||||
type: string
|
||||
uid:
|
||||
description: Namespace UID
|
||||
type: string
|
||||
required:
|
||||
- conditions
|
||||
- name
|
||||
type: object
|
||||
type: array
|
||||
state:
|
||||
default: Active
|
||||
description: The operational state of the Tenant. Possible values
|
||||
@@ -2440,6 +2599,7 @@ spec:
|
||||
- Active
|
||||
type: string
|
||||
required:
|
||||
- conditions
|
||||
- size
|
||||
- state
|
||||
type: object
|
||||
|
||||
@@ -24,6 +24,7 @@ spec:
|
||||
ignoreUserWithGroups:
|
||||
{{- toYaml .Values.manager.options.ignoreUserWithGroups | nindent 4 }}
|
||||
protectedNamespaceRegex: {{ .Values.manager.options.protectedNamespaceRegex | quote }}
|
||||
defaultRegistry: {{ .Values.manager.options.defaultRegistry }}
|
||||
{{- with .Values.manager.options.nodeMetadata }}
|
||||
nodeMetadata:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
|
||||
@@ -274,4 +274,44 @@ webhooks:
|
||||
timeoutSeconds: {{ $.Values.webhooks.mutatingWebhooksTimeoutSeconds }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with .Values.webhooks.hooks.tenants }}
|
||||
{{- if .enabled }}
|
||||
- name: tenants.projectcapsule.dev
|
||||
admissionReviewVersions:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- include "capsule.webhooks.service" (dict "path" "/tenants/mutating" "ctx" $) | nindent 4 }}
|
||||
failurePolicy: {{ .failurePolicy }}
|
||||
matchPolicy: {{ .matchPolicy }}
|
||||
reinvocationPolicy: {{ .reinvocationPolicy }}
|
||||
{{- with .namespaceSelector }}
|
||||
namespaceSelector:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .objectSelector }}
|
||||
objectSelector:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .matchConditions }}
|
||||
matchConditions:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- capsule.clastix.io
|
||||
apiVersions:
|
||||
- v1beta2
|
||||
operations:
|
||||
- CREATE
|
||||
- UPDATE
|
||||
- DELETE
|
||||
resources:
|
||||
- tenants
|
||||
scope: 'Cluster'
|
||||
sideEffects: None
|
||||
timeoutSeconds: {{ $.Values.webhooks.mutatingWebhooksTimeoutSeconds }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- end }}
|
||||
|
||||
@@ -261,6 +261,7 @@ webhooks:
|
||||
- UPDATE
|
||||
resources:
|
||||
- pods
|
||||
- pods/ephemeralcontainers
|
||||
scope: Namespaced
|
||||
sideEffects: None
|
||||
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
|
||||
@@ -382,7 +383,7 @@ webhooks:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- include "capsule.webhooks.service" (dict "path" "/tenants" "ctx" $) | nindent 4 }}
|
||||
{{- include "capsule.webhooks.service" (dict "path" "/tenants/validating" "ctx" $) | nindent 4 }}
|
||||
failurePolicy: {{ .failurePolicy }}
|
||||
matchPolicy: {{ .matchPolicy }}
|
||||
{{- with .namespaceSelector }}
|
||||
@@ -408,7 +409,7 @@ webhooks:
|
||||
- DELETE
|
||||
resources:
|
||||
- tenants
|
||||
scope: '*'
|
||||
scope: 'Cluster'
|
||||
sideEffects: None
|
||||
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1379,6 +1379,10 @@
|
||||
"objectSelector": {
|
||||
"description": "[ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector)",
|
||||
"type": "object"
|
||||
},
|
||||
"reinvocationPolicy": {
|
||||
"description": "[ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,6 +572,8 @@ webhooks:
|
||||
namespaceSelector: {}
|
||||
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
|
||||
matchConditions: []
|
||||
# -- [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)
|
||||
reinvocationPolicy: Never
|
||||
|
||||
tenantResourceObjects:
|
||||
# -- Enable the Hook
|
||||
|
||||
12
cmd/main.go
12
cmd/main.go
@@ -57,7 +57,8 @@ import (
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/route"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/service"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/serviceaccounts"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/tenant"
|
||||
tenantmutation "github.com/projectcapsule/capsule/pkg/webhook/tenant/mutation"
|
||||
tenantvalidation "github.com/projectcapsule/capsule/pkg/webhook/tenant/validation"
|
||||
tntresource "github.com/projectcapsule/capsule/pkg/webhook/tenantresource"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
)
|
||||
@@ -227,19 +228,20 @@ func main() {
|
||||
// webhooks: the order matters, don't change it and just append
|
||||
webhooksList := append(
|
||||
make([]webhook.Webhook, 0),
|
||||
route.Pod(pod.ImagePullPolicy(), pod.ContainerRegistry(), pod.PriorityClass(), pod.RuntimeClass()),
|
||||
route.Pod(pod.ImagePullPolicy(), pod.ContainerRegistry(cfg), pod.PriorityClass(), pod.RuntimeClass()),
|
||||
route.Namespace(utils.InCapsuleGroups(cfg, namespacevalidation.PatchHandler(cfg), namespacevalidation.QuotaHandler(), namespacevalidation.FreezeHandler(cfg), namespacevalidation.PrefixHandler(cfg), namespacevalidation.UserMetadataHandler())),
|
||||
route.Ingress(ingress.Class(cfg, kubeVersion), ingress.Hostnames(cfg), ingress.Collision(cfg), ingress.Wildcard()),
|
||||
route.PVC(pvc.Validating(), pvc.PersistentVolumeReuse()),
|
||||
route.Service(service.Handler()),
|
||||
route.TenantResourceObjects(utils.InCapsuleGroups(cfg, tntresource.WriteOpsHandler())),
|
||||
route.NetworkPolicy(utils.InCapsuleGroups(cfg, networkpolicy.Handler())),
|
||||
route.Tenant(tenant.NameHandler(), tenant.RoleBindingRegexHandler(), tenant.IngressClassRegexHandler(), tenant.StorageClassRegexHandler(), tenant.ContainerRegistryRegexHandler(), tenant.HostnameRegexHandler(), tenant.FreezedEmitter(), tenant.ServiceAccountNameHandler(), tenant.ForbiddenAnnotationsRegexHandler(), tenant.ProtectedHandler(), tenant.MetaHandler()),
|
||||
route.Cordoning(tenant.CordoningHandler(cfg)),
|
||||
route.TenantMutating(tenantmutation.MetaHandler()),
|
||||
route.TenantValidating(tenantvalidation.NameHandler(), tenantvalidation.RoleBindingRegexHandler(), tenantvalidation.IngressClassRegexHandler(), tenantvalidation.StorageClassRegexHandler(), tenantvalidation.ContainerRegistryRegexHandler(), tenantvalidation.HostnameRegexHandler(), tenantvalidation.FreezedEmitter(), tenantvalidation.ServiceAccountNameHandler(), tenantvalidation.ForbiddenAnnotationsRegexHandler(), tenantvalidation.ProtectedHandler()),
|
||||
route.Cordoning(tenantvalidation.CordoningHandler(cfg)),
|
||||
route.Node(utils.InCapsuleGroups(cfg, node.UserMetadataHandler(cfg, kubeVersion))),
|
||||
route.ServiceAccounts(serviceaccounts.Handler(cfg)),
|
||||
route.NamespacePatch(utils.InCapsuleGroups(cfg, namespacemutation.CordoningLabelHandler(cfg), namespacemutation.OwnerReferenceHandler(cfg), namespacemutation.MetadataHandler(cfg))),
|
||||
route.CustomResources(tenant.ResourceCounterHandler(manager.GetClient())),
|
||||
route.CustomResources(tenantvalidation.ResourceCounterHandler(manager.GetClient())),
|
||||
route.Gateway(gateway.Class(cfg)),
|
||||
route.Defaults(defaults.Handler(cfg, kubeVersion)),
|
||||
route.ResourcePoolMutation((resourcepool.PoolMutationHandler(ctrl.Log.WithName("webhooks").WithName("resourcepool")))),
|
||||
|
||||
@@ -5,12 +5,15 @@ package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/client-go/util/retry"
|
||||
@@ -20,6 +23,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/metrics"
|
||||
)
|
||||
|
||||
@@ -52,7 +56,7 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
|
||||
r.Log.Info("Request object not found, could have been deleted after reconcile request")
|
||||
|
||||
// If tenant was deleted or cannot be found, clean up metrics
|
||||
r.Metrics.DeleteAllMetrics(request.Name)
|
||||
r.Metrics.DeleteAllMetricsForTenant(request.Name)
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
@@ -62,17 +66,19 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
|
||||
return result, err
|
||||
}
|
||||
|
||||
preRecNamespaces := instance.Status.Namespaces
|
||||
defer func() {
|
||||
r.syncTenantStatusMetrics(instance)
|
||||
|
||||
// Ensuring the Tenant Status
|
||||
if err = r.updateTenantStatus(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot update Tenant status")
|
||||
if uerr := r.updateTenantStatus(ctx, instance, err); uerr != nil {
|
||||
err = fmt.Errorf("cannot update tenant status: %w", uerr)
|
||||
|
||||
return result, err
|
||||
}
|
||||
// Ensuring Metadata
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// Ensuring Metadata.
|
||||
if err = r.ensureMetadata(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot ensure metadata")
|
||||
err = fmt.Errorf("cannot ensure metadata: %w", err)
|
||||
|
||||
return result, err
|
||||
}
|
||||
@@ -81,35 +87,25 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
|
||||
r.Log.Info("Ensuring limit resources count is updated")
|
||||
|
||||
if err = r.syncCustomResourceQuotaUsages(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot count limited resources")
|
||||
err = fmt.Errorf("cannot count limited resources: %w", err)
|
||||
|
||||
return result, err
|
||||
}
|
||||
// Ensuring all namespaces are collected
|
||||
r.Log.Info("Ensuring all Namespaces are collected")
|
||||
|
||||
if err = r.collectNamespaces(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot collect Namespace resources")
|
||||
|
||||
return result, err
|
||||
}
|
||||
// Ensuring Status metrics are exposed
|
||||
r.Log.Info("Ensuring all status metrics are exposed")
|
||||
r.syncStatusMetrics(instance, preRecNamespaces)
|
||||
|
||||
// Ensuring Namespace metadata
|
||||
// Reconcile Namespaces
|
||||
r.Log.Info("Starting processing of Namespaces", "items", len(instance.Status.Namespaces))
|
||||
|
||||
if err = r.syncNamespaces(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync Namespace items")
|
||||
if err = r.reconcileNamespaces(ctx, instance); err != nil {
|
||||
err = fmt.Errorf("namespace(s) had reconciliation errors")
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Ensuring NetworkPolicy resources
|
||||
r.Log.Info("Starting processing of Network Policies")
|
||||
|
||||
if err = r.syncNetworkPolicies(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync NetworkPolicy items")
|
||||
err = fmt.Errorf("cannot sync networkPolicy items: %w", err)
|
||||
|
||||
return result, err
|
||||
}
|
||||
@@ -117,7 +113,7 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
|
||||
r.Log.Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges.Items))
|
||||
|
||||
if err = r.syncLimitRanges(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync LimitRange items")
|
||||
err = fmt.Errorf("cannot sync limitrange items: %w", err)
|
||||
|
||||
return result, err
|
||||
}
|
||||
@@ -125,7 +121,7 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
|
||||
r.Log.Info("Starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota.Items))
|
||||
|
||||
if err = r.syncResourceQuotas(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync ResourceQuota items")
|
||||
err = fmt.Errorf("cannot sync resourcequota items: %w", err)
|
||||
|
||||
return result, err
|
||||
}
|
||||
@@ -133,15 +129,7 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
|
||||
r.Log.Info("Ensuring RoleBindings for Owners and Tenant")
|
||||
|
||||
if err = r.syncRoleBindings(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync RoleBindings items")
|
||||
|
||||
return result, err
|
||||
}
|
||||
// Ensuring Namespace count
|
||||
r.Log.Info("Ensuring Namespace count")
|
||||
|
||||
if err = r.ensureNamespaceCount(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync Namespace count")
|
||||
err = fmt.Errorf("cannot sync rolebindings items: %w", err)
|
||||
|
||||
return result, err
|
||||
}
|
||||
@@ -151,14 +139,40 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
func (r *Manager) updateTenantStatus(ctx context.Context, tnt *capsulev1beta2.Tenant) error {
|
||||
func (r *Manager) updateTenantStatus(ctx context.Context, tnt *capsulev1beta2.Tenant, reconcileError error) error {
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
||||
if tnt.Spec.Cordoned {
|
||||
tnt.Status.State = capsulev1beta2.TenantStateCordoned
|
||||
} else {
|
||||
tnt.Status.State = capsulev1beta2.TenantStateActive
|
||||
latest := &capsulev1beta2.Tenant{}
|
||||
if err = r.Get(ctx, types.NamespacedName{Name: tnt.GetName()}, latest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.Client.Status().Update(ctx, tnt)
|
||||
latest.Status = tnt.Status
|
||||
|
||||
// Set Ready Condition
|
||||
readyCondition := meta.NewReadyCondition(tnt)
|
||||
if reconcileError != nil {
|
||||
readyCondition.Message = reconcileError.Error()
|
||||
readyCondition.Status = metav1.ConditionFalse
|
||||
readyCondition.Reason = meta.FailedReason
|
||||
}
|
||||
|
||||
latest.Status.Conditions.UpdateConditionByType(readyCondition)
|
||||
|
||||
// Set Cordoned Condition
|
||||
cordonedCondition := meta.NewCordonedCondition(tnt)
|
||||
|
||||
if tnt.Spec.Cordoned {
|
||||
latest.Status.State = capsulev1beta2.TenantStateCordoned
|
||||
|
||||
cordonedCondition.Reason = meta.CordonedReason
|
||||
cordonedCondition.Message = "Tenant is cordoned"
|
||||
cordonedCondition.Status = metav1.ConditionTrue
|
||||
} else {
|
||||
latest.Status.State = capsulev1beta2.TenantStateActive
|
||||
}
|
||||
|
||||
latest.Status.Conditions.UpdateConditionByType(cordonedCondition)
|
||||
|
||||
return r.Client.Status().Update(ctx, latest)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ package tenant
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
capsuleapi "github.com/projectcapsule/capsule/pkg/api"
|
||||
)
|
||||
@@ -17,7 +19,13 @@ func (r *Manager) ensureMetadata(ctx context.Context, tnt *capsulev1beta2.Tenant
|
||||
tnt.Labels = make(map[string]string)
|
||||
}
|
||||
|
||||
tnt.Labels[capsuleapi.TenantNameLabel] = tnt.Name
|
||||
if v, ok := tnt.Labels[capsuleapi.TenantNameLabel]; ok && v == tnt.Name {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.Update(ctx, tnt)
|
||||
if err := r.Update(ctx, tnt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.Get(ctx, types.NamespacedName{Name: tnt.GetName()}, tnt)
|
||||
}
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"slices"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
)
|
||||
|
||||
// Exposing Status Metrics for tenant.
|
||||
func (r *Manager) syncStatusMetrics(tenant *capsulev1beta2.Tenant, preRecNamespaces []string) {
|
||||
func (r *Manager) syncTenantStatusMetrics(tenant *capsulev1beta2.Tenant) {
|
||||
var cordoned float64 = 0
|
||||
|
||||
// Expose namespace-tenant relationship
|
||||
@@ -17,18 +19,55 @@ func (r *Manager) syncStatusMetrics(tenant *capsulev1beta2.Tenant, preRecNamespa
|
||||
r.Metrics.TenantNamespaceRelationshipGauge.WithLabelValues(tenant.GetName(), ns).Set(1)
|
||||
}
|
||||
|
||||
// Cleanup deleted namespaces
|
||||
for _, ns := range preRecNamespaces {
|
||||
if !slices.Contains(tenant.Status.Namespaces, ns) {
|
||||
r.Metrics.DeleteNamespaceRelationshipMetrics(ns)
|
||||
}
|
||||
}
|
||||
// Expose cordoned status
|
||||
r.Metrics.TenantNamespaceCounterGauge.WithLabelValues(tenant.Name).Set(float64(tenant.Status.Size))
|
||||
|
||||
if tenant.Spec.Cordoned {
|
||||
cordoned = 1
|
||||
}
|
||||
// Expose cordoned status
|
||||
r.Metrics.TenantNamespaceCounterGauge.WithLabelValues(tenant.Name).Set(float64(tenant.Status.Size))
|
||||
// Expose the namespace counter
|
||||
|
||||
// Expose Status Metrics
|
||||
for _, status := range []string{meta.ReadyCondition, meta.CordonedCondition} {
|
||||
var value float64
|
||||
|
||||
cond := tenant.Status.Conditions.GetConditionByType(status)
|
||||
if cond == nil {
|
||||
r.Metrics.DeleteTenantConditionMetricByType(tenant.Name, status)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if cond.Status == metav1.ConditionTrue {
|
||||
value = 1
|
||||
}
|
||||
|
||||
r.Metrics.TenantConditionGauge.WithLabelValues(tenant.GetName(), status).Set(value)
|
||||
}
|
||||
|
||||
// Expose the namespace counter (Deprecated)
|
||||
if tenant.Spec.Cordoned {
|
||||
cordoned = 1
|
||||
}
|
||||
|
||||
r.Metrics.TenantCordonedStatusGauge.WithLabelValues(tenant.Name).Set(cordoned)
|
||||
}
|
||||
|
||||
// Exposing Status Metrics for tenant.
|
||||
func (r *Manager) syncNamespaceStatusMetrics(tenant *capsulev1beta2.Tenant, namespace *corev1.Namespace) {
|
||||
for _, status := range []string{meta.ReadyCondition, meta.CordonedCondition} {
|
||||
var value float64
|
||||
|
||||
cond := tenant.Status.Conditions.GetConditionByType(status)
|
||||
if cond == nil {
|
||||
r.Metrics.DeleteTenantNamespaceConditionMetricByType(namespace.Name, status)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if cond.Status == metav1.ConditionTrue {
|
||||
value = 1
|
||||
}
|
||||
|
||||
r.Metrics.TenantNamespaceConditionGauge.WithLabelValues(tenant.GetName(), namespace.GetName(), status).Set(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/valyala/fasttemplate"
|
||||
"golang.org/x/sync/errgroup"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
@@ -20,51 +21,212 @@ import (
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/utils"
|
||||
)
|
||||
|
||||
// Ensuring all annotations are applied to each Namespace handled by the Tenant.
|
||||
func (r *Manager) syncNamespaces(ctx context.Context, tenant *capsulev1beta2.Tenant) (err error) {
|
||||
func (r *Manager) reconcileNamespaces(ctx context.Context, tenant *capsulev1beta2.Tenant) (err error) {
|
||||
if err = r.collectNamespaces(ctx, tenant); err != nil {
|
||||
err = fmt.Errorf("cannot collect namespaces: %w", err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
gcSet := make(map[string]struct{})
|
||||
for _, inst := range tenant.Status.Spaces {
|
||||
gcSet[inst.Name] = struct{}{}
|
||||
}
|
||||
|
||||
group := new(errgroup.Group)
|
||||
|
||||
for _, item := range tenant.Status.Namespaces {
|
||||
namespace := item
|
||||
|
||||
delete(gcSet, namespace)
|
||||
|
||||
group.Go(func() error {
|
||||
return r.syncNamespaceMetadata(ctx, namespace, tenant)
|
||||
return r.reconcileNamespace(ctx, namespace, tenant)
|
||||
})
|
||||
}
|
||||
|
||||
if err = group.Wait(); err != nil {
|
||||
r.Log.Error(err, "Cannot sync Namespaces")
|
||||
|
||||
err = fmt.Errorf("cannot sync Namespaces: %w", err)
|
||||
}
|
||||
|
||||
for name := range gcSet {
|
||||
r.Metrics.DeleteAllMetricsForNamespace(name)
|
||||
|
||||
tenant.Status.RemoveInstance(&capsulev1beta2.TenantStatusNamespaceItem{
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
|
||||
tenant.Status.Size = uint(len(tenant.Status.Namespaces))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Manager) syncNamespaceMetadata(ctx context.Context, namespace string, tnt *capsulev1beta2.Tenant) (err error) {
|
||||
var res controllerutil.OperationResult
|
||||
func (r *Manager) reconcileNamespace(ctx context.Context, namespace string, tnt *capsulev1beta2.Tenant) (err error) {
|
||||
ns := &corev1.Namespace{}
|
||||
if err = r.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = retry.RetryOnConflict(retry.DefaultBackoff, func() (conflictErr error) {
|
||||
ns := &corev1.Namespace{}
|
||||
if conflictErr = r.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil {
|
||||
return conflictErr
|
||||
stat := &capsulev1beta2.TenantStatusNamespaceItem{
|
||||
Name: namespace,
|
||||
UID: ns.GetUID(),
|
||||
}
|
||||
|
||||
metaStatus := &capsulev1beta2.TenantStatusNamespaceMetadata{}
|
||||
|
||||
// Always update tenant status condition after reconciliation
|
||||
defer func() {
|
||||
instance := tnt.Status.GetInstance(stat)
|
||||
if instance != nil {
|
||||
stat = instance
|
||||
}
|
||||
|
||||
res, conflictErr = controllerutil.CreateOrUpdate(ctx, r.Client, ns, func() error {
|
||||
return SyncNamespaceMetadata(tnt, ns)
|
||||
readCondition := meta.NewReadyCondition(ns)
|
||||
|
||||
if err != nil {
|
||||
readCondition.Status = metav1.ConditionFalse
|
||||
readCondition.Reason = meta.FailedReason
|
||||
readCondition.Message = fmt.Sprintf("Failed to reconcile: %v", err)
|
||||
|
||||
if instance != nil && instance.Metadata != nil {
|
||||
stat.Metadata = instance.Metadata
|
||||
}
|
||||
} else if metaStatus != nil {
|
||||
stat.Metadata = metaStatus
|
||||
}
|
||||
|
||||
stat.Conditions.UpdateConditionByType(readCondition)
|
||||
|
||||
cordonedCondition := meta.NewCordonedCondition(ns)
|
||||
|
||||
if ns.Labels[meta.CordonedLabel] == meta.CordonedLabelTrigger {
|
||||
cordonedCondition.Reason = meta.CordonedReason
|
||||
cordonedCondition.Message = "namespace is cordoned"
|
||||
cordonedCondition.Status = metav1.ConditionTrue
|
||||
}
|
||||
|
||||
stat.Conditions.UpdateConditionByType(cordonedCondition)
|
||||
|
||||
tnt.Status.UpdateInstance(stat)
|
||||
|
||||
r.syncNamespaceStatusMetrics(tnt, ns)
|
||||
}()
|
||||
|
||||
err = retry.RetryOnConflict(retry.DefaultBackoff, func() (conflictErr error) {
|
||||
_, conflictErr = controllerutil.CreateOrUpdate(ctx, r.Client, ns, func() error {
|
||||
metaStatus, err = r.reconcileMetadata(ctx, ns, tnt, stat)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return conflictErr
|
||||
})
|
||||
|
||||
r.emitEvent(tnt, namespace, res, "Ensuring Namespace metadata", err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
//nolint:nestif
|
||||
func (r *Manager) reconcileMetadata(
|
||||
ctx context.Context,
|
||||
ns *corev1.Namespace,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
stat *capsulev1beta2.TenantStatusNamespaceItem,
|
||||
) (
|
||||
managed *capsulev1beta2.TenantStatusNamespaceMetadata,
|
||||
err error,
|
||||
) {
|
||||
capsuleLabel, _ := utils.GetTypeLabel(&capsulev1beta2.Tenant{})
|
||||
|
||||
originLabels := ns.GetLabels()
|
||||
if originLabels == nil {
|
||||
originLabels = make(map[string]string)
|
||||
}
|
||||
|
||||
originAnnotations := ns.GetAnnotations()
|
||||
if originAnnotations == nil {
|
||||
originAnnotations = make(map[string]string)
|
||||
}
|
||||
|
||||
managedAnnotations := buildNamespaceAnnotationsForTenant(tnt)
|
||||
managedLabels := buildNamespaceLabelsForTenant(tnt)
|
||||
|
||||
if opts := tnt.Spec.NamespaceOptions; opts != nil && len(opts.AdditionalMetadataList) > 0 {
|
||||
for _, md := range opts.AdditionalMetadataList {
|
||||
var ok bool
|
||||
|
||||
ok, err = utils.IsNamespaceSelectedBySelector(ns, md.NamespaceSelector)
|
||||
if err != nil {
|
||||
return managed, err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
applyTemplateMap(md.Labels, tnt, ns)
|
||||
applyTemplateMap(md.Annotations, tnt, ns)
|
||||
|
||||
utils.MapMergeNoOverrite(managedLabels, md.Labels)
|
||||
utils.MapMergeNoOverrite(managedAnnotations, md.Annotations)
|
||||
}
|
||||
}
|
||||
|
||||
managedMetadataOnly := tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.ManagedMetadataOnly
|
||||
|
||||
// Handle User-Defined Metadata, if allowed
|
||||
if !managedMetadataOnly {
|
||||
if originLabels != nil {
|
||||
maps.Copy(originLabels, managedLabels)
|
||||
}
|
||||
|
||||
if originAnnotations != nil {
|
||||
maps.Copy(originAnnotations, managedAnnotations)
|
||||
}
|
||||
|
||||
// Cleanup old Metadata
|
||||
instance := tnt.Status.GetInstance(stat)
|
||||
if instance != nil && instance.Metadata != nil {
|
||||
for label := range instance.Metadata.Labels {
|
||||
if _, ok := managedLabels[label]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
delete(originLabels, label)
|
||||
}
|
||||
|
||||
for annotation := range instance.Metadata.Annotations {
|
||||
if _, ok := managedAnnotations[annotation]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
delete(originAnnotations, annotation)
|
||||
}
|
||||
}
|
||||
|
||||
managed = &capsulev1beta2.TenantStatusNamespaceMetadata{
|
||||
Labels: managedLabels,
|
||||
Annotations: managedAnnotations,
|
||||
}
|
||||
} else {
|
||||
originLabels = managedLabels
|
||||
originAnnotations = managedAnnotations
|
||||
}
|
||||
|
||||
originLabels["kubernetes.io/metadata.name"] = ns.GetName()
|
||||
originLabels[capsuleLabel] = tnt.GetName()
|
||||
|
||||
ns.SetLabels(originLabels)
|
||||
ns.SetAnnotations(originAnnotations)
|
||||
|
||||
return managed, err
|
||||
}
|
||||
|
||||
func buildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]string {
|
||||
annotations := make(map[string]string)
|
||||
|
||||
@@ -120,6 +282,35 @@ func buildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]s
|
||||
return annotations
|
||||
}
|
||||
|
||||
func buildNamespaceLabelsForTenant(tnt *capsulev1beta2.Tenant) map[string]string {
|
||||
labels := make(map[string]string)
|
||||
|
||||
if md := tnt.Spec.NamespaceOptions; md != nil && md.AdditionalMetadata != nil {
|
||||
maps.Copy(labels, md.AdditionalMetadata.Labels)
|
||||
}
|
||||
|
||||
if tnt.Spec.Cordoned {
|
||||
labels[meta.CordonedLabel] = "true"
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
func (r *Manager) collectNamespaces(ctx context.Context, tenant *capsulev1beta2.Tenant) (err error) {
|
||||
list := &corev1.NamespaceList{}
|
||||
|
||||
err = r.List(ctx, list, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".metadata.ownerReferences[*].capsule", tenant.GetName()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tenant.AssignNamespaces(list.Items)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// applyTemplateMap applies templating to all values in the provided map in place.
|
||||
func applyTemplateMap(m map[string]string, tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) {
|
||||
for k, v := range m {
|
||||
@@ -136,99 +327,3 @@ func applyTemplateMap(m map[string]string, tnt *capsulev1beta2.Tenant, ns *corev
|
||||
m[k] = tmplString
|
||||
}
|
||||
}
|
||||
|
||||
func buildNamespaceLabelsForTenant(tnt *capsulev1beta2.Tenant) map[string]string {
|
||||
labels := make(map[string]string)
|
||||
|
||||
if md := tnt.Spec.NamespaceOptions; md != nil && md.AdditionalMetadata != nil {
|
||||
maps.Copy(labels, md.AdditionalMetadata.Labels)
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
func (r *Manager) ensureNamespaceCount(ctx context.Context, tenant *capsulev1beta2.Tenant) error {
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
tenant.Status.Size = uint(len(tenant.Status.Namespaces))
|
||||
|
||||
found := &capsulev1beta2.Tenant{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: tenant.GetName()}, found); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
found.Status.Size = tenant.Status.Size
|
||||
|
||||
return r.Client.Status().Update(ctx, found, &client.SubResourceUpdateOptions{})
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Manager) collectNamespaces(ctx context.Context, tenant *capsulev1beta2.Tenant) error {
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
||||
list := &corev1.NamespaceList{}
|
||||
|
||||
err = r.List(ctx, list, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".metadata.ownerReferences[*].capsule", tenant.GetName()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, tenant.DeepCopy(), func() error {
|
||||
tenant.AssignNamespaces(list.Items)
|
||||
|
||||
return r.Client.Status().Update(ctx, tenant, &client.SubResourceUpdateOptions{})
|
||||
})
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// SyncNamespaceMetadata sync namespace metadata according to tenant spec.
|
||||
func SyncNamespaceMetadata(tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) error {
|
||||
capsuleLabel, _ := utils.GetTypeLabel(&capsulev1beta2.Tenant{})
|
||||
|
||||
annotations := buildNamespaceAnnotationsForTenant(tnt)
|
||||
labels := buildNamespaceLabelsForTenant(tnt)
|
||||
|
||||
if opts := tnt.Spec.NamespaceOptions; opts != nil && len(opts.AdditionalMetadataList) > 0 {
|
||||
for _, md := range opts.AdditionalMetadataList {
|
||||
ok, err := utils.IsNamespaceSelectedBySelector(ns, md.NamespaceSelector)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
applyTemplateMap(md.Labels, tnt, ns)
|
||||
applyTemplateMap(md.Annotations, tnt, ns)
|
||||
|
||||
maps.Copy(labels, md.Labels)
|
||||
maps.Copy(annotations, md.Annotations)
|
||||
}
|
||||
}
|
||||
|
||||
labels["kubernetes.io/metadata.name"] = ns.GetName()
|
||||
labels[capsuleLabel] = tnt.GetName()
|
||||
|
||||
if tnt.Spec.Cordoned {
|
||||
ns.Labels[utils.CordonedLabel] = "true"
|
||||
} else {
|
||||
delete(ns.Labels, utils.CordonedLabel)
|
||||
}
|
||||
|
||||
if ns.Annotations == nil {
|
||||
ns.SetAnnotations(annotations)
|
||||
} else {
|
||||
maps.Copy(ns.Annotations, annotations)
|
||||
}
|
||||
|
||||
if ns.Labels == nil {
|
||||
ns.SetLabels(labels)
|
||||
} else {
|
||||
maps.Copy(ns.Labels, labels)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
@@ -23,7 +25,9 @@ type Patch struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
var _ = Describe("enforcing a Container Registry", Label("tenant", "images", "registry"), func() {
|
||||
originConfig := &capsulev1beta2.CapsuleConfiguration{}
|
||||
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "container-registry",
|
||||
@@ -43,13 +47,27 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: defaultConfigurationName}, originConfig)).To(Succeed())
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
tnt.ResourceVersion = ""
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
|
||||
|
||||
// Restore Configuration
|
||||
Eventually(func() error {
|
||||
c := &capsulev1beta2.CapsuleConfiguration{}
|
||||
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: originConfig.Name}, c); err != nil {
|
||||
return err
|
||||
}
|
||||
// Apply the initial configuration from originConfig to c
|
||||
c.Spec = originConfig.Spec
|
||||
return k8sClient.Update(context.Background(), c)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
})
|
||||
|
||||
It("should add labels to Namespace", func() {
|
||||
@@ -71,7 +89,6 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
|
||||
It("should deny running a gcr.io container", func() {
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -86,14 +103,21 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cs := ownerClient(tnt.Spec.Owners[0])
|
||||
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
|
||||
Expect(err).ShouldNot(Succeed())
|
||||
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
|
||||
|
||||
return err
|
||||
}).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
It("should allow using a registry only match", func() {
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -110,10 +134,26 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
}
|
||||
|
||||
cs := ownerClient(tnt.Spec.Owners[0])
|
||||
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
|
||||
|
||||
return err
|
||||
}).Should(Succeed())
|
||||
|
||||
By("verifying the image was correctly mutated", func() {
|
||||
created := &corev1.Pod{}
|
||||
Expect(k8sClient.Get(context.Background(), types.NamespacedName{
|
||||
Namespace: ns.Name,
|
||||
Name: pod.Name,
|
||||
}, created)).To(Succeed())
|
||||
|
||||
Expect(created.Spec.Containers).To(HaveLen(1))
|
||||
Expect(created.Spec.Containers[0].Image).To(Equal("myregistry.azurecr.io/myapp:latest"))
|
||||
})
|
||||
})
|
||||
|
||||
It("should deny patching a not matching registry after applying with a matching (Container)", func() {
|
||||
@@ -144,6 +184,17 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
return err
|
||||
}).Should(Succeed())
|
||||
|
||||
By("verifying the image was correctly mutated", func() {
|
||||
created := &corev1.Pod{}
|
||||
Expect(k8sClient.Get(context.Background(), types.NamespacedName{
|
||||
Namespace: ns.Name,
|
||||
Name: pod.Name,
|
||||
}, created)).To(Succeed())
|
||||
|
||||
Expect(created.Spec.Containers).To(HaveLen(1))
|
||||
Expect(created.Spec.Containers[0].Image).To(Equal("myregistry.azurecr.io/myapp:latest"))
|
||||
})
|
||||
|
||||
Eventually(func() error {
|
||||
payload := []Patch{{
|
||||
Op: "replace",
|
||||
@@ -159,6 +210,89 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
}).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
It("should deny patching a not matching registry after applying with a matching (EphemeralContainer)", func() {
|
||||
ns := NewNamespace("")
|
||||
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "container",
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "container",
|
||||
Image: "docker.io/google-containers/pause-amd64:3.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cs := ownerClient(tnt.Spec.Owners[0])
|
||||
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
role := &rbacv1.Role{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ephemeralcontainers-editor",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""}, // core API group
|
||||
Resources: []string{"pods/ephemeralcontainers"},
|
||||
Verbs: []string{"update", "patch"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rb := &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ephemeralcontainers-editor-binding",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: rbacv1.UserKind,
|
||||
Name: tnt.Spec.Owners[0].Name,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Role",
|
||||
Name: role.Name,
|
||||
},
|
||||
}
|
||||
|
||||
// Create role and binding before test logic
|
||||
Expect(k8sClient.Create(context.TODO(), role)).To(Succeed())
|
||||
Expect(k8sClient.Create(context.TODO(), rb)).To(Succeed())
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
|
||||
|
||||
return err
|
||||
}).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg",
|
||||
Image: "attacker/google-containers/pause-amd64:3.0",
|
||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
It("should deny patching a not matching registry after applying with a matching (initContainer)", func() {
|
||||
ns := NewNamespace("")
|
||||
|
||||
@@ -208,7 +342,50 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
}).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
It("should allow patching a matching registry after applying with a matching (Container)", func() {
|
||||
It("should deny patching a not matching registry after applying with a matching (Container)", func() {
|
||||
ns := NewNamespace("")
|
||||
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "container",
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "container",
|
||||
Image: "myregistry.azurecr.io/myapp:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cs := ownerClient(tnt.Spec.Owners[0])
|
||||
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
|
||||
|
||||
return err
|
||||
}).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
payload := []Patch{{
|
||||
Op: "replace",
|
||||
Path: "/spec/initContainers/0/image",
|
||||
Value: "attacker/google-containers/pause-amd64:3.0",
|
||||
}}
|
||||
payloadBytes, _ := json.Marshal(payload)
|
||||
_, err := cs.CoreV1().Pods(ns.GetName()).Patch(context.TODO(), pod.GetName(), types.JSONPatchType, payloadBytes, metav1.PatchOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
It("should allow patching a matching registry after applying with a matching (EphemeralContainer)", func() {
|
||||
ns := NewNamespace("")
|
||||
|
||||
pod := &corev1.Pod{
|
||||
@@ -230,6 +407,42 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
role := &rbacv1.Role{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ephemeralcontainers-editor",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""}, // core API group
|
||||
Resources: []string{"pods/ephemeralcontainers"},
|
||||
Verbs: []string{"update", "patch"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rb := &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ephemeralcontainers-editor-binding",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: rbacv1.UserKind,
|
||||
Name: tnt.Spec.Owners[0].Name,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Role",
|
||||
Name: role.Name,
|
||||
},
|
||||
}
|
||||
|
||||
// Create role and binding before test logic
|
||||
Expect(k8sClient.Create(context.TODO(), role)).To(Succeed())
|
||||
Expect(k8sClient.Create(context.TODO(), rb)).To(Succeed())
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
|
||||
|
||||
@@ -237,13 +450,17 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
}).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
payload := []Patch{{
|
||||
Op: "replace",
|
||||
Path: "/spec/containers/0/image",
|
||||
Value: "myregistry.azurecr.io/google-containers/pause-amd64:3.1",
|
||||
}}
|
||||
payloadBytes, _ := json.Marshal(payload)
|
||||
_, err := cs.CoreV1().Pods(ns.GetName()).Patch(context.TODO(), pod.GetName(), types.JSONPatchType, payloadBytes, metav1.PatchOptions{})
|
||||
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg",
|
||||
Image: "myregistry.azurecr.io/google-containers/pause-amd64:3.1",
|
||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@ import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
)
|
||||
|
||||
var _ = Describe("enforcing some defined ImagePullPolicy", Label("pod"), func() {
|
||||
var _ = Describe("enforcing some defined ImagePullPolicy", Label("tenant", "images", "policy"), func() {
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "image-pull-policies",
|
||||
@@ -48,6 +49,42 @@ var _ = Describe("enforcing some defined ImagePullPolicy", Label("pod"), func()
|
||||
|
||||
cs := ownerClient(tnt.Spec.Owners[0])
|
||||
|
||||
role := &rbacv1.Role{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ephemeralcontainers-editor",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""}, // core API group
|
||||
Resources: []string{"pods/ephemeralcontainers"},
|
||||
Verbs: []string{"update", "patch"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rb := &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ephemeralcontainers-editor-binding",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: rbacv1.UserKind,
|
||||
Name: tnt.Spec.Owners[0].Name,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Role",
|
||||
Name: role.Name,
|
||||
},
|
||||
}
|
||||
|
||||
// Create role and binding before test logic
|
||||
Expect(k8sClient.Create(context.TODO(), role)).To(Succeed())
|
||||
Expect(k8sClient.Create(context.TODO(), rb)).To(Succeed())
|
||||
|
||||
By("allowing Always", func() {
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -69,6 +106,25 @@ var _ = Describe("enforcing some defined ImagePullPolicy", Label("pod"), func()
|
||||
|
||||
return
|
||||
}).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg",
|
||||
Image: "gcr.io/google_containers/pause-amd64:3.0",
|
||||
ImagePullPolicy: corev1.PullAlways,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}).Should(Succeed())
|
||||
|
||||
})
|
||||
|
||||
By("allowing IfNotPresent", func() {
|
||||
@@ -92,6 +148,24 @@ var _ = Describe("enforcing some defined ImagePullPolicy", Label("pod"), func()
|
||||
|
||||
return
|
||||
}).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg",
|
||||
Image: "gcr.io/google_containers/pause-amd64:3.0",
|
||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}).Should(Succeed())
|
||||
})
|
||||
|
||||
By("blocking Never", func() {
|
||||
@@ -115,6 +189,25 @@ var _ = Describe("enforcing some defined ImagePullPolicy", Label("pod"), func()
|
||||
|
||||
return
|
||||
}).ShouldNot(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg",
|
||||
Image: "gcr.io/google_containers/pause-amd64:3.0",
|
||||
ImagePullPolicy: corev1.PullNever,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}).ShouldNot(Succeed())
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,13 +9,14 @@ import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
)
|
||||
|
||||
var _ = Describe("enforcing a defined ImagePullPolicy", Label("pod"), func() {
|
||||
var _ = Describe("enforcing a defined ImagePullPolicy", Label("tenant", "images", "policy"), func() {
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "image-pull-policy",
|
||||
@@ -48,6 +49,42 @@ var _ = Describe("enforcing a defined ImagePullPolicy", Label("pod"), func() {
|
||||
|
||||
cs := ownerClient(tnt.Spec.Owners[0])
|
||||
|
||||
role := &rbacv1.Role{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ephemeralcontainers-editor",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""}, // core API group
|
||||
Resources: []string{"pods/ephemeralcontainers"},
|
||||
Verbs: []string{"update", "patch"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rb := &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ephemeralcontainers-editor-binding",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: rbacv1.UserKind,
|
||||
Name: tnt.Spec.Owners[0].Name,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Role",
|
||||
Name: role.Name,
|
||||
},
|
||||
}
|
||||
|
||||
// Create role and binding before test logic
|
||||
Expect(k8sClient.Create(context.TODO(), role)).To(Succeed())
|
||||
Expect(k8sClient.Create(context.TODO(), rb)).To(Succeed())
|
||||
|
||||
By("allowing Always", func() {
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -69,6 +106,24 @@ var _ = Describe("enforcing a defined ImagePullPolicy", Label("pod"), func() {
|
||||
|
||||
return
|
||||
}).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg",
|
||||
Image: "gcr.io/google_containers/pause-amd64:3.0",
|
||||
ImagePullPolicy: corev1.PullAlways,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}).Should(Succeed())
|
||||
})
|
||||
|
||||
By("blocking IfNotPresent", func() {
|
||||
@@ -92,6 +147,24 @@ var _ = Describe("enforcing a defined ImagePullPolicy", Label("pod"), func() {
|
||||
|
||||
return
|
||||
}).ShouldNot(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg",
|
||||
Image: "gcr.io/google_containers/pause-amd64:3.0",
|
||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
By("blocking Never", func() {
|
||||
@@ -115,6 +188,24 @@ var _ = Describe("enforcing a defined ImagePullPolicy", Label("pod"), func() {
|
||||
|
||||
return
|
||||
}).ShouldNot(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg",
|
||||
Image: "gcr.io/google_containers/pause-amd64:3.0",
|
||||
ImagePullPolicy: corev1.PullNever,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}).ShouldNot(Succeed())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,14 +8,17 @@ import (
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
)
|
||||
|
||||
var _ = Describe("creating a Namespace for a Tenant with additional metadata", Label("namespace"), func() {
|
||||
var _ = Describe("creating a Namespace for a Tenant with additional metadata", Label("namespace", "metadata"), func() {
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-metadata",
|
||||
@@ -35,7 +38,16 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
|
||||
Kind: "User",
|
||||
},
|
||||
},
|
||||
NamespaceOptions: &capsulev1beta2.NamespaceOptions{
|
||||
},
|
||||
}
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
It("should contain additional Namespace metadata", func() {
|
||||
By("prepare tenant", func() {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
AdditionalMetadata: &api.AdditionalMetadataSpec{
|
||||
Labels: map[string]string{
|
||||
"k8s.io/custom-label": "foo",
|
||||
@@ -48,20 +60,16 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
|
||||
"clastix.io/custom-annotation": "buzz",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
EventuallyCreation(func() error {
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
})
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
|
||||
})
|
||||
EventuallyCreation(func() error {
|
||||
tnt.ResourceVersion = ""
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
It("should contain additional Namespace metadata", func() {
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
@@ -105,29 +113,10 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("creating a Namespace for a Tenant with additional metadata list", func() {
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-metadata",
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: "cap",
|
||||
Kind: "dummy",
|
||||
Name: "tenant-metadata",
|
||||
UID: "tenant-metadata",
|
||||
},
|
||||
},
|
||||
},
|
||||
Spec: capsulev1beta2.TenantSpec{
|
||||
Owners: capsulev1beta2.OwnerListSpec{
|
||||
{
|
||||
Name: "gatsby",
|
||||
Kind: "User",
|
||||
},
|
||||
},
|
||||
NamespaceOptions: &capsulev1beta2.NamespaceOptions{
|
||||
It("should contain additional Namespace metadata", func() {
|
||||
By("prepare tenant", func() {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{
|
||||
{
|
||||
Labels: map[string]string{
|
||||
@@ -184,20 +173,16 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata lis
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
EventuallyCreation(func() error {
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
})
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
|
||||
})
|
||||
EventuallyCreation(func() error {
|
||||
tnt.ResourceVersion = ""
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
It("should contain additional Namespace metadata", func() {
|
||||
labels := map[string]string{
|
||||
"matching_namespace_label": "matching_namespace_label_value",
|
||||
}
|
||||
@@ -295,6 +280,434 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata lis
|
||||
return
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
It("should contain additional Namespace metadata", func() {
|
||||
By("prepare tenant", func() {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
ManagedMetadataOnly: false,
|
||||
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{
|
||||
{
|
||||
Labels: map[string]string{
|
||||
"clastix.io/custom-label": "bar",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"clastix.io/custom-annotation": "buzz",
|
||||
},
|
||||
},
|
||||
{
|
||||
Labels: map[string]string{
|
||||
"k8s.io/custom-label": "foo",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"k8s.io/custom-annotation": "bizz",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
tnt.ResourceVersion = ""
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
labels := map[string]string{
|
||||
"matching_namespace_label": "matching_namespace_label_value",
|
||||
}
|
||||
|
||||
ns := NewNamespace("", labels)
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
By("checking additional labels", func() {
|
||||
Eventually(func() (ok bool) {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
|
||||
for _, mv := range tnt.Spec.NamespaceOptions.AdditionalMetadataList {
|
||||
for k, v := range mv.Labels {
|
||||
if k == "capsule.clastix.io/tenant" || k == "kubernetes.io/metadata.name" {
|
||||
continue // this label is managed and shouldn't be set by the user
|
||||
}
|
||||
if ok, _ = HaveKeyWithValue(k, v).Match(ns.Labels); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
|
||||
})
|
||||
By("checking managed labels", func() {
|
||||
Eventually(func() (ok bool) {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
|
||||
if ok, _ = HaveKeyWithValue("capsule.clastix.io/tenant", tnt.GetName()).Match(ns.Labels); !ok {
|
||||
return
|
||||
}
|
||||
if ok, _ = HaveKeyWithValue("kubernetes.io/metadata.name", ns.GetName()).Match(ns.Labels); !ok {
|
||||
return
|
||||
}
|
||||
return
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
|
||||
})
|
||||
|
||||
By("checking additional annotations", func() {
|
||||
Eventually(func() (ok bool) {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
|
||||
for _, mv := range tnt.Spec.NamespaceOptions.AdditionalMetadataList {
|
||||
for k, v := range mv.Annotations {
|
||||
if ok, _ = HaveKeyWithValue(k, v).Match(ns.Annotations); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
|
||||
})
|
||||
|
||||
By("patching labels and annotations on the Namespace", func() {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).To(Succeed())
|
||||
|
||||
before := ns.DeepCopy()
|
||||
ns.Labels["test-label"] = "test-value"
|
||||
ns.Labels["k8s.io/custom-label"] = "foo-value"
|
||||
ns.Annotations["test-annotation"] = "test-value"
|
||||
ns.Annotations["k8s.io/custom-annotation"] = "bizz-value"
|
||||
|
||||
Expect(k8sClient.Patch(context.TODO(), ns, client.MergeFrom(before))).To(Succeed())
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
By("Add additional annotations (Tenant Owner)", func() {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
|
||||
|
||||
expectedLabels := map[string]string{
|
||||
"test-label": "test-value",
|
||||
"clastix.io/custom-label": "bar",
|
||||
"k8s.io/custom-label": "foo",
|
||||
"matching_namespace_label": "matching_namespace_label_value",
|
||||
"capsule.clastix.io/tenant": tnt.GetName(),
|
||||
"kubernetes.io/metadata.name": ns.GetName(),
|
||||
}
|
||||
|
||||
Eventually(func() map[string]string {
|
||||
got := &corev1.Namespace{}
|
||||
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
|
||||
return nil
|
||||
}
|
||||
ann := got.GetLabels()
|
||||
if ann == nil {
|
||||
ann = map[string]string{}
|
||||
}
|
||||
return ann
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedLabels))
|
||||
|
||||
expectedAnnotations := map[string]string{
|
||||
"test-annotation": "test-value",
|
||||
"clastix.io/custom-annotation": "buzz",
|
||||
"k8s.io/custom-annotation": "bizz",
|
||||
}
|
||||
Eventually(func() map[string]string {
|
||||
got := &corev1.Namespace{}
|
||||
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
|
||||
return nil
|
||||
}
|
||||
ann := got.GetAnnotations()
|
||||
if ann == nil {
|
||||
ann = map[string]string{}
|
||||
}
|
||||
return ann
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedAnnotations))
|
||||
|
||||
By("verify tenant status", func() {
|
||||
condition := tnt.Status.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected tenant condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected tenant condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected tenant condition reason to be Succeeded")
|
||||
})
|
||||
|
||||
By("verify namespace status", func() {
|
||||
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns.GetName(), UID: ns.GetUID()})
|
||||
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
|
||||
|
||||
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(instance.Name).To(Equal(ns.GetName()))
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected namespace condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
|
||||
|
||||
expectedMetadata := &capsulev1beta2.TenantStatusNamespaceMetadata{
|
||||
Labels: map[string]string{
|
||||
"clastix.io/custom-label": "bar",
|
||||
"k8s.io/custom-label": "foo",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"clastix.io/custom-annotation": "buzz",
|
||||
"k8s.io/custom-annotation": "bizz",
|
||||
},
|
||||
}
|
||||
|
||||
Expect(instance.Metadata).To(Equal(expectedMetadata))
|
||||
})
|
||||
})
|
||||
|
||||
By("change managed additional metadata", func() {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
ManagedMetadataOnly: false,
|
||||
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{
|
||||
{
|
||||
Labels: map[string]string{
|
||||
"clastix.io/custom-label": "bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
Annotations: map[string]string{
|
||||
"k8s.io/custom-annotation": "bizz",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
By("verify metadata lifecycle (valid update)", func() {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).To(Succeed())
|
||||
|
||||
expectedLabels := map[string]string{
|
||||
"test-label": "test-value",
|
||||
"clastix.io/custom-label": "bar",
|
||||
"matching_namespace_label": "matching_namespace_label_value",
|
||||
"capsule.clastix.io/tenant": tnt.GetName(),
|
||||
"kubernetes.io/metadata.name": ns.GetName(),
|
||||
}
|
||||
Eventually(func() map[string]string {
|
||||
got := &corev1.Namespace{}
|
||||
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
|
||||
return nil
|
||||
}
|
||||
ann := got.GetLabels()
|
||||
if ann == nil {
|
||||
ann = map[string]string{}
|
||||
}
|
||||
return ann
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedLabels))
|
||||
|
||||
expectedAnnotations := map[string]string{
|
||||
"test-annotation": "test-value",
|
||||
"k8s.io/custom-annotation": "bizz",
|
||||
}
|
||||
Eventually(func() map[string]string {
|
||||
got := &corev1.Namespace{}
|
||||
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
|
||||
return nil
|
||||
}
|
||||
ann := got.GetAnnotations()
|
||||
if ann == nil {
|
||||
ann = map[string]string{}
|
||||
}
|
||||
return ann
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedAnnotations))
|
||||
|
||||
By("verify tenant status", func() {
|
||||
condition := tnt.Status.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected tenant condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected tenant condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected tenant condition reason to be Succeeded")
|
||||
})
|
||||
|
||||
By("verify namespace status", func() {
|
||||
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns.GetName(), UID: ns.GetUID()})
|
||||
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
|
||||
|
||||
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(instance.Name).To(Equal(ns.GetName()))
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected namespace condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
|
||||
|
||||
expectedMetadata := &capsulev1beta2.TenantStatusNamespaceMetadata{
|
||||
Labels: map[string]string{
|
||||
"clastix.io/custom-label": "bar",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"k8s.io/custom-annotation": "bizz",
|
||||
},
|
||||
}
|
||||
|
||||
Expect(instance.Metadata).To(Equal(expectedMetadata))
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
By("change managed additional metadata (provoke an error)", func() {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
ManagedMetadataOnly: false,
|
||||
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{
|
||||
{
|
||||
Labels: map[string]string{
|
||||
"clastix.io???custom-label": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
By("verify metadata lifecycle (faulty update)", func() {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).To(Succeed())
|
||||
|
||||
expectedLabels := map[string]string{
|
||||
"test-label": "test-value",
|
||||
"clastix.io/custom-label": "bar",
|
||||
"matching_namespace_label": "matching_namespace_label_value",
|
||||
"capsule.clastix.io/tenant": tnt.GetName(),
|
||||
"kubernetes.io/metadata.name": ns.GetName(),
|
||||
}
|
||||
Eventually(func() map[string]string {
|
||||
got := &corev1.Namespace{}
|
||||
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
|
||||
return nil
|
||||
}
|
||||
ann := got.GetLabels()
|
||||
if ann == nil {
|
||||
ann = map[string]string{}
|
||||
}
|
||||
return ann
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedLabels))
|
||||
|
||||
expectedAnnotations := map[string]string{
|
||||
"test-annotation": "test-value",
|
||||
"k8s.io/custom-annotation": "bizz",
|
||||
}
|
||||
Eventually(func() map[string]string {
|
||||
got := &corev1.Namespace{}
|
||||
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
|
||||
return nil
|
||||
}
|
||||
ann := got.GetAnnotations()
|
||||
if ann == nil {
|
||||
ann = map[string]string{}
|
||||
}
|
||||
return ann
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedAnnotations))
|
||||
|
||||
By("verify tenant status", func() {
|
||||
condition := tnt.Status.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionFalse), "Expected tenant condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected tenant condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.FailedReason), "Expected tenant condition reason to be Succeeded")
|
||||
})
|
||||
|
||||
By("verify namespace status", func() {
|
||||
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns.GetName(), UID: ns.GetUID()})
|
||||
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
|
||||
|
||||
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(instance.Name).To(Equal(ns.GetName()))
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionFalse), "Expected namespace condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.FailedReason), "Expected namespace condition reason to be Succeeded")
|
||||
|
||||
expectedMetadata := &capsulev1beta2.TenantStatusNamespaceMetadata{
|
||||
Labels: map[string]string{
|
||||
"clastix.io/custom-label": "bar",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"k8s.io/custom-annotation": "bizz",
|
||||
},
|
||||
}
|
||||
|
||||
Expect(instance.Metadata).To(Equal(expectedMetadata))
|
||||
})
|
||||
})
|
||||
|
||||
By("change managed additional metadata (empty update)", func() {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
ManagedMetadataOnly: false,
|
||||
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{},
|
||||
}
|
||||
|
||||
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
By("verify metadata lifecycle (empty update)", func() {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).To(Succeed())
|
||||
|
||||
expectedLabels := map[string]string{
|
||||
"test-label": "test-value",
|
||||
"matching_namespace_label": "matching_namespace_label_value",
|
||||
"capsule.clastix.io/tenant": tnt.GetName(),
|
||||
"kubernetes.io/metadata.name": ns.GetName(),
|
||||
}
|
||||
Eventually(func() map[string]string {
|
||||
got := &corev1.Namespace{}
|
||||
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
|
||||
return nil
|
||||
}
|
||||
ann := got.GetLabels()
|
||||
if ann == nil {
|
||||
ann = map[string]string{}
|
||||
}
|
||||
return ann
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedLabels))
|
||||
|
||||
expectedAnnotations := map[string]string{
|
||||
"test-annotation": "test-value",
|
||||
}
|
||||
Eventually(func() map[string]string {
|
||||
got := &corev1.Namespace{}
|
||||
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
|
||||
return nil
|
||||
}
|
||||
ann := got.GetAnnotations()
|
||||
if ann == nil {
|
||||
ann = map[string]string{}
|
||||
}
|
||||
return ann
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedAnnotations))
|
||||
|
||||
By("verify tenant status", func() {
|
||||
condition := tnt.Status.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected tenant condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected tenant condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected tenant condition reason to be Succeeded")
|
||||
})
|
||||
|
||||
By("verify namespace status", func() {
|
||||
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns.GetName(), UID: ns.GetUID()})
|
||||
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
|
||||
|
||||
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(instance.Name).To(Equal(ns.GetName()))
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected namespace condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
|
||||
|
||||
expectedMetadata := &capsulev1beta2.TenantStatusNamespaceMetadata{}
|
||||
Expect(instance.Metadata).To(Equal(expectedMetadata))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
var _ = Describe("creating a Namespace for a Tenant with additional metadata", Label("namespace"), func() {
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-metadata",
|
||||
Name: "tenant-metadata-controller",
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: "cap",
|
||||
@@ -68,6 +68,7 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
|
||||
Expect(ns.Labels).ShouldNot(HaveKeyWithValue("newlabel", "foobazbar"))
|
||||
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
tnt.Spec.NamespaceOptions.AdditionalMetadata.Labels["newlabel"] = "foobazbar"
|
||||
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
|
||||
|
||||
@@ -81,6 +82,7 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
|
||||
Expect(ns.Labels).ShouldNot(HaveKeyWithValue("newannotation", "foobazbar"))
|
||||
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
tnt.Spec.NamespaceOptions.AdditionalMetadata.Annotations["newannotation"] = "foobazbar"
|
||||
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
var _ = Describe("creating a Namespace for a Tenant with additional metadata", Label("namespace"), func() {
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-metadata",
|
||||
Name: "tenant-metadata-webhook",
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: "cap",
|
||||
|
||||
112
e2e/namespace_status_test.go
Normal file
112
e2e/namespace_status_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright 2020-2023 Project Capsule Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
)
|
||||
|
||||
var _ = Describe("creating namespace with status lifecycle", Label("namespace", "status"), func() {
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-status",
|
||||
},
|
||||
Spec: capsulev1beta2.TenantSpec{
|
||||
Owners: capsulev1beta2.OwnerListSpec{
|
||||
{
|
||||
Name: "gatsby",
|
||||
Kind: "User",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
EventuallyCreation(func() error {
|
||||
tnt.ResourceVersion = ""
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
})
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
It("verify namespace lifecycle (functionality)", func() {
|
||||
ns1 := NewNamespace("")
|
||||
By("creating first namespace", func() {
|
||||
NamespaceCreation(ns1, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElements(ns1.GetName()))
|
||||
|
||||
t := &capsulev1beta2.Tenant{}
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
|
||||
|
||||
Expect(tnt.Status.Size).To(Equal(uint(1)))
|
||||
|
||||
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns1.GetName(), UID: ns1.GetUID()})
|
||||
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
|
||||
|
||||
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(instance.Name).To(Equal(ns1.GetName()))
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected namespace condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
|
||||
})
|
||||
|
||||
ns2 := NewNamespace("")
|
||||
By("creating second namespace", func() {
|
||||
NamespaceCreation(ns2, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElements(ns2.GetName()))
|
||||
|
||||
t := &capsulev1beta2.Tenant{}
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
|
||||
|
||||
Expect(tnt.Status.Size).To(Equal(uint(2)))
|
||||
|
||||
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns2.GetName(), UID: ns2.GetUID()})
|
||||
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
|
||||
|
||||
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(instance.Name).To(Equal(ns2.GetName()))
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected namespace condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
|
||||
})
|
||||
|
||||
By("removing first namespace", func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), ns1)).Should(Succeed())
|
||||
|
||||
t := &capsulev1beta2.Tenant{}
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
|
||||
|
||||
Expect(t.Status.Size).To(Equal(uint(1)))
|
||||
|
||||
instance := t.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns1.GetName(), UID: ns1.GetUID()})
|
||||
Expect(instance).To(BeNil(), "Namespace instance should be nil")
|
||||
})
|
||||
|
||||
By("removing second namespace", func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), ns2)).Should(Succeed())
|
||||
|
||||
t := &capsulev1beta2.Tenant{}
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
|
||||
|
||||
Expect(t.Status.Size).To(Equal(uint(0)))
|
||||
|
||||
instance := t.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns2.GetName(), UID: ns2.GetUID()})
|
||||
Expect(instance).To(BeNil(), "Namespace instance should be nil")
|
||||
})
|
||||
})
|
||||
})
|
||||
136
e2e/scalability_test.go
Normal file
136
e2e/scalability_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright 2020-2023 Project Capsule Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
)
|
||||
|
||||
var _ = Describe("verify scalability", Label("scalability"), func() {
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-scalability",
|
||||
},
|
||||
Spec: capsulev1beta2.TenantSpec{
|
||||
Owners: capsulev1beta2.OwnerListSpec{
|
||||
{
|
||||
Name: "gatsby",
|
||||
Kind: "User",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
EventuallyCreation(func() error {
|
||||
tnt.ResourceVersion = ""
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
})
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
It("verify lifecycle (scalability)", func() {
|
||||
const amount = 50
|
||||
|
||||
getTenant := func() *capsulev1beta2.Tenant {
|
||||
t := &capsulev1beta2.Tenant{}
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).To(Succeed())
|
||||
return t
|
||||
}
|
||||
|
||||
waitSize := func(expected uint) {
|
||||
Eventually(func() uint {
|
||||
return getTenant().Status.Size
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expected))
|
||||
}
|
||||
|
||||
waitInstancePresent := func(ns *corev1.Namespace) {
|
||||
Eventually(func() error {
|
||||
t := getTenant()
|
||||
inst := t.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{
|
||||
Name: ns.GetName(),
|
||||
UID: ns.GetUID(),
|
||||
})
|
||||
if inst == nil {
|
||||
return fmt.Errorf("instance not found for ns=%q uid=%q", ns.GetName(), ns.GetUID())
|
||||
}
|
||||
|
||||
condition := inst.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
if inst == nil {
|
||||
return fmt.Errorf("instance not found for ns=%q uid=%q", ns.GetName(), ns.GetUID())
|
||||
}
|
||||
|
||||
if inst.Name != ns.GetName() {
|
||||
return fmt.Errorf("instance.Name=%q, want %q", inst.Name, ns.GetName())
|
||||
}
|
||||
|
||||
cond := inst.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
if cond == nil {
|
||||
return fmt.Errorf("missing %q condition", meta.ReadyCondition)
|
||||
}
|
||||
if cond.Type != meta.ReadyCondition {
|
||||
return fmt.Errorf("cond.Type=%q, want %q", cond.Type, meta.ReadyCondition)
|
||||
}
|
||||
if cond.Status != metav1.ConditionTrue {
|
||||
return fmt.Errorf("cond.Status=%q, want %q", cond.Status, metav1.ConditionTrue)
|
||||
}
|
||||
if cond.Reason != meta.SucceededReason {
|
||||
return fmt.Errorf("cond.Reason=%q, want %q", cond.Reason, meta.SucceededReason)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
}
|
||||
|
||||
waitInstanceAbsent := func(ns *corev1.Namespace) {
|
||||
Eventually(func() bool {
|
||||
t := getTenant()
|
||||
inst := t.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{
|
||||
Name: ns.GetName(),
|
||||
UID: ns.GetUID(),
|
||||
})
|
||||
return inst == nil
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
|
||||
}
|
||||
|
||||
// --- Scale up: create N namespaces and verify Tenant status each time ---
|
||||
namespaces := make([]*corev1.Namespace, 0, amount)
|
||||
for i := 0; i < amount; i++ {
|
||||
ns := NewNamespace(fmt.Sprintf("scale-%d", i))
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
// Expect size bumped to i+1 and instance present
|
||||
waitSize(uint(i + 1))
|
||||
waitInstancePresent(ns)
|
||||
|
||||
namespaces = append(namespaces, ns)
|
||||
}
|
||||
|
||||
// --- Scale down: delete N namespaces and verify Tenant status each time ---
|
||||
for i := 0; i < amount; i++ {
|
||||
ns := namespaces[i]
|
||||
Expect(k8sClient.Delete(context.TODO(), ns)).To(Succeed())
|
||||
|
||||
// Expect size decremented and instance absent
|
||||
waitSize(uint(amount - i - 1))
|
||||
waitInstanceAbsent(ns)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/utils"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
@@ -60,6 +60,19 @@ var _ = Describe("cordoning a Tenant", Label("tenant"), func() {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
By("Verifing Tenant Status", func() {
|
||||
t := &capsulev1beta2.Tenant{}
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
|
||||
|
||||
condition := t.Status.Conditions.GetConditionByType(meta.CordonedCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionFalse), "Expected tenant condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.CordonedCondition), "Expected tenant condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.ActiveReason), "Expected tenant condition reason to be Succeeded")
|
||||
})
|
||||
|
||||
By("creating a Namespace", func() {
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
|
||||
@@ -79,10 +92,22 @@ var _ = Describe("cordoning a Tenant", Label("tenant"), func() {
|
||||
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
|
||||
|
||||
Expect(ns.Labels).To(HaveKey(utils.CordonedLabel))
|
||||
Expect(ns.Labels).To(HaveKey(meta.CordonedLabel))
|
||||
|
||||
})
|
||||
|
||||
By("Verifing Tenant Status", func() {
|
||||
t := &capsulev1beta2.Tenant{}
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
|
||||
|
||||
condition := t.Status.Conditions.GetConditionByType(meta.CordonedCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected tenant condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.CordonedCondition), "Expected tenant condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.CordonedReason), "Expected tenant condition reason to be Succeeded")
|
||||
})
|
||||
|
||||
By("cordoning the Tenant deletion must be blocked", func() {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.Name}, tnt)).Should(Succeed())
|
||||
|
||||
@@ -116,8 +141,20 @@ var _ = Describe("cordoning a Tenant", Label("tenant"), func() {
|
||||
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
|
||||
|
||||
Expect(ns.Labels).ToNot(HaveKey(utils.CordonedLabel))
|
||||
Expect(ns.Labels).ToNot(HaveKey(meta.CordonedLabel))
|
||||
|
||||
})
|
||||
|
||||
By("Verifing Tenant Status", func() {
|
||||
t := &capsulev1beta2.Tenant{}
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
|
||||
|
||||
condition := t.Status.Conditions.GetConditionByType(meta.CordonedCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionFalse), "Expected tenant condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.CordonedCondition), "Expected tenant condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.ActiveReason), "Expected tenant condition reason to be Succeeded")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ type AdditionalMetadataSpec struct {
|
||||
|
||||
type AdditionalMetadataSelectorSpec struct {
|
||||
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
const (
|
||||
// ReadyCondition indicates the resource is ready and fully reconciled.
|
||||
ReadyCondition string = "Ready"
|
||||
CordonedCondition string = "Cordoned"
|
||||
NotReadyCondition string = "NotReady"
|
||||
|
||||
AssignedCondition string = "Assigned"
|
||||
@@ -19,11 +20,105 @@ const (
|
||||
// FailedReason indicates a condition or event observed a failure (Claim Rejected).
|
||||
SucceededReason string = "Succeeded"
|
||||
FailedReason string = "Failed"
|
||||
ActiveReason string = "Active"
|
||||
CordonedReason string = "Cordoned"
|
||||
PoolExhaustedReason string = "PoolExhausted"
|
||||
QueueExhaustedReason string = "QueueExhausted"
|
||||
NamespaceExhaustedReason string = "NamespaceExhausted"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
|
||||
type ConditionList []Condition
|
||||
|
||||
// Adds a condition by type.
|
||||
func (c *ConditionList) GetConditionByType(conditionType string) *Condition {
|
||||
for i := range *c {
|
||||
if (*c)[i].Type == conditionType {
|
||||
return &(*c)[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Adds a condition by type.
|
||||
func (c *ConditionList) UpdateConditionByType(condition Condition) {
|
||||
for i, cond := range *c {
|
||||
if cond.Type == condition.Type {
|
||||
(*c)[i].UpdateCondition(condition)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
*c = append(*c, condition)
|
||||
}
|
||||
|
||||
// Removes a condition by type.
|
||||
func (c *ConditionList) RemoveConditionByType(condition Condition) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
|
||||
filtered := make(ConditionList, 0, len(*c))
|
||||
|
||||
for _, cond := range *c {
|
||||
if cond.Type != condition.Type {
|
||||
filtered = append(filtered, cond)
|
||||
}
|
||||
}
|
||||
|
||||
*c = filtered
|
||||
}
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
type Condition metav1.Condition
|
||||
|
||||
func NewReadyCondition(obj client.Object) Condition {
|
||||
return Condition{
|
||||
Type: ReadyCondition,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: SucceededReason,
|
||||
Message: "reconciled",
|
||||
LastTransitionTime: metav1.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func NewCordonedCondition(obj client.Object) Condition {
|
||||
return Condition{
|
||||
Type: CordonedCondition,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: ActiveReason,
|
||||
Message: "not cordoned",
|
||||
LastTransitionTime: metav1.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Disregards fields like LastTransitionTime and Version, which are not relevant for the API.
|
||||
func (c *Condition) UpdateCondition(condition Condition) (updated bool) {
|
||||
if condition.Type == c.Type &&
|
||||
condition.Status == c.Status &&
|
||||
condition.Reason == c.Reason &&
|
||||
condition.Message == c.Message &&
|
||||
condition.ObservedGeneration == c.ObservedGeneration {
|
||||
return false
|
||||
}
|
||||
|
||||
if condition.Status != c.Status {
|
||||
c.LastTransitionTime = metav1.Now()
|
||||
}
|
||||
|
||||
c.Type = condition.Type
|
||||
c.Status = condition.Status
|
||||
c.Reason = condition.Reason
|
||||
c.Message = condition.Message
|
||||
c.ObservedGeneration = condition.ObservedGeneration
|
||||
c.LastTransitionTime = condition.LastTransitionTime
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func NewBoundCondition(obj client.Object) metav1.Condition {
|
||||
return metav1.Condition{
|
||||
Type: BoundCondition,
|
||||
|
||||
211
pkg/meta/conditions_test.go
Normal file
211
pkg/meta/conditions_test.go
Normal file
@@ -0,0 +1,211 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package meta
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// helper
|
||||
func makeCond(tpe, status, reason, msg string, gen int64) Condition {
|
||||
return Condition{
|
||||
Type: tpe,
|
||||
Status: metav1.ConditionStatus(status),
|
||||
Reason: reason,
|
||||
Message: msg,
|
||||
ObservedGeneration: gen,
|
||||
LastTransitionTime: metav1.NewTime(time.Unix(0, 0)),
|
||||
}
|
||||
}
|
||||
|
||||
func TestConditionList_GetConditionByType(t *testing.T) {
|
||||
t.Run("returns matching condition", func(t *testing.T) {
|
||||
list := ConditionList{
|
||||
makeCond("Ready", "False", "Init", "starting", 1),
|
||||
makeCond("Synced", "True", "Ok", "done", 2),
|
||||
}
|
||||
|
||||
got := list.GetConditionByType("Synced")
|
||||
assert.NotNil(t, got)
|
||||
assert.Equal(t, "Synced", got.Type)
|
||||
assert.Equal(t, metav1.ConditionTrue, got.Status)
|
||||
assert.Equal(t, "Ok", got.Reason)
|
||||
assert.Equal(t, "done", got.Message)
|
||||
})
|
||||
|
||||
t.Run("returns nil when not found", func(t *testing.T) {
|
||||
list := ConditionList{
|
||||
makeCond("Ready", "False", "Init", "starting", 1),
|
||||
}
|
||||
assert.Nil(t, list.GetConditionByType("Missing"))
|
||||
})
|
||||
|
||||
t.Run("returned pointer refers to slice element (not copy)", func(t *testing.T) {
|
||||
list := ConditionList{
|
||||
makeCond("Ready", "False", "Init", "starting", 1),
|
||||
makeCond("Synced", "True", "Ok", "done", 2),
|
||||
}
|
||||
ptr := list.GetConditionByType("Ready")
|
||||
assert.NotNil(t, ptr)
|
||||
|
||||
ptr.Message = "mutated"
|
||||
// This asserts GetConditionByType returns &list[i] (via index),
|
||||
// not &cond where cond is the range variable copy.
|
||||
assert.Equal(t, "mutated", list[0].Message)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConditionList_UpdateConditionByType(t *testing.T) {
|
||||
now := metav1.Now()
|
||||
|
||||
t.Run("updates existing condition in place", func(t *testing.T) {
|
||||
list := ConditionList{
|
||||
makeCond("Ready", "False", "Init", "starting", 1),
|
||||
makeCond("Synced", "True", "Ok", "done", 2),
|
||||
}
|
||||
beforeLen := len(list)
|
||||
|
||||
list.UpdateConditionByType(Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Reconciled",
|
||||
Message: "ready now",
|
||||
ObservedGeneration: 3,
|
||||
LastTransitionTime: now,
|
||||
})
|
||||
|
||||
assert.Equal(t, beforeLen, len(list))
|
||||
got := list.GetConditionByType("Ready")
|
||||
assert.NotNil(t, got)
|
||||
assert.Equal(t, metav1.ConditionTrue, got.Status)
|
||||
assert.Equal(t, "Reconciled", got.Reason)
|
||||
assert.Equal(t, "ready now", got.Message)
|
||||
assert.Equal(t, int64(3), got.ObservedGeneration)
|
||||
})
|
||||
|
||||
t.Run("appends when condition type not present", func(t *testing.T) {
|
||||
list := ConditionList{
|
||||
makeCond("Ready", "True", "Ok", "ready", 1),
|
||||
}
|
||||
beforeLen := len(list)
|
||||
|
||||
list.UpdateConditionByType(Condition{
|
||||
Type: "Synced",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Done",
|
||||
Message: "synced",
|
||||
ObservedGeneration: 2,
|
||||
LastTransitionTime: now,
|
||||
})
|
||||
|
||||
assert.Equal(t, beforeLen+1, len(list))
|
||||
got := list.GetConditionByType("Synced")
|
||||
assert.NotNil(t, got)
|
||||
assert.Equal(t, metav1.ConditionTrue, got.Status)
|
||||
assert.Equal(t, "Done", got.Reason)
|
||||
assert.Equal(t, "synced", got.Message)
|
||||
assert.Equal(t, int64(2), got.ObservedGeneration)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConditionList_RemoveConditionByType(t *testing.T) {
|
||||
t.Run("removes all conditions with matching type", func(t *testing.T) {
|
||||
list := ConditionList{
|
||||
makeCond("A", "True", "x", "m1", 1),
|
||||
makeCond("B", "True", "y", "m2", 1),
|
||||
makeCond("A", "False", "z", "m3", 2),
|
||||
}
|
||||
list.RemoveConditionByType(Condition{Type: "A"})
|
||||
|
||||
assert.Len(t, list, 1)
|
||||
assert.Equal(t, "B", list[0].Type)
|
||||
})
|
||||
|
||||
t.Run("no-op when type not present", func(t *testing.T) {
|
||||
orig := ConditionList{
|
||||
makeCond("A", "True", "x", "m1", 1),
|
||||
}
|
||||
list := append(ConditionList{}, orig...) // copy
|
||||
|
||||
list.RemoveConditionByType(Condition{Type: "Missing"})
|
||||
|
||||
assert.Equal(t, orig, list)
|
||||
})
|
||||
|
||||
t.Run("nil receiver is safe", func(t *testing.T) {
|
||||
var list *ConditionList // nil receiver
|
||||
assert.NotPanics(t, func() {
|
||||
list.RemoveConditionByType(Condition{Type: "X"})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateCondition(t *testing.T) {
|
||||
now := metav1.Now()
|
||||
|
||||
t.Run("no update when all relevant fields match", func(t *testing.T) {
|
||||
c := &Condition{
|
||||
Type: "Ready",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "All good",
|
||||
}
|
||||
|
||||
updated := c.UpdateCondition(Condition{
|
||||
Type: "Ready",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "All good",
|
||||
LastTransitionTime: now,
|
||||
})
|
||||
|
||||
assert.False(t, updated)
|
||||
})
|
||||
|
||||
t.Run("update occurs on message change", func(t *testing.T) {
|
||||
c := &Condition{
|
||||
Type: "Ready",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "Old message",
|
||||
}
|
||||
|
||||
updated := c.UpdateCondition(Condition{
|
||||
Type: "Ready",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "New message",
|
||||
LastTransitionTime: now,
|
||||
})
|
||||
|
||||
assert.True(t, updated)
|
||||
assert.Equal(t, "New message", c.Message)
|
||||
})
|
||||
|
||||
t.Run("update occurs on status change", func(t *testing.T) {
|
||||
c := &Condition{
|
||||
Type: "Ready",
|
||||
Status: "False",
|
||||
Reason: "Pending",
|
||||
Message: "Not ready yet",
|
||||
}
|
||||
|
||||
updated := c.UpdateCondition(Condition{
|
||||
Type: "Ready",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "Ready",
|
||||
LastTransitionTime: now,
|
||||
})
|
||||
|
||||
assert.True(t, updated)
|
||||
assert.Equal(t, "True", string(c.Status))
|
||||
assert.Equal(t, "Success", c.Reason)
|
||||
assert.Equal(t, "Ready", c.Message)
|
||||
})
|
||||
}
|
||||
@@ -15,6 +15,9 @@ const (
|
||||
|
||||
OwnerPromotionLabel = "owner.projectcapsule.dev/promote"
|
||||
OwnerPromotionLabelTrigger = "true"
|
||||
|
||||
CordonedLabel = "projectcapsule.dev/cordoned"
|
||||
CordonedLabelTrigger = "true"
|
||||
)
|
||||
|
||||
func FreezeLabelTriggers(obj client.Object) bool {
|
||||
|
||||
47
pkg/meta/zz_generated.deepcopy.go
Normal file
47
pkg/meta/zz_generated.deepcopy.go
Normal file
@@ -0,0 +1,47 @@
|
||||
//go:build !ignore_autogenerated
|
||||
|
||||
// Copyright 2020-2023 Project Capsule Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package meta
|
||||
|
||||
import ()
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Condition) DeepCopyInto(out *Condition) {
|
||||
*out = *in
|
||||
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition.
|
||||
func (in *Condition) DeepCopy() *Condition {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Condition)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in ConditionList) DeepCopyInto(out *ConditionList) {
|
||||
{
|
||||
in := &in
|
||||
*out = make(ConditionList, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConditionList.
|
||||
func (in ConditionList) DeepCopy() ConditionList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ConditionList)
|
||||
in.DeepCopyInto(out)
|
||||
return *out
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
|
||||
type TenantRecorder struct {
|
||||
TenantNamespaceRelationshipGauge *prometheus.GaugeVec
|
||||
TenantNamespaceConditionGauge *prometheus.GaugeVec
|
||||
TenantConditionGauge *prometheus.GaugeVec
|
||||
TenantCordonedStatusGauge *prometheus.GaugeVec
|
||||
TenantNamespaceCounterGauge *prometheus.GaugeVec
|
||||
TenantResourceUsageGauge *prometheus.GaugeVec
|
||||
@@ -32,11 +34,26 @@ func NewTenantRecorder() *TenantRecorder {
|
||||
Help: "Mapping metric showing namespace to tenant relationships",
|
||||
}, []string{"tenant", "namespace"},
|
||||
),
|
||||
TenantNamespaceConditionGauge: prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: metricsPrefix,
|
||||
Name: "tenant_namespace_condition",
|
||||
Help: "Provides per namespace within a tenant condition status for each condition",
|
||||
}, []string{"tenant", "namespace", "condition"},
|
||||
),
|
||||
|
||||
TenantConditionGauge: prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: metricsPrefix,
|
||||
Name: "tenant_condition",
|
||||
Help: "Provides per tenant condition status for each condition",
|
||||
}, []string{"tenant", "condition"},
|
||||
),
|
||||
TenantCordonedStatusGauge: prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: metricsPrefix,
|
||||
Name: "tenant_status",
|
||||
Help: "Tenant cordon state indicating if tenant operations are restricted (1) or allowed (0) for resource creation and modification",
|
||||
Help: "DEPRECATED: Tenant cordon state indicating if tenant operations are restricted (1) or allowed (0) for resource creation and modification",
|
||||
}, []string{"tenant"},
|
||||
),
|
||||
TenantNamespaceCounterGauge: prometheus.NewGaugeVec(
|
||||
@@ -66,6 +83,8 @@ func NewTenantRecorder() *TenantRecorder {
|
||||
func (r *TenantRecorder) Collectors() []prometheus.Collector {
|
||||
return []prometheus.Collector{
|
||||
r.TenantNamespaceRelationshipGauge,
|
||||
r.TenantNamespaceConditionGauge,
|
||||
r.TenantConditionGauge,
|
||||
r.TenantCordonedStatusGauge,
|
||||
r.TenantNamespaceCounterGauge,
|
||||
r.TenantResourceUsageGauge,
|
||||
@@ -73,6 +92,51 @@ func (r *TenantRecorder) Collectors() []prometheus.Collector {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TenantRecorder) DeleteAllMetricsForNamespace(namespace string) {
|
||||
r.DeleteNamespaceRelationshipMetrics(namespace)
|
||||
r.DeleteTenantNamespaceConditionMetrics(namespace)
|
||||
}
|
||||
|
||||
// DeleteCondition deletes the condition metrics for the ref.
|
||||
func (r *TenantRecorder) DeleteNamespaceRelationshipMetrics(namespace string) {
|
||||
r.TenantNamespaceRelationshipGauge.DeletePartialMatch(map[string]string{
|
||||
"namespace": namespace,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *TenantRecorder) DeleteTenantNamespaceConditionMetrics(namespace string) {
|
||||
r.TenantNamespaceConditionGauge.DeletePartialMatch(map[string]string{
|
||||
"namespace": namespace,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *TenantRecorder) DeleteTenantNamespaceConditionMetricByType(namespace string, condition string) {
|
||||
r.TenantNamespaceConditionGauge.DeletePartialMatch(map[string]string{
|
||||
"namespace": namespace,
|
||||
"condition": condition,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *TenantRecorder) DeleteAllMetricsForTenant(tenant string) {
|
||||
r.DeleteTenantResourceMetrics(tenant)
|
||||
r.DeleteTenantStatusMetrics(tenant)
|
||||
r.DeleteTenantConditionMetrics(tenant)
|
||||
r.DeleteTenantResourceMetrics(tenant)
|
||||
}
|
||||
|
||||
func (r *TenantRecorder) DeleteTenantConditionMetrics(tenant string) {
|
||||
r.TenantConditionGauge.DeletePartialMatch(map[string]string{
|
||||
"tenant": tenant,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *TenantRecorder) DeleteTenantConditionMetricByType(tenant string, condition string) {
|
||||
r.TenantConditionGauge.DeletePartialMatch(map[string]string{
|
||||
"tenant": tenant,
|
||||
"condition": condition,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteCondition deletes the condition metrics for the ref.
|
||||
func (r *TenantRecorder) DeleteTenantResourceMetrics(tenant string) {
|
||||
r.TenantResourceUsageGauge.DeletePartialMatch(map[string]string{
|
||||
@@ -85,25 +149,16 @@ func (r *TenantRecorder) DeleteTenantResourceMetrics(tenant string) {
|
||||
|
||||
// DeleteCondition deletes the condition metrics for the ref.
|
||||
func (r *TenantRecorder) DeleteTenantStatusMetrics(tenant string) {
|
||||
r.TenantNamespaceCounterGauge.DeletePartialMatch(map[string]string{
|
||||
"tenant": tenant,
|
||||
})
|
||||
r.TenantCordonedStatusGauge.DeletePartialMatch(map[string]string{
|
||||
"tenant": tenant,
|
||||
})
|
||||
r.TenantNamespaceRelationshipGauge.DeletePartialMatch(map[string]string{
|
||||
"tenant": tenant,
|
||||
})
|
||||
r.TenantResourceUsageGauge.DeletePartialMatch(map[string]string{
|
||||
"tenant": tenant,
|
||||
})
|
||||
r.TenantResourceLimitGauge.DeletePartialMatch(map[string]string{
|
||||
r.TenantNamespaceConditionGauge.DeletePartialMatch(map[string]string{
|
||||
"tenant": tenant,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteCondition deletes the condition metrics for the ref.
|
||||
func (r *TenantRecorder) DeleteNamespaceRelationshipMetrics(namespace string) {
|
||||
r.TenantNamespaceRelationshipGauge.DeletePartialMatch(map[string]string{
|
||||
"namespace": namespace,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *TenantRecorder) DeleteAllMetrics(tenant string) {
|
||||
r.DeleteTenantResourceMetrics(tenant)
|
||||
r.DeleteTenantStatusMetrics(tenant)
|
||||
}
|
||||
|
||||
16
pkg/utils/maps.go
Normal file
16
pkg/utils/maps.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package utils
|
||||
|
||||
func MapMergeNoOverrite(dst, src map[string]string) {
|
||||
if len(src) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range src {
|
||||
if _, exists := dst[k]; !exists {
|
||||
dst[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
98
pkg/utils/maps_test.go
Normal file
98
pkg/utils/maps_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMapMergeNoOverrite_AddsNonOverlapping(t *testing.T) {
|
||||
dst := map[string]string{"a": "1"}
|
||||
src := map[string]string{"b": "2"}
|
||||
|
||||
MapMergeNoOverrite(dst, src)
|
||||
|
||||
if got, want := dst["a"], "1"; got != want {
|
||||
t.Fatalf("dst[a] = %q, want %q", got, want)
|
||||
}
|
||||
if got, want := dst["b"], "2"; got != want {
|
||||
t.Fatalf("dst[b] = %q, want %q", got, want)
|
||||
}
|
||||
if len(dst) != 2 {
|
||||
t.Fatalf("len(dst) = %d, want 2", len(dst))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapMergeNoOverrite_DoesNotOverwriteExisting(t *testing.T) {
|
||||
dst := map[string]string{"a": "1"}
|
||||
src := map[string]string{"a": "X"} // overlapping key
|
||||
|
||||
MapMergeNoOverrite(dst, src)
|
||||
|
||||
if got, want := dst["a"], "1"; got != want {
|
||||
t.Fatalf("dst[a] overwritten: got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapMergeNoOverrite_EmptySrc_NoChange(t *testing.T) {
|
||||
dst := map[string]string{"a": "1"}
|
||||
src := map[string]string{} // empty
|
||||
|
||||
before := make(map[string]string, len(dst))
|
||||
for k, v := range dst {
|
||||
before[k] = v
|
||||
}
|
||||
|
||||
MapMergeNoOverrite(dst, src)
|
||||
|
||||
if !reflect.DeepEqual(dst, before) {
|
||||
t.Fatalf("dst changed with empty src: got %#v, want %#v", dst, before)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapMergeNoOverrite_NilSrc_NoChange(t *testing.T) {
|
||||
dst := map[string]string{"a": "1"}
|
||||
var src map[string]string // nil
|
||||
|
||||
before := make(map[string]string, len(dst))
|
||||
for k, v := range dst {
|
||||
before[k] = v
|
||||
}
|
||||
|
||||
MapMergeNoOverrite(dst, src)
|
||||
|
||||
if !reflect.DeepEqual(dst, before) {
|
||||
t.Fatalf("dst changed with nil src: got %#v, want %#v", dst, before)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapMergeNoOverrite_Idempotent(t *testing.T) {
|
||||
dst := map[string]string{"a": "1"}
|
||||
src := map[string]string{"b": "2"}
|
||||
|
||||
MapMergeNoOverrite(dst, src)
|
||||
first := map[string]string{}
|
||||
for k, v := range dst {
|
||||
first[k] = v
|
||||
}
|
||||
|
||||
// Call again; result should be identical
|
||||
MapMergeNoOverrite(dst, src)
|
||||
|
||||
if !reflect.DeepEqual(dst, first) {
|
||||
t.Fatalf("non-idempotent merge: after second merge got %#v, want %#v", dst, first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapMergeNoOverrite_NilDst_Panics(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Fatalf("expected panic when dst is nil, but did not panic")
|
||||
}
|
||||
}()
|
||||
|
||||
var dst map[string]string // nil destination map
|
||||
src := map[string]string{"a": "1"}
|
||||
|
||||
// Writing to a nil map panics; document current behavior via this test.
|
||||
MapMergeNoOverrite(dst, src)
|
||||
}
|
||||
@@ -15,10 +15,6 @@ import (
|
||||
"github.com/projectcapsule/capsule/api/v1beta2"
|
||||
)
|
||||
|
||||
const (
|
||||
CordonedLabel = "projectcapsule.dev/cordoned"
|
||||
)
|
||||
|
||||
func GetTypeLabel(t runtime.Object) (label string, err error) {
|
||||
switch v := t.(type) {
|
||||
case *v1beta1.Tenant, *v1beta2.Tenant:
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
capsuleutils "github.com/projectcapsule/capsule/pkg/utils"
|
||||
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
@@ -74,16 +76,21 @@ func (h *cordoningLabelHandler) syncNamespaceCordonLabel(ctx context.Context, c
|
||||
}
|
||||
}
|
||||
|
||||
if !tnt.Spec.Cordoned {
|
||||
condition := tnt.Status.Conditions.GetConditionByType(meta.CordonedCondition)
|
||||
if condition == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if condition.Status != metav1.ConditionTrue {
|
||||
return nil
|
||||
}
|
||||
|
||||
labels := ns.GetLabels()
|
||||
if _, ok := labels[capsuleutils.CordonedLabel]; ok {
|
||||
if _, ok := labels[meta.CordonedLabel]; ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
ns.Labels[capsuleutils.CordonedLabel] = "true"
|
||||
ns.Labels[meta.CordonedLabel] = "true"
|
||||
|
||||
marshaled, err := json.Marshal(ns)
|
||||
if err != nil {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
capsuletenant "github.com/projectcapsule/capsule/controllers/tenant"
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
|
||||
)
|
||||
@@ -49,12 +49,29 @@ func (h *metadataHandler) OnCreate(client client.Client, decoder admission.Decod
|
||||
}
|
||||
|
||||
// sync namespace metadata
|
||||
if err := capsuletenant.SyncNamespaceMetadata(tenant, ns); err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
instance := tenant.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{
|
||||
Name: ns.GetName(),
|
||||
UID: ns.GetUID(),
|
||||
})
|
||||
|
||||
return &response
|
||||
if len(instance.Metadata.Labels) == 0 && len(instance.Metadata.Annotations) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
labels := ns.GetLabels()
|
||||
for k, v := range instance.Metadata.Labels {
|
||||
labels[k] = v
|
||||
}
|
||||
|
||||
ns.SetLabels(labels)
|
||||
|
||||
annotations := ns.GetAnnotations()
|
||||
for k, v := range instance.Metadata.Annotations {
|
||||
annotations[k] = v
|
||||
}
|
||||
|
||||
ns.SetAnnotations(annotations)
|
||||
|
||||
marshaled, err := json.Marshal(ns)
|
||||
if err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
@@ -13,14 +13,19 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type containerRegistryHandler struct{}
|
||||
type containerRegistryHandler struct {
|
||||
configuration configuration.Configuration
|
||||
}
|
||||
|
||||
func ContainerRegistry() capsulewebhook.Handler {
|
||||
return &containerRegistryHandler{}
|
||||
func ContainerRegistry(configuration configuration.Configuration) capsulewebhook.Handler {
|
||||
return &containerRegistryHandler{
|
||||
configuration: configuration,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *containerRegistryHandler) OnCreate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
@@ -42,7 +47,13 @@ func (h *containerRegistryHandler) OnUpdate(c client.Client, decoder admission.D
|
||||
}
|
||||
}
|
||||
|
||||
func (h *containerRegistryHandler) validate(ctx context.Context, c client.Client, decoder admission.Decoder, recorder record.EventRecorder, req admission.Request) *admission.Response {
|
||||
func (h *containerRegistryHandler) validate(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
req admission.Request,
|
||||
) *admission.Response {
|
||||
pod := &corev1.Pod{}
|
||||
if err := decoder.Decode(req, pod); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
@@ -61,34 +72,45 @@ func (h *containerRegistryHandler) validate(ctx context.Context, c client.Client
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
if tnt.Spec.ContainerRegistries != nil {
|
||||
// Evaluate init containers
|
||||
for _, container := range pod.Spec.InitContainers {
|
||||
if response := h.verifyContainerRegistry(recorder, req, container, tnt); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
if tnt.Spec.ContainerRegistries == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Evaluate containers
|
||||
for _, container := range pod.Spec.Containers {
|
||||
if response := h.verifyContainerRegistry(recorder, req, container, tnt); response != nil {
|
||||
return response
|
||||
}
|
||||
for _, container := range pod.Spec.InitContainers {
|
||||
if response := h.verifyContainerRegistry(recorder, req, container.Image, tnt); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
for _, container := range pod.Spec.EphemeralContainers {
|
||||
if response := h.verifyContainerRegistry(recorder, req, container.Image, tnt); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
for _, container := range pod.Spec.Containers {
|
||||
if response := h.verifyContainerRegistry(recorder, req, container.Image, tnt); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *containerRegistryHandler) verifyContainerRegistry(recorder record.EventRecorder, req admission.Request, container corev1.Container, tnt capsulev1beta2.Tenant) *admission.Response {
|
||||
func (h *containerRegistryHandler) verifyContainerRegistry(
|
||||
recorder record.EventRecorder,
|
||||
req admission.Request,
|
||||
image string,
|
||||
tnt capsulev1beta2.Tenant,
|
||||
) *admission.Response {
|
||||
var valid, matched bool
|
||||
|
||||
reg := NewRegistry(container.Image)
|
||||
reg := NewRegistry(image, h.configuration)
|
||||
|
||||
if len(reg.Registry()) == 0 {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "MissingFQCI", "Pod %s/%s is not using a fully qualified container image, cannot enforce registry the current Tenant", req.Namespace, req.Name, reg.Registry())
|
||||
|
||||
response := admission.Denied(NewContainerRegistryForbidden(container.Image, *tnt.Spec.ContainerRegistries).Error())
|
||||
response := admission.Denied(NewContainerRegistryForbidden(image, *tnt.Spec.ContainerRegistries).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
@@ -100,7 +122,7 @@ func (h *containerRegistryHandler) verifyContainerRegistry(recorder record.Event
|
||||
if !valid && !matched {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenContainerRegistry", "Pod %s/%s is using a container hosted on registry %s that is forbidden for the current Tenant", req.Namespace, req.Name, reg.Registry())
|
||||
|
||||
response := admission.Denied(NewContainerRegistryForbidden(container.Image, *tnt.Spec.ContainerRegistries).Error())
|
||||
response := admission.Denied(NewContainerRegistryForbidden(reg.FQCI(), *tnt.Spec.ContainerRegistries).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
package pod
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
)
|
||||
|
||||
type registry map[string]string
|
||||
@@ -49,14 +53,46 @@ func (r registry) Tag() string {
|
||||
return res
|
||||
}
|
||||
|
||||
func (r registry) FQCI() string {
|
||||
reg := r.Registry()
|
||||
repo := r.Repository()
|
||||
img := r.Image()
|
||||
tag := r.Tag()
|
||||
|
||||
// If there's no image, nothing to return
|
||||
if img == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ensure repo ends with "/" if set
|
||||
if repo != "" && repo[len(repo)-1] != '/' {
|
||||
repo += "/"
|
||||
}
|
||||
|
||||
// always append tag to image (strip any trailing : from image just in case)
|
||||
// but our Image() already includes the name:tag, so split carefully
|
||||
name := img
|
||||
if tag != "" && !strings.Contains(img, ":") {
|
||||
name = fmt.Sprintf("%s:%s", img, tag)
|
||||
}
|
||||
|
||||
// build: [registry/]repo+image
|
||||
if reg != "" {
|
||||
return fmt.Sprintf("%s/%s%s", reg, repo, name)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s", repo, name)
|
||||
}
|
||||
|
||||
type Registry interface {
|
||||
Registry() string
|
||||
Repository() string
|
||||
Image() string
|
||||
Tag() string
|
||||
FQCI() string
|
||||
}
|
||||
|
||||
func NewRegistry(value string) Registry {
|
||||
func NewRegistry(value string, cfg configuration.Configuration) Registry {
|
||||
reg := make(registry)
|
||||
r := regexp.MustCompile(`((?P<registry>[a-zA-Z0-9-._]+(:\d+)?)\/)?(?P<repository>.*\/)?(?P<image>[a-zA-Z0-9-._]+:(?P<tag>[a-zA-Z0-9-._]+))?`)
|
||||
match := r.FindStringSubmatch(value)
|
||||
|
||||
@@ -25,49 +25,13 @@ func ImagePullPolicy() capsulewebhook.Handler {
|
||||
|
||||
func (r *imagePullPolicy) OnCreate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
pod := &corev1.Pod{}
|
||||
if err := decoder.Decode(req, pod); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tntList := &capsulev1beta2.TenantList{}
|
||||
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace),
|
||||
}); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
// the Pod is not running in a Namespace managed by a Tenant
|
||||
if len(tntList.Items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
policy := NewPullPolicy(&tnt)
|
||||
// if Tenant doesn't enforce the pull policy, exit
|
||||
if policy == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, container := range pod.Spec.Containers {
|
||||
usedPullPolicy := string(container.ImagePullPolicy)
|
||||
|
||||
if !policy.IsPolicySupported(usedPullPolicy) {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenPullPolicy", "Pod %s/%s pull policy %s is forbidden for the current Tenant", req.Namespace, req.Name, usedPullPolicy)
|
||||
|
||||
response := admission.Denied(NewImagePullPolicyForbidden(usedPullPolicy, container.Name, policy.AllowedPullPolicies()).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return r.validate(ctx, c, decoder, recorder, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *imagePullPolicy) OnUpdate(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
func (r *imagePullPolicy) OnUpdate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return r.validate(ctx, c, decoder, recorder, req)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,3 +40,73 @@ func (r *imagePullPolicy) OnDelete(client.Client, admission.Decoder, record.Even
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *imagePullPolicy) validate(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
req admission.Request,
|
||||
) *admission.Response {
|
||||
pod := &corev1.Pod{}
|
||||
if err := decoder.Decode(req, pod); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tntList := &capsulev1beta2.TenantList{}
|
||||
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace),
|
||||
}); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if len(tntList.Items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
policy := NewPullPolicy(&tnt)
|
||||
if policy == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, container := range pod.Spec.InitContainers {
|
||||
if response := h.verifyPullPolicy(recorder, req, policy, string(container.ImagePullPolicy), container.Name, tnt); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
for _, container := range pod.Spec.EphemeralContainers {
|
||||
if response := h.verifyPullPolicy(recorder, req, policy, string(container.ImagePullPolicy), container.Name, tnt); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
for _, container := range pod.Spec.Containers {
|
||||
if response := h.verifyPullPolicy(recorder, req, policy, string(container.ImagePullPolicy), container.Name, tnt); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *imagePullPolicy) verifyPullPolicy(
|
||||
recorder record.EventRecorder,
|
||||
req admission.Request,
|
||||
policy PullPolicy,
|
||||
usedPullPolicy string,
|
||||
container string,
|
||||
tnt capsulev1beta2.Tenant,
|
||||
) *admission.Response {
|
||||
if !policy.IsPolicySupported(usedPullPolicy) {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenPullPolicy", "Pod %s/%s pull policy %s is forbidden for the current Tenant", req.Namespace, req.Name, usedPullPolicy)
|
||||
|
||||
response := admission.Denied(NewImagePullPolicyForbidden(usedPullPolicy, container, policy.AllowedPullPolicies()).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,18 +7,34 @@ import (
|
||||
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
type tenant struct {
|
||||
type tenantValidating struct {
|
||||
handlers []capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Tenant(handler ...capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &tenant{handlers: handler}
|
||||
func TenantValidating(handler ...capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &tenantValidating{handlers: handler}
|
||||
}
|
||||
|
||||
func (w *tenant) GetHandlers() []capsulewebhook.Handler {
|
||||
func (w *tenantValidating) GetHandlers() []capsulewebhook.Handler {
|
||||
return w.handlers
|
||||
}
|
||||
|
||||
func (w *tenant) GetPath() string {
|
||||
return "/tenants"
|
||||
func (w *tenantValidating) GetPath() string {
|
||||
return "/tenants/validating"
|
||||
}
|
||||
|
||||
type tenantMutating struct {
|
||||
handlers []capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func TenantMutating(handler ...capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &tenantMutating{handlers: handler}
|
||||
}
|
||||
|
||||
func (w *tenantMutating) GetHandlers() []capsulewebhook.Handler {
|
||||
return w.handlers
|
||||
}
|
||||
|
||||
func (w *tenantMutating) GetPath() string {
|
||||
return "/tenants/mutating"
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
capsuleapi "github.com/projectcapsule/capsule/pkg/api"
|
||||
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type metaHandler struct{}
|
||||
|
||||
func MetaHandler() capsulewebhook.Handler {
|
||||
return &metaHandler{}
|
||||
}
|
||||
|
||||
func (h *metaHandler) OnCreate(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *metaHandler) OnDelete(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *metaHandler) OnUpdate(_ client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(_ context.Context, req admission.Request) *admission.Response {
|
||||
tenant := &capsulev1beta2.Tenant{}
|
||||
if err := decoder.Decode(req, tenant); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tenant.Labels != nil {
|
||||
if tenant.Labels[capsuleapi.TenantNameLabel] != "" {
|
||||
if tenant.Labels[capsuleapi.TenantNameLabel] != tenant.Name {
|
||||
response := admission.Denied(fmt.Sprintf("tenant label '%s' is immutable", capsuleapi.TenantNameLabel))
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
73
pkg/webhook/tenant/mutation/metadata.go
Normal file
73
pkg/webhook/tenant/mutation/metadata.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mutation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
capsuleapi "github.com/projectcapsule/capsule/pkg/api"
|
||||
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type metaHandler struct{}
|
||||
|
||||
func MetaHandler() capsulewebhook.Handler {
|
||||
return &metaHandler{}
|
||||
}
|
||||
|
||||
func (h *metaHandler) OnCreate(_ client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(_ context.Context, req admission.Request) *admission.Response {
|
||||
return h.handle(decoder, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *metaHandler) OnUpdate(_ client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
|
||||
return func(_ context.Context, req admission.Request) *admission.Response {
|
||||
return h.handle(decoder, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *metaHandler) OnDelete(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *metaHandler) handle(decoder admission.Decoder, req admission.Request) *admission.Response {
|
||||
tenant := &capsulev1beta2.Tenant{}
|
||||
if err := decoder.Decode(req, tenant); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
labels := tenant.GetLabels()
|
||||
if val, ok := labels[capsuleapi.TenantNameLabel]; ok && val == tenant.Name {
|
||||
return nil
|
||||
}
|
||||
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
|
||||
labels[capsuleapi.TenantNameLabel] = tenant.Name
|
||||
tenant.SetLabels(labels)
|
||||
|
||||
marshaled, err := json.Marshal(tenant)
|
||||
if err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
response := admission.PatchResponseFromRaw(req.Object.Raw, marshaled)
|
||||
|
||||
return &response
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//nolint:dupl
|
||||
package tenant
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
package validation
|
||||
|
||||
import "fmt"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//nolint:dupl
|
||||
package tenant
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//nolint:dupl
|
||||
package tenant
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenant
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//nolint:dupl
|
||||
package tenant
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
Reference in New Issue
Block a user