diff --git a/.github/workflows/build-and-push.yaml b/.github/workflows/build-and-push.yaml index 8c9de159..5c1f979c 100644 --- a/.github/workflows/build-and-push.yaml +++ b/.github/workflows/build-and-push.yaml @@ -58,6 +58,8 @@ jobs: echo "MAIN IMAGE AT: ${{ vars.QUAY_RELEASE_REPO }}:latest" echo "LMES DRIVER IMAGE AT: ${{ vars.QUAY_RELEASE_LMES_DRIVER_REPO }}:latest" echo "LMES JOB IMAGE AT: ${{ vars.QUAY_RELEASE_LMES_JOB_REPO }}:latest" + echo "GUARDRAILS ORCH IMAGE AT: ${{ vars.QUAY_RELEASE_GUARDRAILS_REPO }}:latest" + echo "CI IMAGE AT: quay.io/trustyai/trustyai-service-operator-ci:${{ github.event.pull_request.head.sha }}" # # Set environments depending on context @@ -68,6 +70,8 @@ jobs: echo "IMAGE_NAME=quay.io/trustyai/trustyai-service-operator-ci" >> $GITHUB_ENV echo "DRIVER_IMAGE_NAME=quay.io/trustyai/ta-lmes-driver-ci" >> $GITHUB_ENV echo "JOB_IMAGE_NAME=quay.io/trustyai/ta-lmes-job-ci" >> $GITHUB_ENV + echo "ORCH_IMAGE_NAME=quay.io/trustyai/ta-guardrails-orchestrator-ci" >> $GITHUB_ENV + - name: Set main-branch environment if: env.BUILD_CONTEXT == 'main' run: | @@ -75,6 +79,8 @@ jobs: echo "IMAGE_NAME=${{ vars.QUAY_RELEASE_REPO }}" >> $GITHUB_ENV echo "DRIVER_IMAGE_NAME=${{ vars.QUAY_RELEASE_LMES_DRIVER_REPO }}" >> $GITHUB_ENV echo "JOB_IMAGE_NAME=${{ vars.QUAY_RELEASE_LMES_JOB_REPO }}" >> $GITHUB_ENV + echo "ORCH_IMAGE_NAME=${{ vars.QUAY_RELEASE_GUARDRAILS_REPO }}" >> $GITHUB_ENV + - name: Set tag environment if: env.BUILD_CONTEXT == 'tag' run: | @@ -82,6 +88,7 @@ jobs: echo "IMAGE_NAME=${{ vars.QUAY_RELEASE_REPO }}" >> $GITHUB_ENV echo "DRIVER_IMAGE_NAME=${{ vars.QUAY_RELEASE_LMES_DRIVER_REPO }}" >> $GITHUB_ENV echo "JOB_IMAGE_NAME=${{ vars.QUAY_RELEASE_LMES_JOB_REPO }}" >> $GITHUB_ENV + echo "ORCH_IMAGE_NAME=${{ vars.QUAY_RELEASE_GUARDRAILS_REPO }}" >> $GITHUB_ENV # Run docker commands - name: Put expiry date on CI-tagged image @@ -101,6 +108,10 @@ jobs: run: docker build -f Dockerfile.lmes-job -t ${{ env.JOB_IMAGE_NAME }}:$TAG . - name: Push LMES job image to Quay run: docker push ${{ env.JOB_IMAGE_NAME }}:$TAG + - name: Build Guardrails orchestrator image + run: docker build -f Dockerfile.guardrails -t ${{ env.ORCH_IMAGE_NAME }}:$TAG . + - name: Push Guardrails orchestrator image to Quay + run: docker push ${{ env.ORCH_IMAGE_NAME }}:$TAG # Create CI Manifests - name: Set up manifests for CI @@ -147,8 +158,10 @@ jobs: 📦 [LMES job image](https://quay.io/trustyai/ta-lmes-job:${{ github.event.pull_request.head.sha }}): `quay.io/trustyai/ta-lmes-job:${{ github.event.pull_request.head.sha }}` + 📦 [Guardrails orchestrator image](https://quay.io/trustyai/ta-guardrails-orchestrator:${{ github.event.pull_request.head.sha }}): `quay.io/trustyai/ta-guardrails-orchestrator:${{ github.event.pull_request.head.sha }}` + 🗂️ [CI manifests](https://github.com/trustyai-explainability/trustyai-service-operator-ci/tree/operator-${{ env.TAG }}) - + ``` devFlags: manifests: diff --git a/Dockerfile.orchestrator b/Dockerfile.orchestrator new file mode 100644 index 00000000..0cda1b3a --- /dev/null +++ b/Dockerfile.orchestrator @@ -0,0 +1,68 @@ +ARG UBI_MINIMAL_BASE_IMAGE=registry.access.redhat.com/ubi9/ubi-minimal +ARG UBI_BASE_IMAGE_TAG=latest +ARG PROTOC_VERSION=26.0 +ARG CONFIG_FILE=config/config.yaml + +## Rust builder ################################################################ +# Specific debian version so that compatible glibc version is used +FROM rust:1.80.1-bullseye as rust-builder +ARG PROTOC_VERSION + +ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse + +# Install protoc, no longer included in prost crate +RUN cd /tmp && \ + curl -L -O https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip && \ + unzip protoc-*.zip -d /usr/local && rm protoc-*.zip + +WORKDIR /app + +COPY rust-toolchain.toml rust-toolchain.toml + +RUN rustup component add rustfmt + +## Orchestrator builder ######################################################### +FROM rust-builder as fms-guardrails-orchestr8-builder + +COPY build.rs *.toml LICENSE /app/ +COPY ${CONFIG_FILE} /app/config/config.yaml +COPY protos/ /app/protos/ +COPY src/ /app/src/ + +WORKDIR /app + +# TODO: Make releases via cargo-release +RUN cargo install --root /app/ --path . + +## Tests stage ################################################################## +FROM fms-guardrails-orchestr8-builder as tests +RUN cargo test + +## Lint stage ################################################################### +FROM fms-guardrails-orchestr8-builder as lint +RUN cargo clippy --all-targets --all-features -- -D warnings + +## Formatting check stage ####################################################### +FROM fms-guardrails-orchestr8-builder as format +RUN cargo fmt --check + +## Release Image ################################################################ + +FROM ${UBI_MINIMAL_BASE_IMAGE}:${UBI_BASE_IMAGE_TAG} as fms-guardrails-orchestr8-release + +COPY --from=fms-guardrails-orchestr8-builder /app/bin/ /app/bin/ +COPY ${CONFIG_FILE} /app/config/config.yaml + +RUN microdnf install -y --disableplugin=subscription-manager shadow-utils compat-openssl11 && \ + microdnf clean all --disableplugin=subscription-manager + +RUN groupadd --system orchestr8 --gid 1001 && \ + adduser --system --uid 1001 --gid 0 --groups orchestr8 \ + --create-home --home-dir /app --shell /sbin/nologin \ + --comment "FMS Orchestrator User" orchestr8 + +USER orchestr8 + +ENV ORCHESTRATOR_CONFIG /app/config/config.yaml + +CMD /app/bin/fms-guardrails-orchestr8 \ No newline at end of file diff --git a/PROJECT b/PROJECT index 113c2fb1..4ab8efaf 100644 --- a/PROJECT +++ b/PROJECT @@ -5,6 +5,7 @@ domain: opendatahub.io layout: - go.kubebuilder.io/v4 +multigroup: true plugins: manifests.sdk.operatorframework.io/v2: {} scorecard.sdk.operatorframework.io/v2: {} @@ -33,4 +34,13 @@ resources: defaulting: true validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: opendatahub.io + group: gorch + kind: GuardrailsOrchestrator + path: github.com/trustyai-explainability/trustyai-service-operator/api/gorch/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/guardrails/v1alpha1/groupversion_info.go b/api/gorch/v1alpha1/groupversion_info.go similarity index 81% rename from api/guardrails/v1alpha1/groupversion_info.go rename to api/gorch/v1alpha1/groupversion_info.go index 9ed280ce..159a0677 100644 --- a/api/guardrails/v1alpha1/groupversion_info.go +++ b/api/gorch/v1alpha1/groupversion_info.go @@ -1,5 +1,5 @@ /* -Copyright 2024. +Copyright 2023. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1alpha1 contains API Schema definitions for the trustyai.opendatahub.io v1alpha1 API group +// Package v1alpha1 contains API Schema definitions for the gorch v1alpha1 API group. // +kubebuilder:object:generate=true // +groupName=trustyai.opendatahub.io package v1alpha1 @@ -28,14 +28,14 @@ const ( GroupName = "trustyai.opendatahub.io" Version = "v1alpha1" KindName = "GuardrailsOrchestrator" - FinalizerName = "trustyai.opendatahub.io/guardrails-finalizer" + FinalizerName = "trustyai.opendatahub.io/gorch-finalizer" ) var ( - // GroupVersion is group version used to register these objects - GroupVersion = schema.GroupVersion{Group: GroupName, Version: Version} + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "trustyai.opendatahub.io", Version: "v1alpha1"} - // SchemeBuilder is used to add go types to the GroupVersionKind scheme + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} // AddToScheme adds the types in this group-version to the given scheme. diff --git a/api/gorch/v1alpha1/guardrailsorchestrator_types.go b/api/gorch/v1alpha1/guardrailsorchestrator_types.go new file mode 100644 index 00000000..dbcf323d --- /dev/null +++ b/api/gorch/v1alpha1/guardrailsorchestrator_types.go @@ -0,0 +1,104 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + 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 TLSSpec struct { + Type string `json:"type"` + CertPath string `json:"cert_path"` + KeyPath string `json:"key_path"` + CACertPath string `json:"ca_cert_path"` +} + +type ServiceSpec struct { + Hostname string `json:"hostname"` + Port int `json:"port"` + TLS TLSSpec `json:"tls"` +} + +type GeneratorSpec struct { + Provider string `json:"provider"` + Service ServiceSpec `json:"service"` +} + +type ChunkerSpec struct { + Provider string `json:"provider"` + Service ServiceSpec `json:"service"` +} + +type DetectorSpec struct { + Type string `json:"type"` + Service ServiceSpec `json:"service"` + ChunkerName string `json:"chunkerName"` + DefaultThreshold string `json:"defaultThreshold"` +} + +// GuardrailsOrchestratorSpec defines the desired state of GuardrailsOrchestrator. +type GuardrailsOrchestratorSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Number of replicas + Replicas int32 `json:"replicas"` + // Generator configuration + Generator GeneratorSpec `json:"generator"` + // Chunker configuration + Chunkers []ChunkerSpec `json:"chunker"` + // Detector configuration + Detectors []DetectorSpec `json:"detectors"` + // TLS configuration + TLS string `json:"tls"` +} + +// GuardrailsOrchestratorStatus defines the observed state of GuardrailsOrchestrator. +type GuardrailsOrchestratorStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + Condition string `json:"condition,omitempty"` + Ready bool `json:"ready"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// GuardrailsOrchestrator is the Schema for the guardrailsorchestrators API. +type GuardrailsOrchestrator struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GuardrailsOrchestratorSpec `json:"spec,omitempty"` + Status GuardrailsOrchestratorStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// GuardrailsOrchestratorList contains a list of GuardrailsOrchestrator. +type GuardrailsOrchestratorList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GuardrailsOrchestrator `json:"items"` +} + +func init() { + SchemeBuilder.Register(&GuardrailsOrchestrator{}, &GuardrailsOrchestratorList{}) +} diff --git a/api/gorch/v1alpha1/zz_generated.deepcopy.go b/api/gorch/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..e649c26b --- /dev/null +++ b/api/gorch/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,114 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GuardrailsOrchestrator) DeepCopyInto(out *GuardrailsOrchestrator) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailsOrchestrator. +func (in *GuardrailsOrchestrator) DeepCopy() *GuardrailsOrchestrator { + if in == nil { + return nil + } + out := new(GuardrailsOrchestrator) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GuardrailsOrchestrator) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GuardrailsOrchestratorList) DeepCopyInto(out *GuardrailsOrchestratorList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GuardrailsOrchestrator, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailsOrchestratorList. +func (in *GuardrailsOrchestratorList) DeepCopy() *GuardrailsOrchestratorList { + if in == nil { + return nil + } + out := new(GuardrailsOrchestratorList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GuardrailsOrchestratorList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GuardrailsOrchestratorSpec) DeepCopyInto(out *GuardrailsOrchestratorSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailsOrchestratorSpec. +func (in *GuardrailsOrchestratorSpec) DeepCopy() *GuardrailsOrchestratorSpec { + if in == nil { + return nil + } + out := new(GuardrailsOrchestratorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GuardrailsOrchestratorStatus) DeepCopyInto(out *GuardrailsOrchestratorStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailsOrchestratorStatus. +func (in *GuardrailsOrchestratorStatus) DeepCopy() *GuardrailsOrchestratorStatus { + if in == nil { + return nil + } + out := new(GuardrailsOrchestratorStatus) + in.DeepCopyInto(out) + return out +} diff --git a/api/guardrails/v1alpha1/guardrailsorchestrator_types.go b/api/guardrails/v1alpha1/guardrailsorchestrator_types.go deleted file mode 100644 index d1a80582..00000000 --- a/api/guardrails/v1alpha1/guardrailsorchestrator_types.go +++ /dev/null @@ -1,171 +0,0 @@ -package v1alpha1 - -import ( - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func (g *GuardrailsOrchestrator) SetStatus(condType, reason, message string, status corev1.ConditionStatus) { - now := metav1.Now() - condition := Condition{ - Type: condType, - Status: status, - Reason: reason, - Message: message, - LastTransitionTime: now, - } - // Replace or append condition - found := false - for i, cond := range g.Status.Conditions { - if cond.Type == condType { - g.Status.Conditions[i] = condition - found = true - break - } - } - if !found { - g.Status.Conditions = append(g.Status.Conditions, condition) - } -} - -type ServiceSpec struct { - Hostname string `json:"hostname"` - Port int `json:"port"` - // TLS credentials path - TLS string `json:"tls"` -} - -type GeneratorSpec struct { - // Provider name - Provider string `json:"provider"` - // Service - Service ServiceSpec `json:"service"` -} - -type ChunkerSpec struct { - Type string `json:"type"` - Service ServiceSpec `json:"service"` -} - -type DetectorSpec struct { - // Detector name - Type string `json:"type"` - // Service - Service ServiceSpec `json:"service"` - // Chunker ID - ChunkerID string `json:"chunker_id"` - DefaultThreshold string `json:"default_threshold"` -} - -type TLSSpec struct { - Type string `json:"type"` - CertPath string `json:"cert_path"` - KeyPath string `json:"key_path"` - CACertPath string `json:"ca_cert_path"` -} - -// GuardrailsOrchestratorStatus defines the desired state of GuardrailsOrchestrator -type GuardrailsOrchestratorSpec struct { - // Number of replicas - Replicas int32 `json:"replicas"` - // Generator name - Generator GeneratorSpec `json:"generator"` - // Chunker name - Chunker ChunkerSpec `json:"chunker"` - // Detector name(s) - Detectors []DetectorSpec `json:"detectors"` - // TLS - TLS TLSSpec `json:"tls"` -} - -type GuardrailsOrchestratorPod struct { - Container *GuardrailsOrchestratorContainer `json:"container"` - Volumes []corev1.Volume `json:"volumes, omitempty"` -} - -// The following Getter-ish functions avoid nil pointer panic -func (p *GuardrailsOrchestratorPod) GetContainer() *GuardrailsOrchestratorContainer { - if p == nil { - return nil - } - return p.Container -} - -func (p *GuardrailsOrchestratorPod) GetVolumes() []corev1.Volume { - if p == nil { - return nil - } - return p.Volumes -} - -type GuardrailsOrchestratorContainer struct { - // Define Env information for the main container - // +optional - Env []corev1.EnvVar `json:"env,omitempty"` - // Define the volume mount information - // +optional - VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` - // Compute Resources required by this container. - // More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - // +optional - Resources *corev1.ResourceRequirements `json:"resources,omitempty"` -} - -// The following Getter-ish functions avoid nil pointer panic -func (c *GuardrailsOrchestratorContainer) GetEnv() []corev1.EnvVar { - if c == nil { - return nil - } - return c.Env -} - -func (c *GuardrailsOrchestratorContainer) GetVolumeMounts() []corev1.VolumeMount { - if c == nil { - return nil - } - return c.VolumeMounts -} - -func (c *GuardrailsOrchestratorContainer) GetResources() *corev1.ResourceRequirements { - if c == nil { - return nil - } - return c.Resources -} - -// Condition defines the condition of the GuardrailsOrchestrator -type Condition struct { - Type string `json:"type"` - Status corev1.ConditionStatus `json:"status"` - LastTransitionTime metav1.Time `json:"lastTransitionTime"` - Reason string `json:"reason"` - Message string `json:"message"` -} - -// GuardrailsOrchestratorStatus defines the observed state of GuardrailsOrchestrator -type GuardrailsOrchestratorStatus struct { - Phase string `json:"phase"` - Replicas int32 `json:"replicas"` - Conditions []Condition `json:"conditions"` - Ready corev1.ConditionStatus `json:"ready,omitempty"` -} - -// GuardrailsOrchestrator is the Schema for the GuardrailsOrchestrator API -type GuardrailsOrchestrator struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec GuardrailsOrchestratorSpec `json:"spec,omitempty"` - Status GuardrailsOrchestratorStatus `json:"status,omitempty"` -} - -// GuardrailsOrchestratorList contains a list of GuardrailsOrchestrator -type GuardrailsOrchestratorList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []GuardrailsOrchestrator `json:"items"` -} - -func init() { - SchemeBuilder.Register(&GuardrailsOrchestrator{}, &GuardrailsOrchestratorList{}) -} diff --git a/api/guardrails/v1alpha1/zz_generated.deepcopy.go b/api/guardrails/v1alpha1/zz_generated.deepcopy.go deleted file mode 100644 index a65dbeb3..00000000 --- a/api/guardrails/v1alpha1/zz_generated.deepcopy.go +++ /dev/null @@ -1,268 +0,0 @@ -//go:build !ignore_autogenerated - -/* -Copyright 2023. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Code generated by controller-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - "k8s.io/api/core/v1" -) - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ChunkerSpec) DeepCopyInto(out *ChunkerSpec) { - *out = *in - out.Service = in.Service -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChunkerSpec. -func (in *ChunkerSpec) DeepCopy() *ChunkerSpec { - if in == nil { - return nil - } - out := new(ChunkerSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Condition) DeepCopyInto(out *Condition) { - *out = *in - in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. -func (in *Condition) DeepCopy() *Condition { - if in == nil { - return nil - } - out := new(Condition) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DetectorSpec) DeepCopyInto(out *DetectorSpec) { - *out = *in - out.Service = in.Service -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DetectorSpec. -func (in *DetectorSpec) DeepCopy() *DetectorSpec { - if in == nil { - return nil - } - out := new(DetectorSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GeneratorSpec) DeepCopyInto(out *GeneratorSpec) { - *out = *in - out.Service = in.Service -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratorSpec. -func (in *GeneratorSpec) DeepCopy() *GeneratorSpec { - if in == nil { - return nil - } - out := new(GeneratorSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GuardrailsOrchestrator) DeepCopyInto(out *GuardrailsOrchestrator) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailsOrchestrator. -func (in *GuardrailsOrchestrator) DeepCopy() *GuardrailsOrchestrator { - if in == nil { - return nil - } - out := new(GuardrailsOrchestrator) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GuardrailsOrchestratorContainer) DeepCopyInto(out *GuardrailsOrchestratorContainer) { - *out = *in - if in.Env != nil { - in, out := &in.Env, &out.Env - *out = make([]v1.EnvVar, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.VolumeMounts != nil { - in, out := &in.VolumeMounts, &out.VolumeMounts - *out = make([]v1.VolumeMount, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Resources != nil { - in, out := &in.Resources, &out.Resources - *out = new(v1.ResourceRequirements) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailsOrchestratorContainer. -func (in *GuardrailsOrchestratorContainer) DeepCopy() *GuardrailsOrchestratorContainer { - if in == nil { - return nil - } - out := new(GuardrailsOrchestratorContainer) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GuardrailsOrchestratorList) DeepCopyInto(out *GuardrailsOrchestratorList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]GuardrailsOrchestrator, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailsOrchestratorList. -func (in *GuardrailsOrchestratorList) DeepCopy() *GuardrailsOrchestratorList { - if in == nil { - return nil - } - out := new(GuardrailsOrchestratorList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GuardrailsOrchestratorPod) DeepCopyInto(out *GuardrailsOrchestratorPod) { - *out = *in - if in.Container != nil { - in, out := &in.Container, &out.Container - *out = new(GuardrailsOrchestratorContainer) - (*in).DeepCopyInto(*out) - } - if in.Volumes != nil { - in, out := &in.Volumes, &out.Volumes - *out = make([]v1.Volume, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailsOrchestratorPod. -func (in *GuardrailsOrchestratorPod) DeepCopy() *GuardrailsOrchestratorPod { - if in == nil { - return nil - } - out := new(GuardrailsOrchestratorPod) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GuardrailsOrchestratorSpec) DeepCopyInto(out *GuardrailsOrchestratorSpec) { - *out = *in - out.Generator = in.Generator - out.Chunker = in.Chunker - if in.Detectors != nil { - in, out := &in.Detectors, &out.Detectors - *out = make([]DetectorSpec, len(*in)) - copy(*out, *in) - } - out.TLS = in.TLS -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailsOrchestratorSpec. -func (in *GuardrailsOrchestratorSpec) DeepCopy() *GuardrailsOrchestratorSpec { - if in == nil { - return nil - } - out := new(GuardrailsOrchestratorSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GuardrailsOrchestratorStatus) DeepCopyInto(out *GuardrailsOrchestratorStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailsOrchestratorStatus. -func (in *GuardrailsOrchestratorStatus) DeepCopy() *GuardrailsOrchestratorStatus { - if in == nil { - return nil - } - out := new(GuardrailsOrchestratorStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceSpec. -func (in *ServiceSpec) DeepCopy() *ServiceSpec { - if in == nil { - return nil - } - out := new(ServiceSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSpec. -func (in *TLSSpec) DeepCopy() *TLSSpec { - if in == nil { - return nil - } - out := new(TLSSpec) - in.DeepCopyInto(out) - return out -} diff --git a/cmd/main.go b/cmd/main.go index dcb17fef..be860bd5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -38,6 +38,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" + gorchv1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/gorch/v1alpha1" lmesv1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/lmes/v1alpha1" tasv1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/tas/v1alpha1" "github.com/trustyai-explainability/trustyai-service-operator/controllers" @@ -60,6 +61,7 @@ func init() { utilruntime.Must(kservev1beta1.AddToScheme(scheme)) utilruntime.Must(routev1.AddToScheme(scheme)) utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) + utilruntime.Must(gorchv1alpha1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } @@ -69,7 +71,7 @@ func main() { var probeAddr string var configMap string var enabledServices controllers.EnabledServices - flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address and port the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ @@ -92,11 +94,9 @@ func main() { mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, MetricsBindAddress: metricsAddr, - Port: 9443, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "b7e9931f.trustyai.opendatahub.io", - Namespace: "", // We are defining a cluster-scoped operator // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily // when the Manager ends. This requires the binary to immediately end when the // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly diff --git a/config/base/params.env b/config/base/params.env index e0de8034..a7780ee3 100644 --- a/config/base/params.env +++ b/config/base/params.env @@ -9,4 +9,5 @@ lmes-image-pull-policy=Always lmes-max-batch-size=24 lmes-default-batch-size=8 lmes-detect-device=true +guardrails-orchestrator-image=quay.io/rh-ee-mmisiura/fms-orchestr8-nlp:0.7.0 diff --git a/config/crd/bases/trustyai.opendatahub.io_guardrailsorchestrators.yaml b/config/crd/bases/trustyai.opendatahub.io_guardrailsorchestrators.yaml index 0f74df9c..6f484cfc 100644 --- a/config/crd/bases/trustyai.opendatahub.io_guardrailsorchestrators.yaml +++ b/config/crd/bases/trustyai.opendatahub.io_guardrailsorchestrators.yaml @@ -123,6 +123,1810 @@ spec: - provider - service type: object + pod: + description: Pod configuration + properties: + container: + properties: + env: + description: Define Env information for the main container + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + resources: + description: |- + Compute Resources required by this container. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + 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 + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + 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 + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + volumeMounts: + description: Define the volume mount information + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + required: + - image + type: object + volumes: + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk mount + on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk in + the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in the + blob storage + type: string + fsType: + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage account Managed: azure managed + data disk (only in managed availability set). defaults + to shared' + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the host + that shares a pod's lifetime + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + items: + type: string + type: array + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: boolean + secretFile: + description: |- + secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + secretRef: + description: |- + secretRef is Optional: SecretRef is reference to the authentication secret for User, default is empty. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: |- + defaultMode is optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers (Beta feature). + properties: + driver: + description: |- + driver is the name of the CSI driver that handles this volume. + Consult with your admin for the correct name as registered in the cluster. + type: string + fsType: + description: |- + fsType to mount. Ex. "ext4", "xfs", "ntfs". + If not provided, the empty value is passed to the associated CSI driver + which will determine the default filesystem to apply. + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to complete the CSI + NodePublishVolume and NodeUnpublishVolume calls. + This field is optional, and may be empty if no secret is required. If the + secret object contains more than one secret, all secret references are passed. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. Consult your driver's documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about the + pod that should populate this volume + properties: + defaultMode: + description: |- + Optional: mode bits to use on created files by default. Must be a + Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents information + to create the file containing the pod field + properties: + fieldRef: + description: 'Required: Selects a field of the + pod: only annotations, labels, name and namespace + are supported.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must not + be absolute or contain the ''..'' path. Must + be utf-8 encoded. The first item of the relative + path must not start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: http://kubernetes.io/docs/user-guide/volumes#emptydir + 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: + description: |- + ephemeral represents a volume that is handled by a cluster storage driver. + The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts, + and deleted when the pod is removed. + + Use this if: + a) the volume is only needed while the pod runs, + b) features of normal volumes like restoring from snapshot or capacity + tracking are needed, + c) the storage driver is specified through a storage class, and + d) the storage driver supports dynamic volume provisioning through + a PersistentVolumeClaim (see EphemeralVolumeSource for more + information on the connection between this volume type + and PersistentVolumeClaim). + + Use PersistentVolumeClaim or one of the vendor-specific + APIs for volumes that persist for longer than the lifecycle + of an individual pod. + + Use CSI for light-weight local ephemeral volumes if the CSI driver is meant to + be used that way - see the documentation of the driver for + more information. + + A pod can use both types of ephemeral volumes and + persistent volumes at the same time. + properties: + volumeClaimTemplate: + description: |- + Will be used to create a stand-alone PVC to provision the volume. + The pod in which this EphemeralVolumeSource is embedded will be the + owner of the PVC, i.e. the PVC will be deleted together with the + pod. The name of the PVC will be `-` where + `` is the name from the `PodSpec.Volumes` array + entry. Pod validation will reject the pod if the concatenated name + is not valid for a PVC (for example, too long). + + An existing PVC with that name that is not owned by the pod + will *not* be used for the pod to avoid using an unrelated + volume by mistake. Starting the pod is then blocked until + the unrelated PVC is removed. If such a pre-created PVC is + meant to be used by the pod, the PVC has to updated with an + owner reference to the pod once the pod exists. Normally + this should not be necessary, but it may be useful when + manually reconstructing a broken cluster. + + This field is read-only and no changes will be made by Kubernetes + to the PVC after it has been created. + + Required, must not be nil. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. No other fields are allowed and will be rejected during + validation. + type: object + spec: + description: |- + The specification for the PersistentVolumeClaim. The entire content is + copied unchanged into the PVC that gets created from this + template. The same fields as in a PersistentVolumeClaim + are also valid here. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references + one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + 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 + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + 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 + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource that + is attached to a kubelet's host machine and then exposed + to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target worldwide + names (WWNs)' + items: + type: string + type: array + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously. + items: + type: string + type: array + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to use + for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". The default filesystem depends on FlexVolume script. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the plugin scripts. This may be + empty if no secret object is specified. If the secret object + contains more than one secret, all secrets are passed to the plugin + scripts. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. This depends on the Flocker + control service being running + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as deprecated + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + properties: + fsType: + description: |- + fsType is filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + DEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an + EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir + into the Pod's container. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '.' is supplied, the volume directory will be the + git repository. Otherwise, if specified, the volume will contain the git repository in + the subdirectory with the given name. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the specified + revision. + type: string + required: + - repository + type: object + glusterfs: + description: |- + glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/glusterfs/README.md + properties: + endpoints: + description: |- + endpoints is the endpoint name that details Glusterfs topology. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. This is generally + used for system agents or other privileged things that are allowed + to see the host machine. Most containers will NOT need this. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://examples.k8s.io/volumes/iscsi/README.md + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi + type: string + initiatorName: + description: |- + initiatorName is the custom iSCSI Initiator Name. + If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface + : will be created for the connection. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: |- + portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: |- + targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + pdID: + description: pdID is the ID that identifies Photon Controller + persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources secrets, + configmaps, and downward API + properties: + defaultMode: + description: |- + defaultMode are the mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: sources is the list of volume projections + items: + description: Projection that may be projected along + with other supported volume types + properties: + configMap: + description: configMap information about the configMap + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the + ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about the + downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects a field + of the pod: only annotations, labels, + name and namespace are supported.' + properties: + apiVersion: + description: Version of the schema + the FieldPath is written in terms + of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to + select in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file to + be created. Must not be absolute or + contain the ''..'' path. Must be utf-8 + encoded. The first item of the relative + path must not start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env + vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + secret: + description: secret information about the secret + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: |- + audience is the intended audience of the token. A recipient of a token + must identify itself with an identifier specified in the audience of the + token, and otherwise should reject the token. The audience defaults to the + identifier of the apiserver. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. As the token approaches expiration, the kubelet volume + plugin will proactively rotate the service account token. The kubelet will + start trying to rotate the token if the token is older than 80 percent of + its time to live or if the token is older than 24 hours.Defaults to 1 hour + and must be at least 10 minutes. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + description: quobyte represents a Quobyte mount on the host + that shares a pod's lifetime + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple entries are separated with commas) + which acts as the central registry for volumes + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set by the plugin + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references an already + created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: |- + rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/rbd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + pool: + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". + Default is "xfs". + type: string + gateway: + description: gateway is the host address of the ScaleIO + API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the ScaleIO + Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. If this is not provided, Login operation will fail. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL communication + with Gateway, default false + type: boolean + storageMode: + description: |- + storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. + Default is ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage Pool + associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret + or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. If not specified, default values will be attempted. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: |- + volumeNamespace specifies the scope of the volume within StorageOS. If no + namespace is specified then the Pod's namespace will be used. This allows the + Kubernetes name scoping to be mirrored within StorageOS for tighter integration. + Set VolumeName to any name to override the default behaviour. + Set to "default" if you are not using namespaces within StorageOS. + Namespaces that do not pre-exist within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy Based + Management (SPBM) profile ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + required: + - container + type: object replicas: description: Number of replicas format: int32 @@ -148,6 +1952,7 @@ spec: - chunker - detectors - generator + - pod - replicas - tls type: object diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 4da9af27..5a783854 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -1,11 +1,12 @@ resources: - bases/trustyai.opendatahub.io_trustyaiservices.yaml - bases/trustyai.opendatahub.io_lmevaljobs.yaml + - bases/trustyai.opendatahub.io_guardrailsorchestrators.yaml #+kubebuilder:scaffold:crdkustomizeresource -patchesStrategicMerge: +patchesStrategicMerge: [] #+kubebuilder:scaffold:crdkustomizewebhookpatch #+kubebuilder:scaffold:crdkustomizecainjectionpatch configurations: - - kustomizeconfig.yaml + - kustomizeconfig.yaml \ No newline at end of file diff --git a/config/overlays/odh/params.env b/config/overlays/odh/params.env index f7ff45d0..a497fd4a 100644 --- a/config/overlays/odh/params.env +++ b/config/overlays/odh/params.env @@ -9,3 +9,4 @@ lmes-image-pull-policy=Always lmes-max-batch-size=24 lmes-default-batch-size=8 lmes-detect-device=true +guardrails-orchestrator-image=quay.io/rh-ee-mmisiura/fms-orchestr8-nlp:0.7.0 diff --git a/config/overlays/rhoai/params.env b/config/overlays/rhoai/params.env index 4f99c2a2..c1fe74e0 100644 --- a/config/overlays/rhoai/params.env +++ b/config/overlays/rhoai/params.env @@ -9,3 +9,4 @@ lmes-image-pull-policy=Always lmes-max-batch-size=24 lmes-default-batch-size=8 lmes-detect-device=true +guardrails-orchestrator-image=quay.io/rh-ee-mmisiura/fms-orchestr8-nlp:0.7.0 diff --git a/controllers/gorch.go b/controllers/gorch.go new file mode 100644 index 00000000..3eeaa522 --- /dev/null +++ b/controllers/gorch.go @@ -0,0 +1,23 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import "github.com/trustyai-explainability/trustyai-service-operator/controllers/gorch" + +func init() { + registerService(gorch.ServiceName, gorch.ControllerSetUp) +} diff --git a/controllers/gorch/constants.go b/controllers/gorch/constants.go new file mode 100644 index 00000000..f8b2c410 --- /dev/null +++ b/controllers/gorch/constants.go @@ -0,0 +1,9 @@ +package gorch + +const ( + finalizerName = "trustyai.opendatahub.io/gorch-finalizer" + orchestratorImage = "quay.io/rh-ee-mmisiura/fms-orchestr8-nlp:main_79434f" + orchestratorName = "orchestrator" + orchestratorNamespace = "default" + ServiceName = "GORCH" +) diff --git a/controllers/gorch/deployment.go b/controllers/gorch/deployment.go new file mode 100644 index 00000000..9ca493a2 --- /dev/null +++ b/controllers/gorch/deployment.go @@ -0,0 +1,35 @@ +package gorch + +import ( + "context" + "reflect" + + gorchv1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/gorch/v1alpha1" + templateParser "github.com/trustyai-explainability/trustyai-service-operator/controllers/tas/templates" + appsv1 "k8s.io/api/apps/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +var deploymentTemplatePath = "templates/deployment.yaml" + +type DeploymentConfig struct { + Orchestrator *gorchv1alpha1.GuardrailsOrchestrator + ContainerImage string + VolumeMountName string +} + +func (r *GuardrailsOrchestratorReconciler) createDeployment(ctx context.Context, orchestrator *gorchv1alpha1.GuardrailsOrchestrator) *appsv1.Deployment { + deploymentConfig := DeploymentConfig{ + Orchestrator: orchestrator, + ContainerImage: orchestratorImage, + VolumeMountName: "", + } + var deployment *appsv1.Deployment + deployment, err := templateParser.ParseResource[appsv1.Deployment](deploymentTemplatePath, deploymentConfig, reflect.TypeOf(appsv1.Deployment{})) + if err != nil { + log.FromContext(ctx).Error(err, "Failed to parse deployment template") + } + controllerutil.SetControllerReference(orchestrator, deployment, r.Scheme) + return deployment +} diff --git a/controllers/gorch/guardrailsorchestrator_controller.go b/controllers/gorch/guardrailsorchestrator_controller.go new file mode 100644 index 00000000..6667271c --- /dev/null +++ b/controllers/gorch/guardrailsorchestrator_controller.go @@ -0,0 +1,182 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gorch + +import ( + "context" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "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" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" + + routev1 "github.com/openshift/api/route/v1" + gorchv1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/gorch/v1alpha1" +) + +// GuardrailsOrchestratorReconciler reconciles a GuardrailsOrchestrator object +type GuardrailsOrchestratorReconciler struct { + client.Client + Scheme *runtime.Scheme + Namespace string + Recorder record.EventRecorder +} + +// +kubebuilder:rbac:groups=gorch.opendatahub.io,resources=guardrailsorchestrators,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=gorch.opendatahub.io,resources=guardrailsorchestrators/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=gorch.opendatahub.io,resources=guardrailsorchestrators/finalizers,verbs=update + +// The registered function to set up GORCH controller +func ControllerSetUp(mgr manager.Manager, ns string, recorder record.EventRecorder) error { + + return (&GuardrailsOrchestratorReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Namespace: ns, + Recorder: recorder, + }).SetupWithManager(mgr) +} + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the GuardrailsOrchestrator object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/reconcile +func (r *GuardrailsOrchestratorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) + + // TODO(user): your logic here + orchestrator := &gorchv1alpha1.GuardrailsOrchestrator{} + err := r.Get(ctx, req.NamespacedName, orchestrator) + if err != nil { + if errors.IsNotFound(err) { + log.Info("GuardrailsOrchestrator resource not found. Ignoring since object must be deleted.") + return ctrl.Result{}, nil + } + log.Error(err, "Failed to get GuardrailsOrchestrator") + return ctrl.Result{}, err + } + if !controllerutil.ContainsFinalizer(orchestrator, finalizerName) { + log.Info("Adding Finalizer for the GuardrailsOrchestrator") + if ok := controllerutil.AddFinalizer(orchestrator, finalizerName); !ok { + return ctrl.Result{Requeue: true}, nil + } + } + + if err = r.Update(ctx, orchestrator); err != nil { + log.Error(err, "Failed to update GuardrailsOrchestrator with finalizer") + return ctrl.Result{}, err + } + + // TO-DO: handle deletion of the orchestrator + if controllerutil.ContainsFinalizer(orchestrator, finalizerName) { + controllerutil.RemoveFinalizer(orchestrator, finalizerName) + if err = r.Update(ctx, orchestrator); err != nil { + log.Error(err, "Failed to update GuardrailsOrchestrator without finalizer") + return ctrl.Result{}, err + } + log.Info("Succesfully removed finalizer from GuardrailsOrchestrator") + } + + existingServiceAccount := &corev1.ServiceAccount{} + err = r.Get(ctx, types.NamespacedName{Name: orchestratorName, Namespace: orchestratorNamespace}, existingServiceAccount) + if err != nil && errors.IsNotFound(err) { + serviceAccount := r.createServiceAccount(ctx, orchestrator) + log.Info("Creating a new ServiceAccount", "ServiceAccount.Namespace", serviceAccount.Namespace, "ServiceAccount.Name", serviceAccount.Name) + err = r.Create(ctx, serviceAccount) + if err != nil { + log.Error(err, "Failed to create new ServiceAccount", "ServiceAccount.Namespace", serviceAccount.Namespace, "ServiceAccount.Name", serviceAccount.Name) + return ctrl.Result{}, err + } + // ServiceAccount created successfully - return and requeue + return ctrl.Result{Requeue: true}, nil + } else if err != nil { + log.Error(err, "Failed to get ServiceAccount") + return ctrl.Result{}, err + } + + existingDeployment := &appsv1.Deployment{} + err = r.Get(ctx, types.NamespacedName{Name: orchestratorName, Namespace: orchestratorNamespace}, existingDeployment) + if err != nil && errors.IsNotFound(err) { + // Define a new deployment + deployment := r.createDeployment(ctx, orchestrator) + log.Info("Creating a new Deployment", "Deployment.Namespace", deployment.Namespace, "Deployment.Name", deployment.Name) + err = r.Create(ctx, deployment) + if err != nil { + log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", deployment.Namespace, "Deployment.Name", deployment.Name) + return ctrl.Result{}, err + } + // Deployment created successfully - return and requeue + return ctrl.Result{Requeue: true}, nil + } else if err != nil { + log.Error(err, "Failed to get Deployment") + return ctrl.Result{}, err + } + + existingService := &corev1.Service{} + err = r.Get(ctx, types.NamespacedName{Name: orchestratorName, Namespace: orchestratorNamespace}, existingService) + if err != nil && errors.IsNotFound(err) { + // Define a new service + service := r.createService(ctx, orchestrator) + log.Info("Creating a new Service", "Service.Namespace", service.Namespace, "Service.Name", service.Name) + err = r.Create(ctx, service) + if err != nil { + log.Error(err, "Failed to create new Service", "Service.Namespace", service.Namespace, "Service.Name", service.Name) + return ctrl.Result{}, err + } + // Service created successfully - return and requeue + return ctrl.Result{Requeue: true}, nil + } else if err != nil { + log.Error(err, "Failed to get Service") + return ctrl.Result{}, err + } + + existingRoute := &routev1.Route{} + err = r.Get(ctx, types.NamespacedName{Name: orchestratorName, Namespace: orchestratorNamespace}, existingRoute) + if err != nil && errors.IsNotFound(err) { + // Define a new route + route := r.createRoute(ctx, orchestrator) + log.Info("Creating a new Route", "Route.Namespace", route.Namespace, "Route.Name", route.Name) + err = r.Create(ctx, route) + if err != nil { + log.Error(err, "Failed to create new Route", "Route.Namespace", route.Namespace, "Route.Name", route.Name) + return ctrl.Result{}, err + } + // Route created successfully - return and requeue + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *GuardrailsOrchestratorReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&gorchv1alpha1.GuardrailsOrchestrator{}). + Owns(&appsv1.Deployment{}). + Complete(r) +} diff --git a/controllers/gorch/guardrailsorchestrator_controller_test.go b/controllers/gorch/guardrailsorchestrator_controller_test.go new file mode 100644 index 00000000..9346e00d --- /dev/null +++ b/controllers/gorch/guardrailsorchestrator_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gorch + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + gorchv1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/gorch/v1alpha1" +) + +var _ = Describe("GuardrailsOrchestrator Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + guardrailsorchestrator := &gorchv1alpha1.GuardrailsOrchestrator{} + + BeforeEach(func() { + By("creating the custom resource for the Kind GuardrailsOrchestrator") + err := k8sClient.Get(ctx, typeNamespacedName, guardrailsorchestrator) + if err != nil && errors.IsNotFound(err) { + resource := &gorchv1alpha1.GuardrailsOrchestrator{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &gorchv1alpha1.GuardrailsOrchestrator{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance GuardrailsOrchestrator") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &GuardrailsOrchestratorReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/controllers/gorch/route.go b/controllers/gorch/route.go new file mode 100644 index 00000000..97cb2733 --- /dev/null +++ b/controllers/gorch/route.go @@ -0,0 +1,36 @@ +package gorch + +import ( + "context" + "reflect" + + routev1 "github.com/openshift/api/route/v1" + gorchv1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/gorch/v1alpha1" + templateParser "github.com/trustyai-explainability/trustyai-service-operator/controllers/tas/templates" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const routeTemplatePath = "templates/route.yaml" + +type RouteConfig struct { + Name string + Namespace string + PortName string +} + +func (r *GuardrailsOrchestratorReconciler) createRoute(ctx context.Context, orchestrator *gorchv1alpha1.GuardrailsOrchestrator) *routev1.Route { + + routeConfig := RouteConfig{ + Name: orchestrator.Name, + Namespace: orchestrator.Namespace, + PortName: "http", + } + var route *routev1.Route + route, err := templateParser.ParseResource[routev1.Route](routeTemplatePath, routeConfig, reflect.TypeOf(routev1.Route{})) + if err != nil { + log.FromContext(ctx).Error(err, "Failed to parse route template") + } + controllerutil.SetControllerReference(orchestrator, route, r.Scheme) + return route +} diff --git a/controllers/gorch/service.go b/controllers/gorch/service.go new file mode 100644 index 00000000..6a2b6499 --- /dev/null +++ b/controllers/gorch/service.go @@ -0,0 +1,37 @@ +package gorch + +import ( + "context" + "reflect" + + gorchv1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/gorch/v1alpha1" + "github.com/trustyai-explainability/trustyai-service-operator/controllers/constants" + templateParser "github.com/trustyai-explainability/trustyai-service-operator/controllers/tas/templates" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const serviceTemplatePath = "templates/service.yaml" + +type ServiceConfig struct { + Name string + Namespace string + Version string +} + +func (r *GuardrailsOrchestratorReconciler) createService(ctx context.Context, orchestrator *gorchv1alpha1.GuardrailsOrchestrator) *corev1.Service { + + serviceConfig := ServiceConfig{ + Name: orchestrator.Name, + Namespace: orchestrator.Namespace, + Version: constants.Version, + } + var service *corev1.Service + service, err := templateParser.ParseResource[corev1.Service](serviceTemplatePath, serviceConfig, reflect.TypeOf(corev1.Service{})) + if err != nil { + log.FromContext(ctx).Error(err, "Failed to parse service template") + } + controllerutil.SetControllerReference(orchestrator, service, r.Scheme) + return service +} diff --git a/controllers/gorch/service_account.go b/controllers/gorch/service_account.go new file mode 100644 index 00000000..557e1b89 --- /dev/null +++ b/controllers/gorch/service_account.go @@ -0,0 +1,34 @@ +package gorch + +import ( + "context" + "reflect" + + gorchv1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/gorch/v1alpha1" + templateParser "github.com/trustyai-explainability/trustyai-service-operator/controllers/tas/templates" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const serviceAccountTemplatePath = "templates/service_account.yaml" + +type ServiceAccountConfig struct { + Name string + Namespace string +} + +func (r *GuardrailsOrchestratorReconciler) createServiceAccount(ctx context.Context, orchestrator *gorchv1alpha1.GuardrailsOrchestrator) *corev1.ServiceAccount { + + serviceAccountConfig := ServiceAccountConfig{ + Name: orchestrator.Name, + Namespace: orchestrator.Namespace, + } + var serviceAccount *corev1.ServiceAccount + serviceAccount, err := templateParser.ParseResource[corev1.ServiceAccount](serviceAccountTemplatePath, serviceAccountConfig, reflect.TypeOf(corev1.ServiceAccount{})) + if err != nil { + log.FromContext(ctx).Error(err, "Failed to parse service account template") + } + controllerutil.SetControllerReference(orchestrator, serviceAccount, r.Scheme) + return serviceAccount +} diff --git a/controllers/gorch/suite_test.go b/controllers/gorch/suite_test.go new file mode 100644 index 00000000..133b3f63 --- /dev/null +++ b/controllers/gorch/suite_test.go @@ -0,0 +1,210 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gorch + +import ( + "context" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + routev1 "github.com/openshift/api/route/v1" + "golang.org/x/sys/unix" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + gorchv1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/gorch/v1alpha1" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context +var cancel context.CancelFunc + +func createGuardrailsOrchestrator(ctx context.Context, orchestratorName string, namespace string) error { + typedNamespacedName := types.NamespacedName{Name: orchestratorName, Namespace: namespace} + err := k8sClient.Get(ctx, typedNamespacedName, &gorchv1alpha1.GuardrailsOrchestrator{}) + if err != nil && errors.IsNotFound(err) { + gorch := &gorchv1alpha1.GuardrailsOrchestrator{ + ObjectMeta: metav1.ObjectMeta{ + Name: orchestratorName, + Namespace: namespace, + }, + Spec: gorchv1alpha1.GuardrailsOrchestratorSpec{}, + } + err = k8sClient.Create(ctx, gorch) + } + return err +} + +func deleteGuardrailsOrchestrator(ctx context.Context, orchestratorName string) error { + typedNamespacedName := types.NamespacedName{Name: orchestratorName} + err := k8sClient.Get(ctx, typedNamespacedName, &gorchv1alpha1.GuardrailsOrchestrator{}) + if err == nil { + gorch := &gorchv1alpha1.GuardrailsOrchestrator{ + ObjectMeta: metav1.ObjectMeta{ + Name: typedNamespacedName.Name, + Namespace: typedNamespacedName.Namespace, + }, + } + k8sClient.Delete(ctx, gorch) + } + return err +} + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +func testCreateDeleteGuardrailsOrchestrator(orchestratorName string, namespaceName string) { + It("Should sucessfully reconcile creating a custom resource for the GuardrailsOrchestrator", func() { + By("Creating a custom resource for the GuardrailsOrchestrator") + ctx := context.Background() + typedNamespacedName := types.NamespacedName{Name: orchestratorName, Namespace: namespaceName} + err := createGuardrailsOrchestrator(ctx, orchestratorName, namespaceName) + Expect(err).ToNot(HaveOccurred()) + + By("Checking if the custom resource was successfully created") + err = k8sClient.Get(ctx, typedNamespacedName, &gorchv1alpha1.GuardrailsOrchestrator{}) + Expect(err).ToNot(HaveOccurred()) + + By("Reconciling the custom resource that was created") + reconciler := &GuardrailsOrchestratorReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + // Namespace: namespaceName, + // EventRecorder: &record.FakeRecorder{}, + } + _, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: typedNamespacedName}) + Expect(err).ToNot(HaveOccurred()) + + By("Checking if resources were successfully created in the reconcilation") + Eventually(func() error { + serviceAccount := &corev1.ServiceAccount{} + if err := k8sClient.Get(ctx, types.NamespacedName{Name: orchestratorName, Namespace: namespaceName}, serviceAccount); err != nil { + return err + } + + deployment := &appsv1.Deployment{} + if err := k8sClient.Get(ctx, types.NamespacedName{Name: orchestratorName, Namespace: namespaceName}, deployment); err != nil { + return err + } + + Expect(*deployment.Spec.Replicas).Should(Equal(int32(1))) + Expect(deployment.Namespace).Should(Equal(namespaceName)) + Expect(deployment.Name).Should(Equal(orchestratorName)) + Expect(deployment.Labels["app"]).Should(Equal(orchestratorName)) + + service := &corev1.Service{} + if err := k8sClient.Get(ctx, types.NamespacedName{Name: orchestratorName, Namespace: namespaceName}, service); err != nil { + return err + } + Expect(service.Namespace).Should(Equal(namespaceName)) + + if err := k8sClient.Get(ctx, types.NamespacedName{Name: orchestratorName, Namespace: namespaceName}, &routev1.Route{}); err != nil { + return err + } + return nil + }, time.Second*10, time.Millisecond*10).Should(Succeed()) + + By("Deleting the custom resource for the GuardrailsOrchestrator") + err = deleteGuardrailsOrchestrator(ctx, orchestratorName) + Expect(err).ToNot(HaveOccurred()) + + By("Reconciling the custom resource that was deleted") + _, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: typedNamespacedName}) + Expect(err).ToNot(HaveOccurred()) + + By("Checking if resources were successfully deleted in the reconcilation") + Eventually(func() error { + if err := k8sClient.Get(ctx, typedNamespacedName, &corev1.ServiceAccount{}); err == nil { + return nil + } + if err := k8sClient.Get(ctx, typedNamespacedName, &appsv1.Deployment{}); err == nil { + return nil + } + if err := k8sClient.Get(ctx, typedNamespacedName, &corev1.Service{}); err == nil { + return nil + } + if err := k8sClient.Get(ctx, typedNamespacedName, &routev1.Route{}); err == nil { + return nil + } + return unix.ENOENT + }, time.Second*10, time.Millisecond*100).ShouldNot(Succeed()) + }) +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = gorchv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) + +var _ = Describe("GuardrailsOrchestrator Controller", func() { + Context("GuardrailsOrchestrator Controller Test", func() { + testCreateDeleteGuardrailsOrchestrator("test-orchestrator", "test-namespace") + }) +}) diff --git a/controllers/gorch/templates/deployment.tmpl.yaml b/controllers/gorch/templates/deployment.tmpl.yaml new file mode 100644 index 00000000..bc727c9a --- /dev/null +++ b/controllers/gorch/templates/deployment.tmpl.yaml @@ -0,0 +1,76 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: {{ .Orchestrator.Name }} + namespace: {{ .Orchestrator.Namespace }} + annotations: + sidecar.istio.io/inject: "true" + labels: + app: {{ .Orchestrator.Name }} + component: {{ .Orchestrator.Name }} + deploy-name: {{ .Orchestrator.Name }} + spec: + replicas: 1 + selector: + matchLabels: + app: {{ .Orchestrator.Name }} + component: {{ .Orchestrator.Name }} + deploy-name: {{ .Orchestrator.Name }} + template: + metadata: + labels: + app: {{ .Orchestrator.Name }} + component: {{ .Orchestrator.Name }} + deploy-name: {{ .Orchestrator.Name }} + annotations: + sidecar.istio.io/inject: "true" + spec: + serviceAccountName: {{ .Orchestrator.Name }}-serviceaccount + containers: + - resources: + limits: + cpu: '1' + memory: 2Gi + requests: + cpu: '1' + memory: 2Gi + readinessProbe: + httpGet: + path: /health + port: 8033 + scheme: HTTP + initialDelaySeconds: 10 + timeoutSeconds: 10 + periodSeconds: 20 + successThreshold: 1 + failureThreshold: 3 + terminationMessagePath: /dev/termination-log + name: {{ .Orchestrator.Name }} + command: + - /app/bin/fms-guardrails-orchestr8 + env: + - name: ORCHESTRATOR_CONFIG + value: /config/config.yaml + - name: HTTP_PORT + value: '8033' + - name: RUST_BACKTRACE + value: 'full' + - name: RUST_LOG + value: 'fms_guardrails_orchestr8=trace,tower_grpc=debug' + securityContext: + capabilities: + drop: + - ALL + privileged: false + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + ports: + - name: http + containerPort: 8033 + protocol: TCP + imagePullPolicy: Always + terminationMessagePolicy: File + image: {{ .Orchestrator.Image }} \ No newline at end of file diff --git a/controllers/gorch/templates/role-binding.tmpl.yaml b/controllers/gorch/templates/role-binding.tmpl.yaml new file mode 100644 index 00000000..15a46cc0 --- /dev/null +++ b/controllers/gorch/templates/role-binding.tmpl.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ .Orchestrator.Name }}-rolebinding + namespace: {{ .Orchestrator.Namespace }} +subjects: + - kind: ServiceAccount + name: {{ .Orchestrator.Name }}-serviceaccount +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Orchestrator.Name }}-role \ No newline at end of file diff --git a/controllers/gorch/templates/route-health.tmpl.yaml b/controllers/gorch/templates/route-health.tmpl.yaml new file mode 100644 index 00000000..85242010 --- /dev/null +++ b/controllers/gorch/templates/route-health.tmpl.yaml @@ -0,0 +1,18 @@ +kind: Route +apiVersion: route.openshift.io/v1 +metadata: + name: {{ .Orchestrator.Name }}-health + labels: + app: {{ .Orchestrator.Name }} + component: {{ .Orchestrator.Name }} +spec: + to: + kind: Service + name: {{ .Orchestrator.Name }}-service + weight: 100 + port: + targetPort: health + tls: + termination: edge + insecureEdgeTerminationPolicy: Redirect + wildcardPolicy: None \ No newline at end of file diff --git a/controllers/gorch/templates/route-http.tmpl.yaml b/controllers/gorch/templates/route-http.tmpl.yaml new file mode 100644 index 00000000..10e79f1f --- /dev/null +++ b/controllers/gorch/templates/route-http.tmpl.yaml @@ -0,0 +1,19 @@ +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: {{ .Name }}-http + namespace: {{ .Orchestrator.Namespace }} + labels: + app: {{ .Orchestrator.Name }} + component: {{ .Orchestrator.Name }} +spec: + to: + kind: Service + name: {{ .Orchestrator.Name }}-service + weight: 100 + port: + targetPort: health + tls: + termination: edge + insecureEdgeTerminationPolicy: Redirect + wildcardPolicy: None \ No newline at end of file diff --git a/controllers/gorch/templates/service.tmpl.yaml b/controllers/gorch/templates/service.tmpl.yaml new file mode 100644 index 00000000..2f53a257 --- /dev/null +++ b/controllers/gorch/templates/service.tmpl.yaml @@ -0,0 +1,28 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Orchestrator.Name }}-service + namespace: {{ .Orchestrator.Namespace }} + labels: + app: {{ .Orchestrator.Name}} + component: {{ .Orchestrator.Name }} +spec: + ipFamilies: + - IPv4 + - name: http + protocol: TCP + port: 8033 + targetPort: 8033 + - name: health + protocol: TCP + port: 8034 + targetPort: 8034 + internalTrafficPolicy: Cluster + type: ClusterIP + ipFamilyPolicy: SingleStack + sessionAffinity: None + selector: + app: {{ .Orchestrator.Name }} + component: {{ .Orchestrator.Name }} + deploy-name: {{ .Orchestrator.Name }} + diff --git a/controllers/gorch/templates/serviceaccount.tmpl.yaml b/controllers/gorch/templates/serviceaccount.tmpl.yaml new file mode 100644 index 00000000..9110d0bf --- /dev/null +++ b/controllers/gorch/templates/serviceaccount.tmpl.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Orchestrator.Name }}-serviceaccount + namespace: {{ Orchestrator.Namespace }} \ No newline at end of file diff --git a/controllers/guardrails/constants.go b/controllers/guardrails/constants.go deleted file mode 100644 index a5003f48..00000000 --- a/controllers/guardrails/constants.go +++ /dev/null @@ -1,9 +0,0 @@ -package guardrails - -const ( - GuardrailsOrchestratorPath = "/app/bin/fms-guaradrails-orchestr8" - DefaultContainerImage = " quay.io/rh-ee-mmisiura/fms-orchestr8-nlp:latest" - OrchestratorName = "fms-orchestr8-nlp" - FinalizerName = "trustyai.opendatahub.io/finalizer" - HTTPSPort = "443" -) diff --git a/controllers/guardrails/deployment.go b/controllers/guardrails/deployment.go deleted file mode 100644 index 231e4dcc..00000000 --- a/controllers/guardrails/deployment.go +++ /dev/null @@ -1,140 +0,0 @@ -package guardrails - -import ( - "context" - - "github.com/go-logr/logr" - guardrailsv1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/guardrails/v1alpha1" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" -) - -func createDeployment(orchestrator *guardrailsv1alpha1.GuardrailsOrchestrator, log logr.Logger) *appsv1.Deployment { - var allowPrivilegeEscalation = false - var privileged = false - var runAsNonRootUser = true - var command = []string{"/app/bin/fms-guardrails-orchestr8"} - var devTerminationPath = "/dev/termination-log" - - var envVars = []corev1.EnvVar{ - { - Name: "HTTPS_PORT", - Value: HTTPSPort, - }, - } - envVars = append(envVars, orchestrator.Spec.Pod.GetContainer().Env...) - - var volumeMounts = []corev1.VolumeMount{ - { - Name: "server-tls", - ReadOnly: true, - MountPath: "/tls/server", - }, - } - volumeMounts = append(volumeMounts, orchestrator.Spec.Pod.GetContainer().VolumeMounts...) - - var volumes = []corev1.Volume{} - volumes = append(volumes, orchestrator.Spec.Pod.Volumes...) - - var labels = map[string]string{ - "app": "fmstack-nlp", - "component": orchestrator.Name, - "deploy-name": orchestrator.Name, - } - - var annotations = map[string]string{} - - var resources = corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("1"), - corev1.ResourceMemory: resource.MustParse("2Gi"), - corev1.ResourceRequestsCPU: resource.MustParse("1"), - corev1.ResourceRequestsMemory: resource.MustParse("2Gi"), - }, - } - - var readinessProbe = &corev1.Probe{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/health", - Port: HTTPSPort, - Scheme: "HTTP", - }, - InitialDelaySeconds: 5, - TimeoutSeconds: 5, - PeriodSeconds: 10, - SuccessThreshold: 1, - FailureThreshold: 3, - } - - var ports = []corev1.ContainerPort{{ - Name: "http", - ContainerPort: int32(443), - Protocol: corev1.ProtocolTCP, - }} - - deployment := appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: orchestrator.Name, - Namespace: orchestrator.Namespace, - Annotations: annotations, - Labels: labels, - }, - Spec: appsv1.DeploymentSpec{ - Replicas: &orchestrator.Spec.Replicas, - Selector: &metav1.LabelSelector{}, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: corev1.PodSpec{ - - Containers: []corev1.Container{ - { - Name: orchestrator.Name, - Image: orchestrator.Spec.Pod.GetContainer().Image, - ImagePullPolicy: corev1.PullAlways, - Command: command, - Env: envVars, - SecurityContext: &corev1.SecurityContext{ - AllowPrivilegeEscalation: &allowPrivilegeEscalation, - Capabilities: &corev1.Capabilities{ - Drop: []corev1.Capability{ - "ALL", - }, - }, - Privileged: &privileged, - RunAsNonRoot: &runAsNonRootUser, - SeccompProfile: &corev1.SeccompProfile{ - Type: corev1.SeccompProfileTypeRuntimeDefault, - }, - }, - VolumeMounts: volumeMounts, - Resources: resources, - ReadinessProbe: readinessProbe, - TerminationMessagePath: devTerminationPath, - Ports: ports, - }, - }, - Volumes: volumes, - RestartPolicy: corev1.RestartPolicyNever, - }, - }, - }, - } - return &deployment -} - -func (r *GuardrailsReconciler) deleteDeployment(ctx context.Context, orchestrator *guardrailsv1alpha1.GuardrailsOrchestrator) error { - deployment := appsv1.Deployment{ - TypeMeta: metav1.TypeMeta{ - Kind: "Deployment", - APIVersion: "apps/v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: orchestrator.Name, - Namespace: orchestrator.Namespace, - }, - } - return r.Delete(ctx, &deployment) -} diff --git a/controllers/guardrails/guardrails_orchestrator.go b/controllers/guardrails/guardrails_orchestrator.go deleted file mode 100644 index d6fe2f48..00000000 --- a/controllers/guardrails/guardrails_orchestrator.go +++ /dev/null @@ -1,88 +0,0 @@ -package guardrails - -import ( - "context" - - guardrailsv1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/guardrails/v1alpha1" - "github.com/trustyai-explainability/trustyai-service-operator/controllers/utils" - appsv1 "k8s.io/api/apps/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" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/source" -) - -type GuardrailsReconciler struct { - client.Client - Scheme *runtime.Scheme - EventRecorder record.EventRecorder - Namespace string -} - -func ControllerSetUp(mgr ctrl.Manager, recorder record.EventRecorder, ns string) error { - return (&GuardrailsReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - EventRecorder: recorder, - Namespace: ns, - }).SetupWithManager(mgr) -} - -func (r *GuardrailsReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - // your logic here - log := log.FromContext(ctx) - orchestrator := &guardrailsv1alpha1.GuardrailsOrchestrator{} - err := r.Get(context.TODO(), req.NamespacedName, orchestrator) - if err != nil { - log.Error(err, "unable to fetch GuardrailsOrchestrator") - return ctrl.Result{}, err - } - - // Handle deletion - if orchestrator.DeletionTimestamp != nil { - if utils.ContainsString(orchestrator.Finalizers, FinalizerName) { - // Delete the deployment - - if err := r.deleteDeployment(ctx, orchestrator); err != nil { - log.Error(err, "unable to delete GuardrailsOrchestrator deployment") - } - log.Info("deleted GuardrailsOrchestrator deployment") - } - orchestrator.Finalizers = utils.RemoveString(orchestrator.Finalizers, FinalizerName) - if err := r.Update(ctx, orchestrator); err != nil { - log.Error(err, "failed to remove the finalizer") - return ctrl.Result{Requeue: true}, nil - } - return ctrl.Result{Requeue: true}, nil - } - - // Handle creation - if !utils.ContainsString(orchestrator.Finalizers, FinalizerName) { - orchestrator.Finalizers = append(orchestrator.Finalizers, FinalizerName) - if err := r.Update(ctx, orchestrator); err != nil { - log.Error(err, "failed to add the finalizer") - } - } - deployment := createDeployment(orchestrator, log) - if err := r.Create(ctx, deployment); err != nil { - log.Error(err, "unable to create deployment") - return ctrl.Result{}, err - } - return ctrl.Result{}, nil -} - -func (r *GuardrailsReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&guardrailsv1alpha1.GuardrailsOrchestrator{}). - Watches( - &source.Kind{Type: &appsv1.Deployment{}}, - &handler.EnqueueRequestForOwner{ - OwnerType: &guardrailsv1alpha1.GuardrailsOrchestrator{}, - IsController: true, - }, - ). - Complete(r) -} diff --git a/controllers/guardrails/guardrails_orchestrator_test.go b/controllers/guardrails/guardrails_orchestrator_test.go deleted file mode 100644 index dd87aeb5..00000000 --- a/controllers/guardrails/guardrails_orchestrator_test.go +++ /dev/null @@ -1,164 +0,0 @@ -package guardrails - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - guardrailsv1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/guardrails/v1alpha1" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -var ( - allowPrivilegeEscalation = false - privileged = false - runAsNonRootUser = true - command = []string{"/app/bin/fms-guardrails-orchestr8"} - devTerminationPath = "/dev/termination-log" - resources = corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("1"), - corev1.ResourceMemory: resource.MustParse("2Gi"), - corev1.ResourceRequestsCPU: resource.MustParse("1"), - corev1.ResourceRequestsMemory: resource.MustParse("2Gi"), - }, - } - readinessProbe = &corev1.Probe{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/health", - Port: 8033, - Scheme: "HTTP", - }, - InitialDelaySeconds: 5, - TimeoutSeconds: 5, - PeriodSeconds: 10, - SuccessThreshold: 1, - FailureThreshold: 3, - } -) - -func TestSimpleDeployment(t *testing.T) { - log := log.FromContext(context.Background()) - - var orchestrator = &guardrailsv1alpha1.GuardrailsOrchestrator{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-orchestrator", - Namespace: "default", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "GuardrailsOrchestrator", - APIVersion: "guardrails.trusty.ai/v1alpha1", - }, - Spec: guardrailsv1alpha1.GuardrailsOrchestratorSpec{ - Replicas: 1, - }, - } - - expect := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: guardrailsv1alpha1.Name, - Namespace: "default", - Annotations: map[string]string{}, - }, - TypeMeta: metav1.TypeMeta{ - Kind: "Deployment", - APIVersion: "apps/v1", - }, - Spec: appsv1.DeploymentSpec{ - Replicas: int32(1), - Selector: &metav1.LabelSelector{}, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - "app": "fmstack-nlp", - "component": orchestrator.Name, - "deploy-name": orchestrator.Name, - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: orchestrator.Name, - Image: orchestrator.Spec.DefaultContainerImage, - ImagePullPolicy: corev1.PullAlways, - Command: command, - Env: []corev1.EnvVar{ - { - Name: "HTTPS_PORT", - Value: orchestrator.HTTPSPort, - }, - }, - SecurityContext: &corev1.SecurityContext{ - AllowPrivilegeEscalation: &allowPrivilegeEscalation, - Capabilities: &corev1.Capabilities{ - Drop: []corev1.Capability{ - "ALL", - }, - }, - Privileged: &privileged, - RunAsNonRoot: &runAsNonRootUser, - SeccompProfile: &corev1.SeccompProfile{ - Type: corev1.SeccompProfileTypeRuntimeDefault, - }, - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: "server-tls", - ReadOnly: true, - MountPath: "/tls/server", - }, - }, - Resources: resources, - ReadinessProbe: readinessProbe, - TerminationMessagePath: devTerminationPath, - Ports: []corev1.ContainerPort{ - { - Name: "http", - ContainerPort: 8033, - Protocol: corev1.ProtocolTCP, - }, - }, - }, - }, - Volumes: []corev1.Volume{ - { - Name: "server-tls", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: "caikitstack-caikit-inf-tls", - DefaultMode: 256, - }, - }, - }, - }, - RestartPolicy: corev1.RestartPolicyNever, - }, - }, - }, - } - newDeployment := createDeployment(orchestrator, log) - assert.Equal(t, expect, newDeployment) -} - -func TestEnvVarsDeployment(t *testing.T) { - log := log.FromContext(context.Background()) - - var orchestrator = &guardrailsv1alpha1.GuardrailsOrchestrator{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-orchestrator", - Namespace: "default", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "GuardrailsOrchestrator", - APIVersion: "guardrails.trusty.ai/v1alpha1", - }, - Spec: guardrailsv1alpha1.GuardrailsOrchestratorSpec{ - Replicas: 1, - }, - } - -} diff --git a/controllers/guardrails/route.go b/controllers/guardrails/route.go deleted file mode 100644 index be6f11c5..00000000 --- a/controllers/guardrails/route.go +++ /dev/null @@ -1,39 +0,0 @@ -package guardrails - -import ( - "context" - - routev1 "github.com/openshift/api/route/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" - controllerutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - - guardrailsv1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/guardrails/v1alpha1" -) - -func (r *GuardrailsReconciler) createRoute(ctx context.Context, orchestrator *guardrailsv1alpha1.GuardrailsOrchestrator) (*routev1.Route, error) { - route := &routev1.Route{ - ObjectMeta: metav1.ObjectMeta{ - Name: orchestrator.Name, - Namespace: orchestrator.Namespace, - Labels: map[string]string{"app": orchestrator.Name}, - }, - Spec: routev1.RouteSpec{ - Host: orchestrator.Name, - To: routev1.RouteTargetReference{ - Kind: "Service", - Name: orchestrator.Name, - }, - Port: &routev1.RoutePort{ - TargetPort: intstr.FromString("http"), - }, - TLS: &routev1.TLSConfig{ - Termination: routev1.TLSTerminationPassthrough, - InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, - }, - WildcardPolicy: routev1.WildcardPolicyNone, - }, - } - controllerutil.SetControllerReference(orchestrator, route, r.Scheme) - return route, nil -} diff --git a/controllers/guardrails/service.go b/controllers/guardrails/service.go deleted file mode 100644 index a4a99f8e..00000000 --- a/controllers/guardrails/service.go +++ /dev/null @@ -1,37 +0,0 @@ -package guardrails - -import ( - "context" - - guardrailsv1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/guardrails/v1alpha1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" -) - -func (r *GuardrailsReconciler) createService(ctx context.Context, orchestrator *guardrailsv1alpha1.GuardrailsOrchestrator) (*corev1.Service, error) { - // your logic here - service := &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: orchestrator.Name, - Namespace: orchestrator.Namespace, - Labels: map[string]string{"app": orchestrator.Name}, - }, - Spec: corev1.ServiceSpec{ - - IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, - Ports: []corev1.ServicePort{ - { - Name: "http", - Protocol: corev1.ProtocolTCP, - Port: 8033, - TargetPort: intstr.FromInt(8033), - }, - }, - Type: corev1.ServiceTypeClusterIP, - }, - } - controllerutil.SetControllerReference(orchestrator, service, r.Scheme) - return service, nil -} diff --git a/controllers/utils/utils.go b/controllers/utils/utils.go index 419ab3b0..f0b95c86 100644 --- a/controllers/utils/utils.go +++ b/controllers/utils/utils.go @@ -3,7 +3,13 @@ package utils import ( "os" + "bytes" + "embed" + "reflect" + "text/template" + appsv1 "k8s.io/api/apps/v1" + "sigs.k8s.io/yaml" ) func IsDeploymentReady(deployment *appsv1.Deployment) bool { @@ -55,3 +61,39 @@ func GenerateNonTLSServiceURL(crName string, namespace string) string { func GenerateKServeLoggerURL(crName string, namespace string) string { return "http://" + crName + "." + namespace + ".svc.cluster.local" } + +var templateFS embed.FS + +// executeTemplate parses the template file and executes it with the provided data. +func executeTemplate(templatePath string, data interface{}) (bytes.Buffer, error) { + var processed bytes.Buffer + tmpl, err := template.ParseFS(templateFS, templatePath) + if err != nil { + return processed, err + } + err = tmpl.Execute(&processed, data) + return processed, err +} + +// unmarshallResource unmarshal YAML bytes into the specified Kubernetes resource. +func unmarshallResource(yamlBytes []byte, outType reflect.Type) (interface{}, error) { + outValue := reflect.New(outType.Elem()).Interface() + err := yaml.Unmarshal(yamlBytes, outValue) + return outValue, err +} + +// ParseResource parses templates and return a provided Kubernetes resource. +func ParseResource[T any](templatePath string, data interface{}, outType reflect.Type) (*T, error) { + processed, err := executeTemplate(templatePath, data) + if err != nil { + return nil, err + } + + // Convert the processed bytes into the provided Kubernetes resource. + result, err := unmarshallResource(processed.Bytes(), outType) + if err != nil { + return nil, err + } + + return result.(*T), nil +}