From 7d899b970405a6e87539e1fbfb503dd9e3c1abb2 Mon Sep 17 00:00:00 2001 From: DQ Date: Fri, 11 Apr 2025 17:13:55 +1000 Subject: [PATCH 1/2] Update GitHub Actions workflow: rename to 'Unit Tests' and adjust permissions for contents and pull-requests --- .github/workflows/test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 96abe10..c00c285 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Tests +name: Unit Tests on: push: @@ -8,6 +8,9 @@ jobs: test: name: Run on Ubuntu runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write steps: - name: Clone the code uses: actions/checkout@v4 From 722359c1ea00e7f3eb9975add9749c1f090349dd Mon Sep 17 00:00:00 2001 From: DQ Date: Fri, 11 Apr 2025 21:42:48 +1000 Subject: [PATCH 2/2] Enhance: Update Operation CRD and Controller Logic - Added ApplicationSpec to the Operation CRD, allowing for multiple applications with provision and teardown job specifications. - Implemented deep copy methods for ApplicationSpec and updated OperationSpec to include validation annotations. - Enhanced the Operation controller to manage application deployments, ensuring all applications are ready and handling finalizer logic. - Introduced unit tests for the Operation adapter and controller to validate functionality and ensure proper handling of application lifecycle events. - Updated dependencies in go.mod, including upgrading testify and adding uuid library for unique operation IDs. --- api/v1/operation_types.go | 25 +- api/v1/zz_generated.deepcopy.go | 40 +- .../crd/bases/app.github.com_operations.yaml | 7578 ++++++++++++++++- go.mod | 5 +- go.sum | 6 +- .../mocks/mock_operation_adapter.go | 117 + internal/controller/operation_adapter.go | 243 + internal/controller/operation_adapter_test.go | 449 + internal/controller/operation_controller.go | 69 +- .../controller/operation_controller_test.go | 162 +- internal/utils/controller/cachekey.go | 90 + internal/utils/controller/cachekey_test.go | 273 + internal/utils/controller/operation/const.go | 17 + .../utils/controller/operation/operation.go | 136 + .../controller/operation/operation_test.go | 141 + internal/utils/controller/operation/status.go | 11 + .../utils/controller/operation/status_test.go | 24 + test/e2e/e2e_test.go | 4 + 18 files changed, 9373 insertions(+), 17 deletions(-) create mode 100644 internal/controller/mocks/mock_operation_adapter.go create mode 100644 internal/controller/operation_adapter.go create mode 100644 internal/controller/operation_adapter_test.go create mode 100644 internal/utils/controller/cachekey.go create mode 100644 internal/utils/controller/cachekey_test.go create mode 100644 internal/utils/controller/operation/const.go create mode 100644 internal/utils/controller/operation/operation.go create mode 100644 internal/utils/controller/operation/operation_test.go create mode 100644 internal/utils/controller/operation/status.go create mode 100644 internal/utils/controller/operation/status_test.go diff --git a/api/v1/operation_types.go b/api/v1/operation_types.go index 4fdfc6b..133df65 100644 --- a/api/v1/operation_types.go +++ b/api/v1/operation_types.go @@ -17,29 +17,50 @@ limitations under the License. package v1 import ( + batchv1 "k8s.io/api/batch/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +type ApplicationSpec struct { + Name string `json:"name"` + Provision batchv1.JobSpec `json:"provision"` + Teardown batchv1.JobSpec `json:"teardown"` + // +kubebuilder:validation:Optional + Dependencies []string `json:"dependencies,omitempty"` +} + // OperationSpec defines the desired state of Operation. type OperationSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file - // Foo is an example field of Operation. Edit operation_types.go to remove/update - Foo string `json:"foo,omitempty"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + Applications []ApplicationSpec `json:"applications"` + // +kubebuilder:validation:optional + // +kubebuilder:validation:Pattern:=`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$` + ExpireAt string `json:"expireAt,omitempty"` } // OperationStatus defines the observed state of Operation. type OperationStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file + + // Conditions is a list of conditions to describe the status of the deploy + Conditions []metav1.Condition `json:"conditions"` + Phase string `json:"phase"` + CacheKey string `json:"cacheKey"` + OperationID string `json:"operationId"` } // +kubebuilder:object:root=true // +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Key",type="string",JSONPath=`.status.cacheKey` // Operation is the Schema for the operations API. type Operation struct { diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 176b7bd..c0c3bf6 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -128,6 +128,28 @@ func (in *AppDeploymentStatus) DeepCopy() *AppDeploymentStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApplicationSpec) DeepCopyInto(out *ApplicationSpec) { + *out = *in + in.Provision.DeepCopyInto(&out.Provision) + in.Teardown.DeepCopyInto(&out.Teardown) + if in.Dependencies != nil { + in, out := &in.Dependencies, &out.Dependencies + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSpec. +func (in *ApplicationSpec) DeepCopy() *ApplicationSpec { + if in == nil { + return nil + } + out := new(ApplicationSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Cache) DeepCopyInto(out *Cache) { *out = *in @@ -222,8 +244,8 @@ func (in *Operation) DeepCopyInto(out *Operation) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Operation. @@ -279,6 +301,13 @@ func (in *OperationList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OperationSpec) DeepCopyInto(out *OperationSpec) { *out = *in + if in.Applications != nil { + in, out := &in.Applications, &out.Applications + *out = make([]ApplicationSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperationSpec. @@ -294,6 +323,13 @@ func (in *OperationSpec) DeepCopy() *OperationSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OperationStatus) DeepCopyInto(out *OperationStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperationStatus. diff --git a/config/crd/bases/app.github.com_operations.yaml b/config/crd/bases/app.github.com_operations.yaml index cea74ba..655e06e 100644 --- a/config/crd/bases/app.github.com_operations.yaml +++ b/config/crd/bases/app.github.com_operations.yaml @@ -14,7 +14,14 @@ spec: singular: operation scope: Namespaced versions: - - name: v1 + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .status.cacheKey + name: Key + type: string + name: v1 schema: openAPIV3Schema: properties: @@ -26,10 +33,7577 @@ spec: type: object spec: properties: - foo: + applications: + items: + properties: + dependencies: + items: + type: string + type: array + name: + type: string + provision: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + backoffLimit: + format: int32 + type: integer + backoffLimitPerIndex: + format: int32 + type: integer + completionMode: + type: string + completions: + format: int32 + type: integer + managedBy: + type: string + manualSelector: + type: boolean + maxFailedIndexes: + format: int32 + type: integer + parallelism: + format: int32 + type: integer + podFailurePolicy: + properties: + rules: + items: + properties: + action: + type: string + onExitCodes: + properties: + containerName: + type: string + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + items: + properties: + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + successPolicy: + properties: + rules: + items: + properties: + succeededCount: + format: int32 + type: integer + succeededIndexes: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + suspend: + type: boolean + template: + properties: + metadata: + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + x-kubernetes-list-type: atomic + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + searches: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ip: + type: string + required: + - ip + type: object + type: array + x-kubernetes-list-map-keys: + - ip + x-kubernetes-list-type: map + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + initContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + x-kubernetes-list-type: atomic + resourceClaims: + items: + properties: + name: + type: string + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + default: ext4 + type: string + kind: + type: string + readOnly: + default: false + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + default: default + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + default: /etc/ceph/keyring + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + default: xfs + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + default: ThinProvisioned + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + required: + - containers + type: object + type: object + ttlSecondsAfterFinished: + format: int32 + type: integer + required: + - template + type: object + teardown: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + backoffLimit: + format: int32 + type: integer + backoffLimitPerIndex: + format: int32 + type: integer + completionMode: + type: string + completions: + format: int32 + type: integer + managedBy: + type: string + manualSelector: + type: boolean + maxFailedIndexes: + format: int32 + type: integer + parallelism: + format: int32 + type: integer + podFailurePolicy: + properties: + rules: + items: + properties: + action: + type: string + onExitCodes: + properties: + containerName: + type: string + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + items: + properties: + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + successPolicy: + properties: + rules: + items: + properties: + succeededCount: + format: int32 + type: integer + succeededIndexes: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + suspend: + type: boolean + template: + properties: + metadata: + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + x-kubernetes-list-type: atomic + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + searches: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ip: + type: string + required: + - ip + type: object + type: array + x-kubernetes-list-map-keys: + - ip + x-kubernetes-list-type: map + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + initContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + x-kubernetes-list-type: atomic + resourceClaims: + items: + properties: + name: + type: string + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + default: ext4 + type: string + kind: + type: string + readOnly: + default: false + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + default: default + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + default: /etc/ceph/keyring + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + default: xfs + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + default: ThinProvisioned + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + required: + - containers + type: object + type: object + ttlSecondsAfterFinished: + format: int32 + type: integer + required: + - template + type: object + required: + - name + - provision + - teardown + type: object + minItems: 1 + type: array + expireAt: + pattern: ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$ type: string + required: + - applications type: object status: + properties: + cacheKey: + type: string + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + 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 + operationId: + type: string + phase: + type: string + required: + - cacheKey + - conditions + - operationId + - phase type: object type: object served: true diff --git a/go.mod b/go.mod index 4c992eb..3e045bb 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,11 @@ godebug default=go1.23 require ( github.com/go-logr/logr v1.4.2 + github.com/google/uuid v1.6.0 github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/gomega v1.36.1 - github.com/stretchr/testify v1.9.0 + github.com/samber/lo v1.49.1 + github.com/stretchr/testify v1.10.0 go.uber.org/mock v0.5.1 k8s.io/api v0.32.1 k8s.io/apimachinery v0.32.1 @@ -44,7 +46,6 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect - github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/go.sum b/go.sum index de2f8df..14ebb5d 100644 --- a/go.sum +++ b/go.sum @@ -113,6 +113,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -126,8 +128,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/internal/controller/mocks/mock_operation_adapter.go b/internal/controller/mocks/mock_operation_adapter.go new file mode 100644 index 0000000..607d140 --- /dev/null +++ b/internal/controller/mocks/mock_operation_adapter.go @@ -0,0 +1,117 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/Azure/operation-cache-controller/internal/controller (interfaces: OperationAdapterInterface) +// +// Generated by this command: +// +// mockgen -destination=./mocks/mock_operation_adapter.go -package=mocks github.com/Azure/operation-cache-controller/internal/controller OperationAdapterInterface +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + reconciler "github.com/Azure/operation-cache-controller/internal/utils/reconciler" + gomock "go.uber.org/mock/gomock" +) + +// MockOperationAdapterInterface is a mock of OperationAdapterInterface interface. +type MockOperationAdapterInterface struct { + ctrl *gomock.Controller + recorder *MockOperationAdapterInterfaceMockRecorder + isgomock struct{} +} + +// MockOperationAdapterInterfaceMockRecorder is the mock recorder for MockOperationAdapterInterface. +type MockOperationAdapterInterfaceMockRecorder struct { + mock *MockOperationAdapterInterface +} + +// NewMockOperationAdapterInterface creates a new mock instance. +func NewMockOperationAdapterInterface(ctrl *gomock.Controller) *MockOperationAdapterInterface { + mock := &MockOperationAdapterInterface{ctrl: ctrl} + mock.recorder = &MockOperationAdapterInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOperationAdapterInterface) EXPECT() *MockOperationAdapterInterfaceMockRecorder { + return m.recorder +} + +// EnsureAllAppsAreDeleted mocks base method. +func (m *MockOperationAdapterInterface) EnsureAllAppsAreDeleted(ctx context.Context) (reconciler.OperationResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnsureAllAppsAreDeleted", ctx) + ret0, _ := ret[0].(reconciler.OperationResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EnsureAllAppsAreDeleted indicates an expected call of EnsureAllAppsAreDeleted. +func (mr *MockOperationAdapterInterfaceMockRecorder) EnsureAllAppsAreDeleted(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureAllAppsAreDeleted", reflect.TypeOf((*MockOperationAdapterInterface)(nil).EnsureAllAppsAreDeleted), ctx) +} + +// EnsureAllAppsAreReady mocks base method. +func (m *MockOperationAdapterInterface) EnsureAllAppsAreReady(ctx context.Context) (reconciler.OperationResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnsureAllAppsAreReady", ctx) + ret0, _ := ret[0].(reconciler.OperationResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EnsureAllAppsAreReady indicates an expected call of EnsureAllAppsAreReady. +func (mr *MockOperationAdapterInterfaceMockRecorder) EnsureAllAppsAreReady(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureAllAppsAreReady", reflect.TypeOf((*MockOperationAdapterInterface)(nil).EnsureAllAppsAreReady), ctx) +} + +// EnsureFinalizer mocks base method. +func (m *MockOperationAdapterInterface) EnsureFinalizer(ctx context.Context) (reconciler.OperationResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnsureFinalizer", ctx) + ret0, _ := ret[0].(reconciler.OperationResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EnsureFinalizer indicates an expected call of EnsureFinalizer. +func (mr *MockOperationAdapterInterfaceMockRecorder) EnsureFinalizer(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureFinalizer", reflect.TypeOf((*MockOperationAdapterInterface)(nil).EnsureFinalizer), ctx) +} + +// EnsureFinalizerRemoved mocks base method. +func (m *MockOperationAdapterInterface) EnsureFinalizerRemoved(ctx context.Context) (reconciler.OperationResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnsureFinalizerRemoved", ctx) + ret0, _ := ret[0].(reconciler.OperationResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EnsureFinalizerRemoved indicates an expected call of EnsureFinalizerRemoved. +func (mr *MockOperationAdapterInterfaceMockRecorder) EnsureFinalizerRemoved(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureFinalizerRemoved", reflect.TypeOf((*MockOperationAdapterInterface)(nil).EnsureFinalizerRemoved), ctx) +} + +// EnsureNotExpired mocks base method. +func (m *MockOperationAdapterInterface) EnsureNotExpired(ctx context.Context) (reconciler.OperationResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnsureNotExpired", ctx) + ret0, _ := ret[0].(reconciler.OperationResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EnsureNotExpired indicates an expected call of EnsureNotExpired. +func (mr *MockOperationAdapterInterfaceMockRecorder) EnsureNotExpired(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureNotExpired", reflect.TypeOf((*MockOperationAdapterInterface)(nil).EnsureNotExpired), ctx) +} diff --git a/internal/controller/operation_adapter.go b/internal/controller/operation_adapter.go new file mode 100644 index 0000000..bca8c89 --- /dev/null +++ b/internal/controller/operation_adapter.go @@ -0,0 +1,243 @@ +package controller + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/operation-cache-controller/internal/utils/reconciler" + "github.com/go-logr/logr" + "github.com/samber/lo" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + appsv1 "github.com/Azure/operation-cache-controller/api/v1" + ctlrutils "github.com/Azure/operation-cache-controller/internal/utils/controller" + apdutils "github.com/Azure/operation-cache-controller/internal/utils/controller/appdeployment" + oputils "github.com/Azure/operation-cache-controller/internal/utils/controller/operation" +) + +type operationAdapterContextKey struct{} + +//go:generate mockgen -destination=./mocks/mock_operation_adapter.go -package=mocks github.com/Azure/operation-cache-controller/internal/controller OperationAdapterInterface +type OperationAdapterInterface interface { + EnsureNotExpired(ctx context.Context) (reconciler.OperationResult, error) + EnsureFinalizer(ctx context.Context) (reconciler.OperationResult, error) + EnsureFinalizerRemoved(ctx context.Context) (reconciler.OperationResult, error) + EnsureAllAppsAreReady(ctx context.Context) (reconciler.OperationResult, error) + EnsureAllAppsAreDeleted(ctx context.Context) (reconciler.OperationResult, error) +} + +type OperationAdapter struct { + operation *appsv1.Operation + logger logr.Logger + client client.Client + recorder record.EventRecorder +} + +func NewOperationAdapter(ctx context.Context, operation *appsv1.Operation, logger logr.Logger, client client.Client, recorder record.EventRecorder) OperationAdapterInterface { + if operationAdapter, ok := ctx.Value(operationAdapterContextKey{}).(OperationAdapterInterface); ok { + return operationAdapter + } + + return &OperationAdapter{ + operation: operation, + logger: logger, + client: client, + recorder: recorder, + } +} + +func (o *OperationAdapter) phaseIn(phases ...string) bool { + + for _, phase := range phases { + if phase == o.operation.Status.Phase { + return true + } + } + return false +} + +func (o *OperationAdapter) EnsureNotExpired(ctx context.Context) (reconciler.OperationResult, error) { + o.logger.V(1).Info("Operation EnsureNotExpired") + if len(o.operation.Spec.ExpireAt) == 0 { + return reconciler.ContinueProcessing() + } + if o.phaseIn(oputils.PhaseDeleted, oputils.PhaseDeleting) { + return reconciler.ContinueProcessing() + } + expireTime, err := time.Parse(time.RFC3339, o.operation.Spec.ExpireAt) + if err != nil { + o.logger.Error(err, fmt.Sprintf("Failed to parse expire time: %s", o.operation.Spec.ExpireAt)) + o.recorder.Event(o.operation, "Warning", "InvalidExpireTime", "Failed to parse expire time") + return reconciler.ContinueProcessing() + } + if time.Now().Before(expireTime) { + return reconciler.ContinueProcessing() + } + // Expired + o.logger.Info("deleting expired operation", "expireAt", o.operation.Spec.ExpireAt) + if err := o.client.Delete(ctx, o.operation, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil { + o.logger.Error(err, "Failed to delete expired operation") + o.recorder.Event(o.operation, "Warning", "DeleteFailed", "Failed to delete expired operation") + return reconciler.RequeueWithError(err) + } + // Stop processing if the operation is deleted + return reconciler.ContinueProcessing() +} + +func (o *OperationAdapter) EnsureAllAppsAreReady(ctx context.Context) (reconciler.OperationResult, error) { + o.logger.V(1).Info("Operation EnsureAllAppsAreReady") + if o.phaseIn(oputils.PhaseDeleted, oputils.PhaseDeleting) { + return reconciler.ContinueProcessing() + } + if o.phaseIn(oputils.PhaseEmpty) { + o.logger.V(1).Info("initializing operation status") + oputils.ClearConditions(o.operation) + o.operation.Status.OperationID = oputils.NewOperationId() + } + if o.phaseIn(oputils.PhaseReconciling) { + err := o.reconcilingApplications(ctx) + if err != nil { + o.logger.Error(err, "reconciling applications failed") + o.recorder.Event(o.operation, "Warning", "ReconcileFailed", "Failed to reconcile deployments") + return reconciler.RequeueWithError(err) + } + + o.operation.Status.Phase = oputils.PhaseReconciled + return reconciler.RequeueOnErrorOrStop(o.client.Status().Update(ctx, o.operation)) + } + + // check the diff between the expected and actual apps, set phase to reconciling and requeue if changes + expectedCacheKey := ctlrutils.NewCacheKeyFromApplications(o.operation.Spec.Applications) + if o.operation.Status.CacheKey != expectedCacheKey { + o.operation.Status.CacheKey = expectedCacheKey + o.operation.Status.Phase = oputils.PhaseReconciling + } + return reconciler.RequeueOnErrorOrContinue(o.client.Status().Update(ctx, o.operation)) +} + +func (o *OperationAdapter) EnsureFinalizer(ctx context.Context) (reconciler.OperationResult, error) { + o.logger.V(1).Info("operation EnsureFinalizer") + if o.operation.ObjectMeta.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(o.operation, oputils.FinalizerName) { + controllerutil.AddFinalizer(o.operation, oputils.FinalizerName) + } + return reconciler.RequeueOnErrorOrContinue(o.client.Update(ctx, o.operation)) +} + +func (o *OperationAdapter) EnsureFinalizerRemoved(ctx context.Context) (reconciler.OperationResult, error) { + o.logger.V(1).Info("operation EnsureFinalizerDeleted") + if !o.operation.ObjectMeta.DeletionTimestamp.IsZero() && controllerutil.ContainsFinalizer(o.operation, oputils.FinalizerName) { + if o.phaseIn(oputils.PhaseDeleted) { + o.logger.V(1).Info("All app deleted removing finalizer") + controllerutil.RemoveFinalizer(o.operation, oputils.FinalizerName) + return reconciler.RequeueOnErrorOrContinue(o.client.Update(ctx, o.operation)) + } + if !o.phaseIn(oputils.PhaseDeleting) { + o.logger.V(1).Info("App is not deleted yet, setting phase to deleting") + o.operation.Status.Phase = oputils.PhaseDeleting + return reconciler.RequeueOnErrorOrContinue(o.client.Status().Update(ctx, o.operation)) + } + } + return reconciler.ContinueProcessing() +} + +func (o *OperationAdapter) EnsureAllAppsAreDeleted(ctx context.Context) (reconciler.OperationResult, error) { + o.logger.V(1).Info("Operation EnsureAllAppsAreDeleted") + if o.phaseIn(oputils.PhaseDeleting) { + // deleting logic here + o.operation.Status.Phase = oputils.PhaseDeleted + return reconciler.RequeueOnErrorOrStop(o.client.Status().Update(ctx, o.operation)) + } + return reconciler.ContinueProcessing() +} + +func (o *OperationAdapter) reconcilingApplications(ctx context.Context) error { + logger := o.logger.WithValues("operation", "reconcilingApplications") + currentAppDeployments, err := o.listCurrentAppDeployments(ctx) + if err != nil { + return fmt.Errorf("failed to list current appDeployments: %w", err) + } + logger.V(1).Info(fmt.Sprintf("current app deployments count %d", len(currentAppDeployments))) + for _, app := range currentAppDeployments { + logger.V(1).Info("current app deployment", "appName", app.Name, "opId", app.Spec.OpId, "provision", app.Spec.Provision, "teardown", app.Spec.Teardown, "dependencies", app.Spec.Dependencies) + } + + expectedAppDeployments := o.expectedAppDeployments() + logger.V(1).Info(fmt.Sprintf("expected app deployments count %d", len(expectedAppDeployments))) + for _, app := range expectedAppDeployments { + logger.V(1).Info("expected app deployment", "appName", app.Name, "opId", app.Spec.OpId, "provision", app.Spec.Provision, "teardown", app.Spec.Teardown, "dependencies", app.Spec.Dependencies) + } + + added, removed, updated := oputils.DiffAppDeployments(expectedAppDeployments, currentAppDeployments, oputils.CompareProvisionJobs) + for _, app := range added { + logger.V(1).Info(fmt.Sprintf("app to be added %s", app.Name), "opId", app.Spec.OpId, "provision", app.Spec.Provision, "teardown", app.Spec.Teardown, "dependencies", app.Spec.Dependencies) + if err := ctrl.SetControllerReference(o.operation, &app, o.client.Scheme()); err != nil { + return fmt.Errorf("failed to set controller reference: %w", err) + } + if err := o.client.Create(ctx, &app); err != nil { + return fmt.Errorf("failed to create app deployment: %w", err) + } + } + + for _, app := range removed { + logger.V(1).Info(fmt.Sprintf("app to be removed %s", app.Name), "opId", app.Spec.OpId, "provision", app.Spec.Provision, "teardown", app.Spec.Teardown, "dependencies", app.Spec.Dependencies) + if err := o.client.Delete(ctx, &app, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil { + return fmt.Errorf("failed to delete app deployment: %w", err) + } + } + + for _, app := range updated { + logger.V(1).Info(fmt.Sprintf("app to be updated %s", app.Name), "appName", app.Name, "opId", app.Spec.OpId, "provision", app.Spec.Provision, "teardown", app.Spec.Teardown, "dependencies", app.Spec.Dependencies) + if err := o.client.Update(ctx, &app); err != nil { + return fmt.Errorf("failed to update app deployment: %w", err) + } + } + + // check if all expected app deployments are ready + for _, app := range expectedAppDeployments { + appdeployment := &appsv1.AppDeployment{} + if err := o.client.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, appdeployment); err != nil { + return fmt.Errorf("failed to get app deployment: %w", err) + } + // check if all dependencies are ready + if appdeployment.Status.Phase != apdutils.PhaseReady { + return fmt.Errorf("app deployment is not ready: name %s, status, %s", app.Name, app.Status.Phase) + } + } + + return nil +} + +func (o *OperationAdapter) expectedAppDeployments() []appsv1.AppDeployment { + return lo.Map(o.operation.Spec.Applications, func(app appsv1.ApplicationSpec, index int) appsv1.AppDeployment { + return appsv1.AppDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: apdutils.OperationScopedAppDeployment(app.Name, o.operation.Status.OperationID), + Namespace: o.operation.Namespace, + }, + Spec: appsv1.AppDeploymentSpec{ + OpId: o.operation.Status.OperationID, + Provision: app.Provision, + Teardown: app.Teardown, + Dependencies: app.Dependencies, + }, + } + }) +} + +func (o *OperationAdapter) listCurrentAppDeployments(ctx context.Context) ([]appsv1.AppDeployment, error) { + appDeploymentList := &appsv1.AppDeploymentList{} + if err := o.client.List(ctx, appDeploymentList, client.MatchingFields{operationOwnerKey: o.operation.Name}); err != nil { + return nil, fmt.Errorf("failed to list appDeployments: %w", err) + } + return lo.Map(appDeploymentList.Items, func(app appsv1.AppDeployment, index int) appsv1.AppDeployment { + return appsv1.AppDeployment{ + ObjectMeta: app.ObjectMeta, + Spec: app.Spec, + } + }), nil +} diff --git a/internal/controller/operation_adapter_test.go b/internal/controller/operation_adapter_test.go new file mode 100644 index 0000000..b40f21a --- /dev/null +++ b/internal/controller/operation_adapter_test.go @@ -0,0 +1,449 @@ +package controller + +import ( + "context" + "testing" + "time" + + appsv1 "github.com/Azure/operation-cache-controller/api/v1" + mockpkg "github.com/Azure/operation-cache-controller/internal/mocks" + adputils "github.com/Azure/operation-cache-controller/internal/utils/controller/appdeployment" + oputils "github.com/Azure/operation-cache-controller/internal/utils/controller/operation" + "github.com/Azure/operation-cache-controller/internal/utils/reconciler" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +var ( + emptyOperation = &appsv1.Operation{} + validOperation = &appsv1.Operation{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-operation", + Namespace: "default", + }, + Spec: appsv1.OperationSpec{ + Applications: []appsv1.ApplicationSpec{ + { + Name: "test-app1", + Provision: newTestJobSpec(), + Teardown: newTestJobSpec(), + + Dependencies: []string{"test-app2"}, + }, + { + Name: "test-app2", + Provision: newTestJobSpec(), + Teardown: newTestJobSpec(), + }, + }, + }, + } + emptyAppDeploymentList = &appsv1.AppDeploymentList{} + + validAppDeploymentList = &appsv1.AppDeploymentList{ + Items: []appsv1.AppDeployment{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-app1", + Namespace: "default", + }, + Spec: appsv1.AppDeploymentSpec{ + OpId: "test-operation", + Provision: newTestJobSpec(), + Teardown: newTestJobSpec(), + Dependencies: []string{"test-app2"}, + }, + Status: appsv1.AppDeploymentStatus{ + Phase: adputils.PhaseReady, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-app2", + Namespace: "default", + }, + Spec: appsv1.AppDeploymentSpec{ + OpId: "test-operation", + Provision: newTestJobSpec(), + Teardown: newTestJobSpec(), + }, + Status: appsv1.AppDeploymentStatus{ + Phase: adputils.PhaseReady, + }, + }, + }, + } + + changedValidAppDeploymentList = &appsv1.AppDeploymentList{ + Items: []appsv1.AppDeployment{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-app2", + Namespace: "default", + }, + Spec: appsv1.AppDeploymentSpec{ + OpId: "test-operation", + Provision: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-container", + Image: "test-image", + Command: []string{ + "echo", + "world", + }, + Args: []string{ + "hello", + }, + }, + }, + }, + }, + }, + Teardown: newTestJobSpec(), + }, + Status: appsv1.AppDeploymentStatus{ + Phase: adputils.PhaseReady, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-app3", + Namespace: "default", + }, + Spec: appsv1.AppDeploymentSpec{ + OpId: "test-operation", + Provision: newTestJobSpec(), + Teardown: newTestJobSpec(), + }, + Status: appsv1.AppDeploymentStatus{ + Phase: adputils.PhaseReady, + }, + }, + }, + } +) + +func TestNewOperationAdapter(t *testing.T) { + ctx := context.Background() + logger := log.FromContext(ctx) + + mockCtrl := gomock.NewController(t) + mockClient := mockpkg.NewMockClient(mockCtrl) + mockRecorderCtrl := gomock.NewController(t) + mockRecorder := mockpkg.NewMockEventRecorder(mockRecorderCtrl) + + operation := emptyOperation.DeepCopy() + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + assert.NotNil(t, adapter) +} + +func TestOperationAdapter_EnsureNotExpired(t *testing.T) { + ctx := context.Background() + logger := log.FromContext(ctx) + + mockCtrl := gomock.NewController(t) + mockClient := mockpkg.NewMockClient(mockCtrl) + mockRecorderCtrl := gomock.NewController(t) + mockRecorder := mockpkg.NewMockEventRecorder(mockRecorderCtrl) + + t.Run("happy path: continue processing when expire is not set", func(t *testing.T) { + operation := validOperation.DeepCopy() + operation.Spec.ExpireAt = time.Now().Add(time.Hour).Format(time.RFC3339) + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + res, err := adapter.EnsureNotExpired(ctx) + assert.NoError(t, err) + assert.Equal(t, reconciler.OperationResult{}, res) + }) + t.Run("happy path: continue processing when operation is in deleting phase", func(t *testing.T) { + operation := validOperation.DeepCopy() + operation.Spec.ExpireAt = time.Now().Add(time.Hour).Format(time.RFC3339) + operation.Status.Phase = oputils.PhaseDeleting + + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + res, err := adapter.EnsureNotExpired(ctx) + assert.NoError(t, err) + assert.Equal(t, reconciler.OperationResult{}, res) + + operation.Status.Phase = oputils.PhaseDeleted + adapter = NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + res, err = adapter.EnsureNotExpired(ctx) + assert.NoError(t, err) + assert.Equal(t, reconciler.OperationResult{}, res) + }) + t.Run("happy path: continue processing when expire time is in the future", func(t *testing.T) { + operation := validOperation.DeepCopy() + operation.Spec.ExpireAt = time.Now().Add(time.Hour).Format(time.RFC3339) + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + res, err := adapter.EnsureNotExpired(ctx) + assert.NoError(t, err) + assert.Equal(t, reconciler.OperationResult{}, res) + }) + t.Run("Sad path: failed to parse expire time", func(t *testing.T) { + operation := validOperation.DeepCopy() + operation.Spec.ExpireAt = "invalid-time" + mockRecorder.EXPECT().Event(operation, "Warning", "InvalidExpireTime", "Failed to parse expire time") + + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + res, err := adapter.EnsureNotExpired(ctx) + assert.NoError(t, err) + assert.Equal(t, reconciler.OperationResult{}, res) + }) + + t.Run("happy path: delete operation when expire time is in the past", func(t *testing.T) { + operation := validOperation.DeepCopy() + operation.Spec.ExpireAt = time.Now().Add(-time.Hour).Format(time.RFC3339) + + mockClient.EXPECT().Delete(ctx, operation, gomock.Any()).Return(nil) + + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + res, err := adapter.EnsureNotExpired(ctx) + assert.NoError(t, err) + assert.Equal(t, reconciler.OperationResult{}, res) + }) + t.Run("sad path: delete operation failed", func(t *testing.T) { + operation := validOperation.DeepCopy() + operation.Spec.ExpireAt = time.Now().Add(-time.Hour).Format(time.RFC3339) + + mockClient.EXPECT().Delete(ctx, operation, gomock.Any()).Return(assert.AnError) + + mockRecorder.EXPECT().Event(operation, "Warning", "DeleteFailed", "Failed to delete expired operation") + + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + res, err := adapter.EnsureNotExpired(ctx) + assert.Error(t, err) + assert.Equal(t, reconciler.OperationResult{RequeueDelay: reconciler.DefaultRequeueDelay, RequeueRequest: true}, res) + }) +} + +func TestOperationAdapter_EnsureAllAppsAreReady(t *testing.T) { + ctx := context.Background() + logger := log.FromContext(ctx) + + t.Run("happy path: continue processing when operation is in deleting phase", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockClient := mockpkg.NewMockClient(mockCtrl) + mockRecorderCtrl := gomock.NewController(t) + mockRecorder := mockpkg.NewMockEventRecorder(mockRecorderCtrl) + + operation := validOperation.DeepCopy() + operation.Status.Phase = oputils.PhaseDeleting + + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + res, err := adapter.EnsureAllAppsAreReady(ctx) + assert.NoError(t, err) + assert.Equal(t, reconciler.OperationResult{}, res) + + operation.Status.Phase = oputils.PhaseDeleted + adapter = NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + res, err = adapter.EnsureAllAppsAreReady(ctx) + assert.NoError(t, err) + assert.Equal(t, reconciler.OperationResult{}, res) + }) + + t.Run("happy path: continue processing when operation is in empty phase", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockClient := mockpkg.NewMockClient(mockCtrl) + mockStatusWriterCtrl := gomock.NewController(t) + mockStatusWriter := mockpkg.NewMockStatusWriter(mockStatusWriterCtrl) + mockClient.EXPECT().Status().Return(mockStatusWriter) + mockRecorderCtrl := gomock.NewController(t) + mockRecorder := mockpkg.NewMockEventRecorder(mockRecorderCtrl) + operation := emptyOperation.DeepCopy() + + mockStatusWriter.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil) + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + res, err := adapter.EnsureAllAppsAreReady(ctx) + assert.NoError(t, err) + assert.Equal(t, reconciler.OperationResult{RequeueDelay: reconciler.DefaultRequeueDelay}, res) + assert.NotEmpty(t, operation.Status.CacheKey) + assert.Equal(t, operation.Status.Phase, oputils.PhaseReconciling) + }) + + t.Run("happy path: continue processing when operation is in reconciling phase", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockClient := mockpkg.NewMockClient(mockCtrl) + mockRecorderCtrl := gomock.NewController(t) + mockRecorder := mockpkg.NewMockEventRecorder(mockRecorderCtrl) + + operation := validOperation.DeepCopy() + operation.Status.Phase = oputils.PhaseReconciling + + appList := emptyAppDeploymentList.DeepCopy() + mockClient.EXPECT().List(ctx, appList, gomock.Any()).DoAndReturn(func(ctx context.Context, list *appsv1.AppDeploymentList, opts ...interface{}) error { + *list = *changedValidAppDeploymentList + return nil + }) + scheme := runtime.NewScheme() + utilruntime.Must(appsv1.AddToScheme(scheme)) + mockClient.EXPECT().Scheme().Return(scheme).AnyTimes() + mockClient.EXPECT().Get(ctx, gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, key client.ObjectKey, obj runtime.Object, opt ...interface{}) error { + *obj.(*appsv1.AppDeployment) = appsv1.AppDeployment{} + return nil + }).AnyTimes() + mockClient.EXPECT().Create(ctx, gomock.Any()).Return(nil) + mockClient.EXPECT().Delete(ctx, gomock.Any(), gomock.Any()).Return(nil) + mockClient.EXPECT().Update(ctx, gomock.Any()).Return(nil) + mockRecorder.EXPECT().Event(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) + + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + res, err := adapter.EnsureAllAppsAreReady(ctx) + assert.ErrorContains(t, err, "app deployment is not ready") + assert.Equal(t, reconciler.OperationResult{RequeueDelay: reconciler.DefaultRequeueDelay, RequeueRequest: true}, res) + assert.Equal(t, operation.Status.Phase, oputils.PhaseReconciling) + + }) + + t.Run("happy path: continue processing when all apps are ready", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockClient := mockpkg.NewMockClient(mockCtrl) + mockRecorderCtrl := gomock.NewController(t) + mockRecorder := mockpkg.NewMockEventRecorder(mockRecorderCtrl) + mockStatusWriterCtrl := gomock.NewController(t) + mockStatusWriter := mockpkg.NewMockStatusWriter(mockStatusWriterCtrl) + mockClient.EXPECT().Status().Return(mockStatusWriter) + + // set operation to reconciling phase + operation := validOperation.DeepCopy() + operation.Status.Phase = oputils.PhaseReconciling + + appList := emptyAppDeploymentList.DeepCopy() + mockClient.EXPECT().List(ctx, appList, gomock.Any()).DoAndReturn(func(ctx context.Context, list *appsv1.AppDeploymentList, opts ...interface{}) error { + *list = *validAppDeploymentList + return nil + }) + scheme := runtime.NewScheme() + mockClient.EXPECT().Scheme().Return(scheme).AnyTimes() + readyAppDeployment := &appsv1.AppDeployment{} + readyAppDeployment.Status.Phase = adputils.PhaseReady + mockClient.EXPECT().Get(ctx, gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, key client.ObjectKey, obj runtime.Object, opt ...interface{}) error { + *obj.(*appsv1.AppDeployment) = *readyAppDeployment + return nil + }).AnyTimes() + mockStatusWriter.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil) + + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + res, err := adapter.EnsureAllAppsAreReady(ctx) + assert.NoError(t, err) + assert.Equal(t, reconciler.OperationResult{RequeueDelay: reconciler.DefaultRequeueDelay, CancelRequest: true}, res) + assert.Equal(t, operation.Status.Phase, oputils.PhaseReconciled) + }) + +} + +func TestOperationAdapter_EnsureFinalizer(t *testing.T) { + ctx := context.Background() + logger := log.FromContext(ctx) + + mockCtrl := gomock.NewController(t) + mockClient := mockpkg.NewMockClient(mockCtrl) + mockRecorderCtrl := gomock.NewController(t) + mockRecorder := mockpkg.NewMockEventRecorder(mockRecorderCtrl) + + t.Run("happy path: continue processing when finalizer is not set", func(t *testing.T) { + operation := validOperation.DeepCopy() + operation.ObjectMeta.Finalizers = nil + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + + mockClient.EXPECT().Update(ctx, operation).Return(nil) + res, err := adapter.EnsureFinalizer(ctx) + assert.NoError(t, err) + assert.Equal(t, reconciler.OperationResult{RequeueDelay: reconciler.DefaultRequeueDelay}, res) + }) +} + +func TestOperationAdapter_EnsureFinalizerRemoved(t *testing.T) { + ctx := context.Background() + logger := log.FromContext(ctx) + + mockCtrl := gomock.NewController(t) + mockClient := mockpkg.NewMockClient(mockCtrl) + mockRecorderCtrl := gomock.NewController(t) + mockRecorder := mockpkg.NewMockEventRecorder(mockRecorderCtrl) + mockStatusWriterCtrl := gomock.NewController(t) + mockStatusWriter := mockpkg.NewMockStatusWriter(mockStatusWriterCtrl) + mockClient.EXPECT().Status().Return(mockStatusWriter) + + t.Run("happy path: continue processing when finalizer is not set", func(t *testing.T) { + operation := validOperation.DeepCopy() + operation.ObjectMeta.Finalizers = nil + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + + res, err := adapter.EnsureFinalizerRemoved(ctx) + assert.NoError(t, err) + assert.Equal(t, reconciler.OperationResult{}, res) + }) + + t.Run("happy path: continue processing when operation is in deleted phase", func(t *testing.T) { + + operation := validOperation.DeepCopy() + operation.ObjectMeta.Finalizers = []string{oputils.FinalizerName} + operation.ObjectMeta.DeletionTimestamp = &metav1.Time{Time: time.Now()} + operation.Status.Phase = oputils.PhaseDeleted + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + + mockClient.EXPECT().Update(ctx, operation).Return(nil) + + res, err := adapter.EnsureFinalizerRemoved(ctx) + assert.NoError(t, err) + assert.Equal(t, reconciler.OperationResult{RequeueDelay: reconciler.DefaultRequeueDelay}, res) + assert.Equal(t, len(operation.ObjectMeta.Finalizers), 0) + }) + + t.Run("happy path: continue processing when operation is not in deleting phase", func(t *testing.T) { + operation := validOperation.DeepCopy() + operation.ObjectMeta.Finalizers = []string{oputils.FinalizerName} + operation.ObjectMeta.DeletionTimestamp = &metav1.Time{Time: time.Now()} + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + + mockStatusWriter.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil) + res, err := adapter.EnsureFinalizerRemoved(ctx) + assert.NoError(t, err) + assert.Equal(t, reconciler.OperationResult{RequeueDelay: reconciler.DefaultRequeueDelay}, res) + }) +} + +func TestOperationAdapter_EnsureAllAppsAreDeleted(t *testing.T) { + ctx := context.Background() + logger := log.FromContext(ctx) + + mockCtrl := gomock.NewController(t) + mockClient := mockpkg.NewMockClient(mockCtrl) + mockRecorderCtrl := gomock.NewController(t) + mockRecorder := mockpkg.NewMockEventRecorder(mockRecorderCtrl) + mockStatusWriterCtrl := gomock.NewController(t) + mockStatusWriter := mockpkg.NewMockStatusWriter(mockStatusWriterCtrl) + mockClient.EXPECT().Status().Return(mockStatusWriter) + + t.Run("happy path: continue processing when operation is in deleting phase", func(t *testing.T) { + operation := validOperation.DeepCopy() + operation.Status.Phase = oputils.PhaseDeleting + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + + mockStatusWriter.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil) + + res, err := adapter.EnsureAllAppsAreDeleted(ctx) + assert.NoError(t, err) + assert.Equal(t, reconciler.OperationResult{RequeueDelay: reconciler.DefaultRequeueDelay, CancelRequest: true}, res) + assert.Equal(t, operation.Status.Phase, oputils.PhaseDeleted) + + }) + + t.Run("happy path: continue processing when operation is in empty phase", func(t *testing.T) { + operation := emptyOperation.DeepCopy() + adapter := NewOperationAdapter(ctx, operation, logger, mockClient, mockRecorder) + + res, err := adapter.EnsureAllAppsAreDeleted(ctx) + assert.NoError(t, err) + assert.Equal(t, reconciler.OperationResult{}, res) + }) +} diff --git a/internal/controller/operation_controller.go b/internal/controller/operation_controller.go index 6fab454..fbe8abe 100644 --- a/internal/controller/operation_controller.go +++ b/internal/controller/operation_controller.go @@ -19,18 +19,24 @@ package controller import ( "context" + batchv1 "k8s.io/api/batch/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/log" - appv1 "github.com/Azure/operation-cache-controller/api/v1" + appsv1 "github.com/Azure/operation-cache-controller/api/v1" + "github.com/Azure/operation-cache-controller/internal/utils/reconciler" ) // OperationReconciler reconciles a Operation object type OperationReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + recorder record.EventRecorder } // +kubebuilder:rbac:groups=app.github.com,resources=operations,verbs=get;list;watch;create;update;patch;delete @@ -47,17 +53,68 @@ type OperationReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.4/pkg/reconcile func (r *OperationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = logf.FromContext(ctx) + logger := log.FromContext(ctx) - // TODO(user): your logic here + operation := &appsv1.Operation{} + if err := r.Get(ctx, req.NamespacedName, operation); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + adapter := NewOperationAdapter(ctx, operation, logger, r.Client, r.recorder) + return r.ReconcileHandler(ctx, adapter) +} + +func (r *OperationReconciler) ReconcileHandler(ctx context.Context, adapter OperationAdapterInterface) (ctrl.Result, error) { + operations := []reconciler.ReconcileOperation{ + adapter.EnsureFinalizer, + adapter.EnsureFinalizerRemoved, + adapter.EnsureNotExpired, + adapter.EnsureAllAppsAreReady, + adapter.EnsureAllAppsAreDeleted, + } + + for _, operation := range operations { + result, err := operation(ctx) + if err != nil || result.RequeueRequest { + return ctrl.Result{RequeueAfter: reconciler.DefaultRequeueDelay}, err + } + if result.CancelRequest { + return ctrl.Result{}, nil + } + } return ctrl.Result{}, nil } +var operationOwnerKey = ".operation.metadata.controller" + // SetupWithManager sets up the controller with the Manager. func (r *OperationReconciler) SetupWithManager(mgr ctrl.Manager) error { + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &appsv1.AppDeployment{}, operationOwnerKey, + func(rawObj client.Object) []string { + // grab the AppDeployment object, extract the owner + adp := rawObj.(*appsv1.AppDeployment) + owner := metav1.GetControllerOf(adp) + if owner == nil { + return nil + } + // Make sure the owner is a Operation object + if owner.APIVersion != appsv1.GroupVersion.String() || owner.Kind != "Operation" { + return nil + } + return []string{owner.Name} + }); err != nil { + return err + } + + r.recorder = mgr.GetEventRecorderFor("Operation") + return ctrl.NewControllerManagedBy(mgr). - For(&appv1.Operation{}). + For(&appsv1.Operation{}). + Owns(&batchv1.Job{}). + WithOptions(controller.Options{ + MaxConcurrentReconciles: 100, + }). Named("operation"). Complete(r) } diff --git a/internal/controller/operation_controller_test.go b/internal/controller/operation_controller_test.go index 3c77853..9b6f89e 100644 --- a/internal/controller/operation_controller_test.go +++ b/internal/controller/operation_controller_test.go @@ -18,19 +18,165 @@ package controller import ( "context" + "fmt" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/reconcile" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" appv1 "github.com/Azure/operation-cache-controller/api/v1" + "github.com/Azure/operation-cache-controller/internal/controller/mocks" + "github.com/Azure/operation-cache-controller/internal/utils/reconciler" ) var _ = Describe("Operation Controller", func() { + Context("When setupWithManager is called", func() { + It("Should setup the controller with the manager", func() { + + // Create a new mock controller + mockCtrl := gomock.NewController(GinkgoT()) + defer mockCtrl.Finish() + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).NotTo(HaveOccurred()) + + err = (&OperationReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + recorder: k8sManager.GetEventRecorderFor("appdeployment-controller"), + }).SetupWithManager(k8sManager) + + Expect(err).NotTo(HaveOccurred()) + }) + }) + Context("When creating a new Operation Controller", func() { + var ( + timeout = time.Second * 10 + interval = time.Millisecond * 250 + ) + It("Should create a new Operation Controller", func() { + key := types.NamespacedName{ + Name: "test-operation", + Namespace: "default", + } + operation := &appv1.Operation{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: appv1.OperationSpec{ + Applications: []appv1.ApplicationSpec{ + { + Name: "test-app1", + Provision: newTestJobSpec(), + Teardown: newTestJobSpec(), + }, + { + Name: "test-app2", + Provision: newTestJobSpec(), + Teardown: newTestJobSpec(), + Dependencies: []string{"test-app1"}, + }, + }, + }, + } + + Expect(k8sClient.Create(context.Background(), operation)).To(Succeed()) + + feched := &appv1.Operation{} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), key, feched) + return err == nil + }, timeout, interval).Should(BeTrue()) + }) + }) + Context("When reconciling a resource with adapter", func() { + var ( + mockClientCtrl *gomock.Controller + mockRecorderCtrl *gomock.Controller + mockAdapterCtrl *gomock.Controller + mockAdapter *mocks.MockOperationAdapterInterface + + operationReconciler *OperationReconciler + + key = types.NamespacedName{ + Name: "test-operation", + Namespace: "default", + } + ) + + BeforeEach(func() { + mockClientCtrl = gomock.NewController(GinkgoT()) + mockRecorderCtrl = gomock.NewController(GinkgoT()) + mockAdapterCtrl = gomock.NewController(GinkgoT()) + mockAdapter = mocks.NewMockOperationAdapterInterface(mockAdapterCtrl) + operationReconciler = &OperationReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + }) + + AfterEach(func() { + mockClientCtrl.Finish() + mockRecorderCtrl.Finish() + mockAdapterCtrl.Finish() + }) + + It("Should reconcile the resource with adapter", func() { + By("Reconciling the created resource") + ctx := context.Background() + + mockAdapter.EXPECT().EnsureFinalizer(gomock.Any()).Return(reconciler.ContinueOperationResult(), nil) + mockAdapter.EXPECT().EnsureFinalizerRemoved(gomock.Any()).Return(reconciler.ContinueOperationResult(), nil) + mockAdapter.EXPECT().EnsureNotExpired(gomock.Any()).Return(reconciler.ContinueOperationResult(), nil) + mockAdapter.EXPECT().EnsureAllAppsAreReady(gomock.Any()).Return(reconciler.ContinueOperationResult(), nil) + mockAdapter.EXPECT().EnsureAllAppsAreDeleted(gomock.Any()).Return(reconciler.ContinueOperationResult(), nil) + + res, err := operationReconciler.Reconcile(context.WithValue(ctx, operationAdapterContextKey{}, mockAdapter), reconcile.Request{ + NamespacedName: key, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(res).Should(Equal(reconcile.Result{})) + + }) + It("should return the error if any", func() { + By("Reconciling the created resource") + ctx := context.Background() + testErr := fmt.Errorf("test-error") + mockAdapter.EXPECT().EnsureFinalizer(gomock.Any()).Return(reconciler.ContinueOperationResult(), testErr) + + res, err := operationReconciler.Reconcile(context.WithValue(ctx, operationAdapterContextKey{}, mockAdapter), reconcile.Request{ + NamespacedName: key, + }) + Expect(err).To(HaveOccurred()) + Expect(res).Should(Equal(reconcile.Result{RequeueAfter: reconciler.DefaultRequeueDelay})) + }) + + It("should cancel the reconcile loop", func() { + By("Reconciling the created resource") + ctx := context.Background() + + mockAdapter.EXPECT().EnsureFinalizer(gomock.Any()).Return(reconciler.OperationResult{ + CancelRequest: true, + }, nil) + + res, err := operationReconciler.Reconcile(context.WithValue(ctx, operationAdapterContextKey{}, mockAdapter), reconcile.Request{ + NamespacedName: key, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(res).Should(Equal(reconcile.Result{})) + }) + }) Context("When reconciling a resource", func() { const resourceName = "test-resource" @@ -51,7 +197,21 @@ var _ = Describe("Operation Controller", func() { Name: resourceName, Namespace: "default", }, - // TODO(user): Specify other spec details if needed. + Spec: appv1.OperationSpec{ + Applications: []appv1.ApplicationSpec{ + { + Name: "test-app1", + Provision: newTestJobSpec(), + Teardown: newTestJobSpec(), + }, + { + Name: "test-app2", + Provision: newTestJobSpec(), + Teardown: newTestJobSpec(), + Dependencies: []string{"test-app1"}, + }, + }, + }, } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) } diff --git a/internal/utils/controller/cachekey.go b/internal/utils/controller/cachekey.go new file mode 100644 index 0000000..3303714 --- /dev/null +++ b/internal/utils/controller/cachekey.go @@ -0,0 +1,90 @@ +package controller + +import ( + "crypto/sha256" + "encoding/hex" + "sort" + "strings" + + "github.com/samber/lo" + corev1 "k8s.io/api/core/v1" + + appsv1 "github.com/Azure/operation-cache-controller/api/v1" +) + +type AppCacheField struct { + Name string + Image string + Command []string + Args []string + WorkingDir string + Env []corev1.EnvVar + Dependencies []string +} + +func (c *AppCacheField) NewCacheKey() string { + hasher := sha256.New() + hasher.Write([]byte(c.Name)) + hasher.Write([]byte(c.Image)) + hasher.Write([]byte(strings.Join(c.Command, " "))) + hasher.Write([]byte(strings.Join(c.Args, " "))) + hasher.Write([]byte(c.WorkingDir)) + + // Sort environment variables to ensure consistent hashing + sort.Slice(c.Env, func(i, j int) bool { + return c.Env[i].Name < c.Env[j].Name + }) + for _, env := range c.Env { + hasher.Write([]byte(env.Name + "=" + env.Value)) + } + + // Sort dependencies to ensure consistent hashing + sort.Strings(c.Dependencies) + for _, dep := range c.Dependencies { + hasher.Write([]byte(dep)) + } + + return hex.EncodeToString(hasher.Sum(nil)) +} + +func NewCacheKeyFromApplications(apps []appsv1.ApplicationSpec) string { + // sort the apps by name to ensure consistent hashing + sort.Slice(apps, func(i, j int) bool { + return apps[i].Name < apps[j].Name + }) + + srcCacheKeys := lo.Reduce(apps, func(acc []string, app appsv1.ApplicationSpec, index int) []string { + return append(acc, AppCacheFieldFromApplicationProvision(app).NewCacheKey()) + }, []string{}) + + // get the cache id for the source + hasher := sha256.New() + for _, id := range srcCacheKeys { + hasher.Write([]byte(id)) + } + return hex.EncodeToString(hasher.Sum(nil)) +} + +func AppCacheFieldFromApplicationProvision(app appsv1.ApplicationSpec) *AppCacheField { + return &AppCacheField{ + Name: app.Name, + Image: app.Provision.Template.Spec.Containers[0].Image, + Command: app.Provision.Template.Spec.Containers[0].Command, + Args: app.Provision.Template.Spec.Containers[0].Args, + WorkingDir: app.Provision.Template.Spec.Containers[0].WorkingDir, + Env: app.Provision.Template.Spec.Containers[0].Env, + Dependencies: app.Dependencies, + } +} + +func AppCacheFieldFromApplicationTeardown(app appsv1.ApplicationSpec) *AppCacheField { + return &AppCacheField{ + Name: app.Name, + Image: app.Teardown.Template.Spec.Containers[0].Image, + Command: app.Teardown.Template.Spec.Containers[0].Command, + Args: app.Teardown.Template.Spec.Containers[0].Args, + WorkingDir: app.Teardown.Template.Spec.Containers[0].WorkingDir, + Env: app.Teardown.Template.Spec.Containers[0].Env, + Dependencies: app.Dependencies, + } +} diff --git a/internal/utils/controller/cachekey_test.go b/internal/utils/controller/cachekey_test.go new file mode 100644 index 0000000..23166d3 --- /dev/null +++ b/internal/utils/controller/cachekey_test.go @@ -0,0 +1,273 @@ +package controller + +import ( + "testing" + + "github.com/stretchr/testify/assert" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + + appsv1 "github.com/Azure/operation-cache-controller/api/v1" +) + +func TestNewCacheKey(t *testing.T) { + tests := []struct { + name string + source AppCacheField + expected string + }{ + { + name: "basic", + source: AppCacheField{ + Name: "test-app-1", + Image: "nginx:latest", + Command: []string{"nginx"}, + Args: []string{"-g", "daemon off;"}, + WorkingDir: "/", + Env: []corev1.EnvVar{ + {Name: "ENV_VAR1", Value: "value1"}, + {Name: "ENV_VAR2", Value: "value2"}, + }, + }, + expected: "126075cdb21390e83c4703ace30f4e028568f38c193c9075171fea907570d23e", // Set the expected value after running the test once + }, + { + name: "basic", + source: AppCacheField{ + Name: "test-app-1", + Image: "nginx:latest", + Command: []string{"nginx"}, + Args: []string{"-g", "daemon off;"}, + WorkingDir: "/", + Env: []corev1.EnvVar{ + {Name: "ENV_VAR2", Value: "value2"}, + {Name: "ENV_VAR1", Value: "value1"}, + }, + }, + expected: "126075cdb21390e83c4703ace30f4e028568f38c193c9075171fea907570d23e", // Set the expected value after running the test once + }, + { + name: "basic with dependencies", + source: AppCacheField{ + Name: "test-app-2", + Image: "nginx:latest", + Command: []string{"nginx"}, + Args: []string{"-g", "daemon off;"}, + WorkingDir: "/", + Env: []corev1.EnvVar{ + {Name: "ENV_VAR1", Value: "value1"}, + {Name: "ENV_VAR2", Value: "value2"}, + }, + Dependencies: []string{"test-app-1"}, + }, + expected: "b5eab152dfbbb5d9d0f0b6c2920204b72991a86af0d6ebd673aaf4b80761b207", + }, + { + name: "basic with dependencies", + source: AppCacheField{ + Name: "test-app-2", + Image: "nginx:latest", + Command: []string{"nginx"}, + Args: []string{"-g", "daemon off;"}, + WorkingDir: "/", + Env: []corev1.EnvVar{ + {Name: "ENV_VAR2", Value: "value2"}, + {Name: "ENV_VAR1", Value: "value1"}, + }, + Dependencies: []string{"test-app-1"}, + }, + expected: "b5eab152dfbbb5d9d0f0b6c2920204b72991a86af0d6ebd673aaf4b80761b207", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := tt.source.NewCacheKey() + + assert.Equal(t, tt.expected, actual) + }) + } +} + +func TestNewCacheKeyFromApplications(t *testing.T) { + tests := []struct { + name string + source []appsv1.ApplicationSpec + expected string + }{ + { + name: "basic", + source: []appsv1.ApplicationSpec{ + { + Name: "test-app-1", + Provision: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "nginx:latest", + Command: []string{"nginx"}, + }, + }, + }, + }, + }, + }, + }, + expected: "fac2c1ee35f29e0cf01df3d2d087aa63f816cddcf37519b86f9788a8fe437db0", + }, + { + name: "basic with dependencies", + source: []appsv1.ApplicationSpec{ + { + Name: "test-app-1", + Provision: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "nginx:latest", + Command: []string{"nginx"}, + }, + }, + }, + }, + }, + }, + { + Name: "test-app-2", + Provision: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "nginx:latest", + Command: []string{"nginx"}, + }, + }, + }, + }, + }, + Dependencies: []string{"test-app-1"}, + }, + }, + expected: "4ad519825833e792749137e9ff7ad3efcab0907f9d3adc8751eef2cf871648a4", + }, + + { + name: "basic with multiple dependencies", + source: []appsv1.ApplicationSpec{ + { + Name: "test-app-1", + Provision: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "nginx:latest", + Command: []string{"nginx"}, + }, + }, + }, + }, + }, + }, + { + Name: "test-app-2", + Provision: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "nginx:latest", + Command: []string{"nginx"}, + }, + }, + }, + }, + }, + Dependencies: []string{"test-app-1"}, + }, + { + Name: "test-app-3", + Provision: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "nginx:latest", + Command: []string{"nginx"}, + }, + }, + }, + }, + }, + Dependencies: []string{"test-app-1", "test-app-2"}, + }, + }, + expected: "d9dc8143d50f0758ab226d2a199d3f884569750fa7c0a1928b2597af3eb3f7f0", + }, + { + name: "basic with multiple dependencies and different order", + source: []appsv1.ApplicationSpec{ + { + Name: "test-app-1", + Provision: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "nginx:latest", + Command: []string{"nginx"}, + }, + }, + }, + }, + }, + }, + { + Name: "test-app-3", + Provision: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "nginx:latest", + Command: []string{"nginx"}, + }, + }, + }, + }, + }, + Dependencies: []string{"test-app-2", "test-app-1"}, + }, + { + Name: "test-app-2", + Provision: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "nginx:latest", + Command: []string{"nginx"}, + }, + }, + }, + }, + }, + Dependencies: []string{"test-app-1"}, + }, + }, + + expected: "d9dc8143d50f0758ab226d2a199d3f884569750fa7c0a1928b2597af3eb3f7f0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := NewCacheKeyFromApplications(tt.source) + + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/internal/utils/controller/operation/const.go b/internal/utils/controller/operation/const.go new file mode 100644 index 0000000..206df5c --- /dev/null +++ b/internal/utils/controller/operation/const.go @@ -0,0 +1,17 @@ +package operation + +const ( + FinalizerName = "finalizer.operation.controller.azure.com" + AcquiredAnnotationKey = "operation.controller.azure.com/acquired" + + // PhaseEmpty is the phase when the operation is empty. + PhaseEmpty = "" + // PhaseReconciling is the phase when the operation is reconciling. + PhaseReconciling = "Reconciling" + // PhaseReconciled is the phase when the operation is reconciled. + PhaseReconciled = "Reconciled" + // Deletion is the phase when the operation is deleting. + PhaseDeleting = "Deleting" + // PhaseDeleted is the phase when the operation is deleted. + PhaseDeleted = "Deleted" +) diff --git a/internal/utils/controller/operation/operation.go b/internal/utils/controller/operation/operation.go new file mode 100644 index 0000000..164a11c --- /dev/null +++ b/internal/utils/controller/operation/operation.go @@ -0,0 +1,136 @@ +package operation + +import ( + "sort" + "strings" + + "github.com/google/uuid" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + + appsv1 "github.com/Azure/operation-cache-controller/api/v1" +) + +// NewOperationId generates a new operation id which is an UUID. +func NewOperationId() string { + return strings.Replace(uuid.New().String(), "-", "", -1) +} + +// DiffAppDeployments returns the difference between two slices of AppDeployment. +func DiffAppDeployments(expected, actual []appsv1.AppDeployment, + equals func(a, b appsv1.AppDeployment) bool) (added, removed, updated []appsv1.AppDeployment) { + // Find added and updated AppDeployments. + for _, e := range expected { + found := false + for _, a := range actual { + if a.Name == e.Name { + found = true + if !equals(a, e) { + updated = append(updated, a) + } + break + } + } + if !found { + added = append(added, e) + } + } + + // Find removed AppDeployments. + for _, a := range actual { + found := false + for _, e := range expected { + if a.Name == e.Name { + found = true + break + } + } + if !found { + removed = append(removed, a) + } + } + + return added, removed, updated +} + +func isJobResultIdentical(a, b batchv1.JobSpec) bool { + if len(a.Template.Spec.Containers) == 0 && len(b.Template.Spec.Containers) == 0 { + return true + } + if len(a.Template.Spec.Containers) == 0 || len(b.Template.Spec.Containers) == 0 { + return false + } + if a.Template.Spec.Containers[0].Image != b.Template.Spec.Containers[0].Image { + return false + } + if !isStringSliceIdentical(a.Template.Spec.Containers[0].Command, b.Template.Spec.Containers[0].Command) { + return false + } + if !isStringSliceIdentical(a.Template.Spec.Containers[0].Args, b.Template.Spec.Containers[0].Args) { + return false + } + if a.Template.Spec.Containers[0].WorkingDir != b.Template.Spec.Containers[0].WorkingDir { + return false + } + if !isEnvVarsIdentical(a.Template.Spec.Containers[0].Env, b.Template.Spec.Containers[0].Env) { + return false + } + + return true +} + +func isEnvVarsIdentical(a, b []corev1.EnvVar) bool { + if len(a) != len(b) { + return false + } + // sort environment variables to ensure consistent hashing + sort.Slice(a, func(i, j int) bool { + return a[i].Name < a[j].Name + }) + sort.Slice(b, func(i, j int) bool { + return b[i].Name < b[j].Name + }) + for i, env := range a { + if env.Name != b[i].Name || env.Value != b[i].Value { + return false + } + } + return true +} + +func isStringSliceIdentical(a, b []string) bool { + if len(a) != len(b) { + return false + } + sort.Strings(a) + sort.Strings(b) + for i, dep := range a { + if dep != b[i] { + return false + } + } + return true +} + +func CompareProvisionJobs(a, b appsv1.AppDeployment) bool { + if sameProvisionJob := isJobResultIdentical(a.Spec.Provision, b.Spec.Provision); !sameProvisionJob { + return false + } + // sort dependencies to ensure consistent hashing + sort.Strings(a.Spec.Dependencies) + sort.Strings(b.Spec.Dependencies) + for i, dep := range a.Spec.Dependencies { + if dep != b.Spec.Dependencies[i] { + return false + } + } + + return true +} + +func CompareTeardownJobs(a, b appsv1.AppDeployment) bool { + if sameTeardownJob := isJobResultIdentical(a.Spec.Teardown, b.Spec.Teardown); !sameTeardownJob { + return false + } + return true +} diff --git a/internal/utils/controller/operation/operation_test.go b/internal/utils/controller/operation/operation_test.go new file mode 100644 index 0000000..bb2569a --- /dev/null +++ b/internal/utils/controller/operation/operation_test.go @@ -0,0 +1,141 @@ +package operation + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + appsv1 "github.com/Azure/operation-cache-controller/api/v1" +) + +func TestNewOperationId(t *testing.T) { + tests := []struct { + name string + }{ + {name: "basic"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + id := NewOperationId() + assert.NotEmpty(t, id) + }) + } +} + +func TestDiffAppDeployments(t *testing.T) { + tests := []struct { + name string + expected []appsv1.AppDeployment + actual []appsv1.AppDeployment + equals func(a, b appsv1.AppDeployment) bool + wantAdded int + wantRemoved int + wantUpdated int + }{ + { + name: "basic example", + expected: []appsv1.AppDeployment{{ObjectMeta: metav1.ObjectMeta{Name: "app1"}}, {ObjectMeta: metav1.ObjectMeta{Name: "app2"}}}, + actual: []appsv1.AppDeployment{{ObjectMeta: metav1.ObjectMeta{Name: "app2"}}, {ObjectMeta: metav1.ObjectMeta{Name: "app3"}}}, + equals: func(a, b appsv1.AppDeployment) bool { return a.Name == b.Name }, + wantAdded: 1, wantRemoved: 1, wantUpdated: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + added, removed, updated := DiffAppDeployments(tt.expected, tt.actual, tt.equals) + assert.Equal(t, tt.wantAdded, len(added)) + assert.Equal(t, tt.wantRemoved, len(removed)) + assert.Equal(t, tt.wantUpdated, len(updated)) + }) + } +} + +func TestCompareProvisionJobs(t *testing.T) { + tests := []struct { + name string + a appsv1.AppDeployment + b appsv1.AppDeployment + want bool + }{ + { + name: "different images", + a: appsv1.AppDeployment{ + Spec: appsv1.AppDeploymentSpec{ + Provision: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Image: "image1"}}, + }, + }, + }, + Dependencies: []string{"dep1", "dep2"}, + }, + }, + b: appsv1.AppDeployment{ + Spec: appsv1.AppDeploymentSpec{ + Provision: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Image: "image2"}}, + }, + }, + }, + Dependencies: []string{"dep2", "dep1"}, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := CompareProvisionJobs(tt.a, tt.b) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestCompareTeardownJobs(t *testing.T) { + tests := []struct { + name string + a appsv1.AppDeployment + b appsv1.AppDeployment + want bool + }{ + { + name: "different teardown images", + a: appsv1.AppDeployment{ + Spec: appsv1.AppDeploymentSpec{ + Teardown: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Image: "image1"}}, + }, + }, + }, + }, + }, + b: appsv1.AppDeployment{ + Spec: appsv1.AppDeploymentSpec{ + Teardown: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Image: "image2"}}, + }, + }, + }, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := CompareTeardownJobs(tt.a, tt.b) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/utils/controller/operation/status.go b/internal/utils/controller/operation/status.go new file mode 100644 index 0000000..703b886 --- /dev/null +++ b/internal/utils/controller/operation/status.go @@ -0,0 +1,11 @@ +package operation + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + appsv1 "github.com/Azure/operation-cache-controller/api/v1" +) + +func ClearConditions(operation *appsv1.Operation) { + operation.Status.Conditions = []metav1.Condition{} +} diff --git a/internal/utils/controller/operation/status_test.go b/internal/utils/controller/operation/status_test.go new file mode 100644 index 0000000..72bff9d --- /dev/null +++ b/internal/utils/controller/operation/status_test.go @@ -0,0 +1,24 @@ +package operation + +import ( + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + appsv1 "github.com/Azure/operation-cache-controller/api/v1" +) + +func TestConditions(t *testing.T) { + operation := &appsv1.Operation{} + operation.Status.Conditions = []metav1.Condition{ + { + Type: "test", + Status: "test", + Reason: "test", + Message: "test", + }, + } + ClearConditions(operation) + assert.Equal(t, 0, len(operation.Status.Conditions)) +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 049bc1f..c1b51c2 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -89,6 +89,10 @@ var _ = Describe("Manager", Ordered, func() { By("removing manager namespace") cmd = exec.Command("kubectl", "delete", "ns", namespace) _, _ = utils.Run(cmd) + + By("deleting the operation clusterrolebinding") + cmd = exec.Command("kubectl", "delete", "clusterrolebinding", metricsRoleBindingName) + _, _ = utils.Run(cmd) }) // After each test, check for failures and collect logs, events,