Unverified Commit 7a341f86 authored by julienmancuso's avatar julienmancuso Committed by GitHub
Browse files

feat: simplify k8s deployment (#1708)

parent 5505507b
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
{{- if .Values.istio.enabled }}
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: {{ include "helm.fullname" . }}
spec:
gateways:
- {{ .Values.istio.gateway }}
hosts:
- "{{ .Values.istio.host }}"
http:
- match:
- uri:
prefix: /
route:
- destination:
host: {{ include "helm.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local
port:
number: 80
{{- end }}
\ No newline at end of file
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
replicaCount: 1
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
image:
repository: "nvcr.io/nvidian/nim-llm-dev/dynamo-api-store"
# This sets the pull policy for images.
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
#imagePullSecrets: []
# This is to override the chart name.
nameOverride: ""
fullnameOverride: ""
# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
serviceAccount:
# Specifies whether a service account should be created
create: true
# Automatically mount a ServiceAccount's API credentials?
automount: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
# This is for setting Kubernetes Annotations to a Pod.
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
podAnnotations: {}
# This is for setting Kubernetes Labels to a Pod.
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
podLabels: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
service:
# This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
type: ClusterIP
# This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
port: 80
# This sets the annotations for the service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
annotations: {}
# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
# Istio settings
istio:
enabled: false
host: ""
gateway: ""
postgresql:
host: ""
port: 5432
# allow to override the secret name containing the postgres password
passwordSecret: ""
database: "dynamo"
minio:
host: ""
port: 9000
# allow to override the secret name containing the minio username and password
passwordSecret: ""
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
dynamo:
apiStore:
port: 8000
# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
livenessProbe:
httpGet:
path: /readyz
port: http
timeoutSeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: http
timeoutSeconds: 5
periodSeconds: 10
# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
# Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
# secret:
# secretName: mysecret
# optional: false
# Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true
nodeSelector: {}
tolerations: []
affinity: {}
namespaceRestriction:
# Whether to restrict the operator to a single namespace
enabled: false
# The target namespace to restrict to. If empty, defaults to the release namespace
targetNamespace: ""
\ No newline at end of file
......@@ -27,9 +27,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.7
version: 0.3.2
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "0.1.0"
appVersion: "0.3.2"
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
debug = true
[history]
maxAge = 345600
maxEntries = 1000
[worker.oci]
enabled = true
gc = true
gckeepstorage = "1000GB"
[[worker.oci.gcpolicy]]
keepBytes = "200GB"
keepDuration = "168h" # 7 days
filters = [ "type==source.local", "type==exec.cachemount", "type==source.git.checkout"]
[[worker.oci.gcpolicy]]
all = false
keepDuration = "336h" # 14 days
keepBytes = "300GB"
[[worker.oci.gcpolicy]]
all = true
keepBytes = "500GB"
[registry."docker.io"]
mirrors = []
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
{{- if .Values.dynamo.imageBuildEngine | eq "buildkit" }}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
app.kubernetes.io/name: {{ include "dynamo-operator.fullname" . }}-buildkitd
name: {{ include "dynamo-operator.fullname" . }}-buildkitd
spec:
serviceName: {{ include "dynamo-operator.fullname" . }}-buildkitd
podManagementPolicy: Parallel
updateStrategy:
type: RollingUpdate
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: {{ include "dynamo-operator.fullname" . }}-buildkitd
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "dynamo-operator.fullname" . }}-buildkitd
annotations:
container.apparmor.security.beta.kubernetes.io/buildkitd: unconfined
# see buildkit/docs/rootless.md for caveats of rootless mode
spec:
containers:
- name: buildkitd
image: moby/buildkit:v0.20.0-rootless
args:
- --oci-worker-no-process-sandbox
- --addr
- unix:///run/user/1000/buildkit/buildkitd.sock
- --addr
- tcp://0.0.0.0:1234
resources:
requests:
cpu: 3
memory: 8Gi
limits:
cpu: 8
memory: 30Gi
readinessProbe:
exec:
command:
- buildctl
- debug
- workers
initialDelaySeconds: 5
periodSeconds: 30
livenessProbe:
exec:
command:
- buildctl
- debug
- workers
initialDelaySeconds: 5
periodSeconds: 30
securityContext:
seccompProfile:
type: Unconfined
# To change UID/GID, you need to rebuild the image
runAsUser: 1000
runAsGroup: 1000
volumeMounts:
- mountPath: /home/user/.local/share/buildkit
name: cache
- mountPath: /home/user/.config/buildkit
name: config
readOnly: true
- mountPath: /dev/shm
name: dshm
securityContext:
fsGroup: 1000
volumes:
- name: config
configMap:
name: {{ include "dynamo-operator.fullname" . }}-buildkitd
items:
- key: buildkitd.toml
path: buildkitd.toml
- name: dshm
emptyDir:
medium: Memory
volumeClaimTemplates:
- metadata:
name: cache
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1000Gi
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "dynamo-operator.fullname" . }}-buildkitd
labels:
app.kubernetes.io/name: {{ include "dynamo-operator.fullname" . }}-buildkitd
spec:
ports:
- name: http
port: 1234
targetPort: 1234
protocol: TCP
clusterIP: None
selector:
app.kubernetes.io/name: {{ include "dynamo-operator.fullname" . }}-buildkitd
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "dynamo-operator.fullname" . }}-buildkitd
data:
buildkitd.toml: |
{{- .Files.Get "buildkitd.toml" | nindent 4 }}
{{- end }}
\ No newline at end of file
......@@ -105,10 +105,6 @@ spec:
env:
- name: KUBERNETES_CLUSTER_DOMAIN
value: {{ quote .Values.kubernetesClusterDomain }}
{{if .Values.dynamo.dockerRegistry.useKubernetesSecret}}
- name: DOCKER_CONFIG
value: /docker/.docker
{{end}}
envFrom:
- secretRef:
name: dynamo-deployment-env
......@@ -131,19 +127,6 @@ spec:
10 }}
securityContext: {{- toYaml .Values.controllerManager.manager.containerSecurityContext
| nindent 10 }}
{{if .Values.dynamo.dockerRegistry.useKubernetesSecret}}
volumeMounts:
- name: docker-config
mountPath: /docker/.docker
readOnly: true
volumes:
- name: docker-config
secret:
secretName: {{ include "dynamo-operator.componentsDockerRegistrySecretName" . }}
items:
- key: .dockerconfigjson
path: config.json
{{end}}
securityContext:
runAsNonRoot: true
serviceAccountName: {{ include "dynamo-operator.fullname" . }}-controller-manager
......
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "dynamo-operator.fullname" . }}-image-builder
labels:
app.kubernetes.io/component: rbac
app.kubernetes.io/created-by: dynamo-operator
app.kubernetes.io/part-of: dynamo-operator
nvidia.com/dynamo-image-builder-pod: "true"
{{- include "dynamo-operator.labels" . | nindent 4 }}
annotations:
{{- toYaml .Values.dynamo.imageBuilder.serviceAccount.annotations | nindent 4 }}
\ No newline at end of file
......@@ -331,7 +331,6 @@ rules:
- nvidia.com
resources:
- dynamocomponentdeployments
- dynamocomponents
- dynamographdeployments
verbs:
- create
......@@ -345,7 +344,6 @@ rules:
- nvidia.com
resources:
- dynamocomponentdeployments/finalizers
- dynamocomponents/finalizers
- dynamographdeployments/finalizers
verbs:
- update
......@@ -353,7 +351,6 @@ rules:
- nvidia.com
resources:
- dynamocomponentdeployments/status
- dynamocomponents/status
- dynamographdeployments/status
verbs:
- get
......
......@@ -21,15 +21,6 @@ metadata:
{{- include "dynamo-operator.labels" . | nindent 4 }}
type: Opaque
stringData:
{{- if .Values.dynamo.apiStore.endpoint }}
API_STORE_ENDPOINT : {{ .Values.dynamo.apiStore.endpoint | quote }}
{{- else }}
API_STORE_ENDPOINT : http://{{ .Release.Name }}-dynamo-api-store
{{- end }}
API_STORE_CLUSTER_NAME: {{ .Values.dynamo.apiStore.clusterName | quote }}
DYNAMO_SYSTEM_NAMESPACE: {{ .Release.Namespace }}
DYNAMO_DEPLOYMENT_NAMESPACE: {{ .Release.Namespace }}
DYNAMO_IMAGE_BUILDER_NAMESPACE: {{ .Release.Namespace }}
INTERNAL_IMAGES_DEBUGGER: {{ .Values.dynamo.internalImages.debugger | quote }}
......@@ -40,25 +31,3 @@ stringData:
{{- if .Values.dynamo.dynamoIngressSuffix }}
DYNAMO_INGRESS_SUFFIX: {{ .Values.dynamo.dynamoIngressSuffix | quote }}
{{- end }}
\ No newline at end of file
DOCKER_REGISTRY_SERVER: {{ required "docker registry server is required" .Values.dynamo.dockerRegistry.server | quote }}
{{- if .Values.dynamo.dockerRegistry.useKubernetesSecret }}
DOCKER_REGISTRY_SECRET_NAME: {{ include "dynamo-operator.componentsDockerRegistrySecretName" . }}
{{- end }}
DOCKER_REGISTRY_SECURE: {{ .Values.dynamo.dockerRegistry.secure | quote }}
DOCKER_REGISTRY_DYNAMO_COMPONENTS_REPOSITORY_NAME: {{ .Values.dynamo.dockerRegistry.dynamoComponentsRepositoryName | quote }}
INTERNAL_IMAGES_DYNAMO_COMPONENTS_DOWNLOADER: {{ .Values.dynamo.internalImages.dynamoComponentsDownloader | quote }}
INTERNAL_IMAGES_KANIKO: {{ .Values.dynamo.internalImages.kaniko | quote }}
INTERNAL_IMAGES_BUILDKIT: {{ .Values.dynamo.internalImages.buildkit | quote }}
INTERNAL_IMAGES_BUILDKIT_ROOTLESS: {{ .Values.dynamo.internalImages.buildkitRootless | quote }}
BUILDKIT_URL: tcp://{{ include "dynamo-operator.fullname" . }}-buildkitd:1234
DYNAMO_IMAGE_BUILD_ENGINE: {{ .Values.dynamo.imageBuildEngine | quote }}
ADD_NAMESPACE_PREFIX_TO_IMAGE_NAME: {{ .Values.dynamo.addNamespacePrefixToImageName | quote }}
ESTARGZ_ENABLED: {{ .Values.dynamo.estargz.enabled | quote }}
KANIKO_CACHE_REPO: {{ .Values.dynamo.kaniko.cacheRepo | quote }}
KANIKO_SNAPSHOT_MODE: {{ .Values.dynamo.kaniko.snapshotMode | quote }}
\ No newline at end of file
......@@ -59,8 +59,9 @@ controllerManager:
drop:
- ALL
image:
repository: controller
tag: latest
repository: nvcr.io/nvidia/ai-dynamo/kubernetes-operator
# tag is optional - if not set, will use Chart.AppVersion
tag: ""
resources:
limits:
cpu: 1024m
......@@ -81,15 +82,8 @@ dynamo:
annotations: {}
enableLWS: false
apiStore:
endpoint: http://dynamo-server.dynamo-system.svc.cluster.local
clusterName: default
internalImages:
dynamoComponentsDownloader: rapidfort/curl:latest
kaniko: gcr.io/kaniko-project/executor:debug
buildkit: moby/buildkit:v0.20.2
buildkitRootless: moby/buildkit:v0.20.2-rootless
debugger: python:3.12-slim
enableRestrictedSecurityContext: false
......@@ -103,17 +97,7 @@ dynamo:
password: ""
existingSecretName: ''
secure: true
dynamoComponentsRepositoryName: dynamo-pipelines
imageBuildEngine: buildkit # options: kaniko, buildkit, buildkit-rootless
addNamespacePrefixToImageName: false
estargz:
enabled: false
kaniko:
cacheRepo: ''
snapshotMode: '' # options: full, redo, time
#imagePullSecrets: []
kubernetesClusterDomain: cluster.local
......
......@@ -25,8 +25,8 @@ dynamo-operator:
controllerManager:
manager:
image:
repository: ""
tag: "latest"
repository: "nvcr.io/nvidia/ai-dynamo/kubernetes-operator"
tag: ""
pullPolicy: IfNotPresent
args:
- --health-probe-bind-address=:8081
......@@ -34,31 +34,16 @@ dynamo-operator:
imagePullSecrets: []
dynamo:
enableLWS: false
apiStore:
endpoint:
clusterName: default
internalImages:
dynamoComponentsDownloader: rapidfort/curl:latest
kaniko: gcr.io/kaniko-project/executor:debug
buildkit: moby/buildkit:v0.20.2
buildkitRootless: moby/buildkit:v0.20.2-rootless
debugger: python:3.12-slim
enableRestrictedSecurityContext: false
dockerRegistry:
useKubernetesSecret: true
useKubernetesSecret: false
server:
username:
password:
existingSecretName:
secure: true
dynamoComponentsRepositoryName: dynamo-pipelines
imageBuildEngine: buildkit
addNamespacePrefixToImageName: false
estargz:
enabled: false
kaniko:
cacheRepo: ""
snapshotMode: ""
ingress:
enabled: false
className:
......@@ -69,32 +54,6 @@ dynamo-operator:
ingressHostSuffix: ""
virtualServiceSupportsHTTPS: false
dynamo-api-store:
enabled: true
namespaceRestriction:
enabled: true
targetNamespace:
istio:
enabled: false
host: ""
gateway: ""
dynamo:
env:
resource_scope: "user"
image:
repository: ""
tag: "latest"
pullPolicy: IfNotPresent
imagePullSecrets: []
ingress:
enabled: false
className: nginx
hosts:
- host: ""
paths:
- path: /
pathType: Prefix
## In-cluster minio deployment configuration
## ref: https://github.com/bitnami/charts/blob/minio/13.3.1/bitnami/minio/values.yaml
## @param minio.apiIngress.enabled Enable ingress controller resource for MinIO API
......@@ -147,18 +106,6 @@ minio:
api: 9000
console: 9001
postgresql:
enabled: true
auth:
existingSecret: ""
postgresPassword: ""
username: "admin"
password: "password"
database: "dynamo"
primary:
persistence:
enabled: false
etcd:
enabled: true
persistence:
......
......@@ -44,9 +44,54 @@ help: ## Display this help.
##@ Development
.PHONY: ensure-yq
ensure-yq:
@if ! command -v yq &> /dev/null; then \
echo "Installing yq..."; \
ARCH=$$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/'); \
OS=$$(uname -s | tr '[:upper:]' '[:lower:]'); \
wget https://github.com/mikefarah/yq/releases/latest/download/yq_$${OS}_$${ARCH} -O /usr/local/bin/yq && \
chmod +x /usr/local/bin/yq; \
else \
echo "yq is already installed: $$(yq --version)"; \
fi
.PHONY: manifests
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
manifests: controller-gen ensure-yq ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
$(CONTROLLER_GEN) rbac:roleName=manager-role crd:maxDescLen=0 webhook paths="./..." output:crd:artifacts:config=config/crd/bases
echo "Removing name from mainContainer required fields"
for file in config/crd/bases/*.yaml; do \
yq eval '(.. | select(has("mainContainer")) | .mainContainer.required) |= (. - ["name"])' -i --indent 2 $$file || exit 1; \
done
echo "Adding NVIDIA header to CRD files"
for file in config/crd/bases/*.yaml; do \
if ! head -20 "$$file" | grep -q "NVIDIA CORPORATION"; then \
{ printf '%s\n' \
'# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.' \
'# SPDX-License-Identifier: Apache-2.0' \
'#' \
'# 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.' \
''; \
cat "$$file"; \
} > "$$file.tmp" && mv "$$file.tmp" "$$file"; \
fi; \
done
echo "Adding helm.sh/resource-policy: keep annotation to CRD files"
for file in config/crd/bases/*.yaml; do \
if ! yq eval '.metadata.annotations."helm.sh/resource-policy"' "$$file" | grep -q "keep"; then \
yq eval '.metadata.annotations."helm.sh/resource-policy" = "keep"' -i "$$file"; \
fi; \
done
.PHONY: generate
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
......
......@@ -16,13 +16,6 @@ resources:
kind: DynamoComponentDeployment
path: github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
domain: nvidia.com
kind: DynamoComponent
path: github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
......
/*
* SPDX-FileCopyrightText: Copyright (c) 2022 Atalaya Tech. Inc
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* 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.
* Modifications Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES
*/
package api_store_client
import (
"context"
"fmt"
"strings"
"github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/dynamo/schemas"
)
type ApiStoreClient struct {
endpoint string
}
func NewApiStoreClient(endpoint string) *ApiStoreClient {
return &ApiStoreClient{
endpoint: endpoint,
}
}
func (c *ApiStoreClient) GetDynamoComponent(ctx context.Context, name, version string) (component *schemas.DynamoComponent, err error) {
url_ := urlJoin(c.endpoint, fmt.Sprintf("/api/v1/dynamo_components/%s/versions/%s", name, version))
component = &schemas.DynamoComponent{}
_, err = DoJsonRequest(ctx, "GET", url_, nil, nil, nil, component, nil)
return
}
func (c *ApiStoreClient) PresignDynamoComponentDownloadURL(ctx context.Context, name, version string) (component *schemas.DynamoComponent, err error) {
url_ := urlJoin(c.endpoint, fmt.Sprintf("/api/v1/dynamo_components/%s/versions/%s/presign_download_url", name, version))
component = &schemas.DynamoComponent{}
_, err = DoJsonRequest(ctx, "PATCH", url_, nil, nil, nil, component, nil)
return
}
func urlJoin(baseURL string, pathPart string) string {
return strings.TrimRight(baseURL, "/") + "/" + strings.TrimLeft(pathPart, "/")
}
/*
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* 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 api_store_client
import (
"context"
"crypto/tls"
"fmt"
"time"
"resty.dev/v3"
)
var defaultClient *resty.Client
func GetDefaultClient() *resty.Client {
if defaultClient == nil {
defaultClient = resty.New().
SetTimeout(90*time.Second).
SetRetryCount(3).
SetRetryWaitTime(2*time.Second).
SetRetryMaxWaitTime(10*time.Second).
SetHeader("Content-Type", "application/json").
SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) // Optional: mirrors your custom transport
}
return defaultClient
}
func DoJsonRequest(ctx context.Context, method string, url string, headers map[string]string, query map[string]string, payload interface{}, result interface{}, timeout *time.Duration) (int, error) {
client := GetDefaultClient()
if timeout != nil {
client.SetTimeout(*timeout)
}
req := client.R().
SetContext(ctx).
SetBody(payload).
SetResult(result).
SetHeaders(headers).
SetQueryParams(query)
var resp *resty.Response
var err error
switch method {
case "GET":
resp, err = req.Get(url)
case "POST":
resp, err = req.Post(url)
case "PUT":
resp, err = req.Put(url)
case "DELETE":
resp, err = req.Delete(url)
case "PATCH":
resp, err = req.Patch(url)
default:
return 0, fmt.Errorf("unsupported method: %s", method)
}
if err != nil {
return 0, fmt.Errorf("request error: %w", err)
}
if resp.IsError() {
return resp.StatusCode(), fmt.Errorf("http %s %s failed with status %d: %s", method, url, resp.StatusCode(), resp.String())
}
return resp.StatusCode(), nil
}
/*
* SPDX-FileCopyrightText: Copyright (c) 2022 Atalaya Tech. Inc
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* 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.
* Modifications Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES
*/
package v1alpha1
import (
dynamoCommon "github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/dynamo/common"
"github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/dynamo/schemas"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
DynamoComponentConditionTypeImageBuilding = "ImageBuilding"
DynamoComponentConditionTypeImageExists = "ImageExists"
DynamoComponentConditionTypeImageExistsChecked = "ImageExistsChecked"
DynamoComponentConditionTypeModelsExists = "ModelsExists"
DynamoComponentConditionTypeDynamoComponentAvailable = "DynamoComponentAvailable"
)
// 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.
// DynamoComponentSpec defines the desired state of DynamoComponent
type DynamoComponentSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
// +kubebuilder:validation:Required
DynamoComponent string `json:"dynamoComponent"`
DownloadURL string `json:"downloadUrl,omitempty"`
ServiceName string `json:"serviceName,omitempty"`
// +kubebuilder:validation:Optional
Image string `json:"image,omitempty"`
ImageBuildTimeout *schemas.Duration `json:"imageBuildTimeout,omitempty"`
// +kubebuilder:validation:Optional
BuildArgs []string `json:"buildArgs,omitempty"`
// +kubebuilder:validation:Optional
ImageBuilderExtraPodMetadata *dynamoCommon.ExtraPodMetadata `json:"imageBuilderExtraPodMetadata,omitempty"`
// +kubebuilder:validation:Optional
ImageBuilderExtraPodSpec *dynamoCommon.ExtraPodSpec `json:"imageBuilderExtraPodSpec,omitempty"`
// +kubebuilder:validation:Optional
ImageBuilderExtraContainerEnv []corev1.EnvVar `json:"imageBuilderExtraContainerEnv,omitempty"`
// +kubebuilder:validation:Optional
ImageBuilderContainerResources *corev1.ResourceRequirements `json:"imageBuilderContainerResources,omitempty"`
// +kubebuilder:validation:Optional
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
// +kubebuilder:validation:Optional
DockerConfigJSONSecretName string `json:"dockerConfigJsonSecretName,omitempty"`
// +kubebuilder:validation:Optional
DownloaderContainerEnvFrom []corev1.EnvFromSource `json:"downloaderContainerEnvFrom,omitempty"`
}
// DynamoComponentStatus defines the observed state of DynamoComponent
type DynamoComponentStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
Conditions []metav1.Condition `json:"conditions"`
}
// +genclient
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="DynamoComponent",type="string",JSONPath=".spec.dynamoComponent",description="Dynamo component"
// +kubebuilder:printcolumn:name="Image-Exists",type="string",JSONPath=".status.conditions[?(@.type=='ImageExists')].status",description="Image Exists"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:resource:shortName=dc
// DynamoComponent is the Schema for the dynamocomponents API
type DynamoComponent struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec DynamoComponentSpec `json:"spec,omitempty"`
Status DynamoComponentStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// DynamoComponentList contains a list of DynamoComponent
type DynamoComponentList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []DynamoComponent `json:"items"`
}
func init() {
SchemeBuilder.Register(&DynamoComponent{}, &DynamoComponentList{})
}
func (s *DynamoComponent) GetSpec() any {
return s.Spec
}
func (s *DynamoComponent) SetSpec(spec any) {
s.Spec = spec.(DynamoComponentSpec)
}
func (s *DynamoComponent) IsReady() bool {
return meta.IsStatusConditionTrue(s.Status.Conditions, DynamoComponentConditionTypeDynamoComponentAvailable)
}
// GetImage returns the docker image of the DynamoComponent
func (s *DynamoComponent) GetImage() string {
if s.Spec.Image != "" {
// if the image is specified in the spec, return it
return s.Spec.Image
}
// if the image is not specified in the spec, the image is stored in the status condition ImageExists
if meta.FindStatusCondition(s.Status.Conditions, DynamoComponentConditionTypeImageExists) != nil {
return meta.FindStatusCondition(s.Status.Conditions, DynamoComponentConditionTypeImageExists).Message
}
return ""
}
......@@ -38,9 +38,9 @@ const (
// DynamoComponentDeploymentSpec defines the desired state of DynamoComponentDeployment
type DynamoComponentDeploymentSpec struct {
DynamoComponent string `json:"dynamoComponent"`
DynamoComponent string `json:"dynamoComponent,omitempty"`
// contains the tag of the DynamoComponent: for example, "my_package:MyService"
DynamoTag string `json:"dynamoTag"`
DynamoTag string `json:"dynamoTag,omitempty"`
DynamoComponentDeploymentSharedSpec `json:",inline"`
}
......@@ -59,6 +59,8 @@ type DynamoComponentDeploymentSharedSpec struct {
// contains the name of the service
ServiceName string `json:"serviceName,omitempty"`
ComponentType string `json:"componentType,omitempty"`
// dynamo namespace of the service (allows to override the dynamo namespace of the service defined in annotations inside the dynamo archive)
DynamoNamespace *string `json:"dynamoNamespace,omitempty"`
......@@ -165,7 +167,7 @@ func (s *DynamoComponentDeployment) SetSpec(spec any) {
}
func (s *DynamoComponentDeployment) IsMainComponent() bool {
return strings.HasSuffix(s.Spec.DynamoTag, s.Spec.ServiceName)
return strings.HasSuffix(s.Spec.DynamoTag, s.Spec.ServiceName) || s.Spec.ComponentType == commonconsts.ComponentTypeMain
}
func (s *DynamoComponentDeployment) GetDynamoDeploymentConfig() []byte {
......@@ -189,3 +191,11 @@ func (s *DynamoComponentDeployment) SetDynamoDeploymentConfig(config []byte) {
Value: string(config),
})
}
// GetImage returns the docker image of the DynamoComponent
func (s *DynamoComponentDeployment) GetImage() string {
if s.Spec.ExtraPodSpec != nil && s.Spec.ExtraPodSpec.MainContainer != nil {
return s.Spec.ExtraPodSpec.MainContainer.Image
}
return ""
}
......@@ -32,7 +32,7 @@ import (
// DynamoGraphDeploymentSpec defines the desired state of DynamoGraphDeployment.
type DynamoGraphDeploymentSpec struct {
// required
DynamoGraph string `json:"dynamoGraph"`
DynamoGraph string `json:"dynamoGraph,omitempty"`
// optional
// key is the name of the service defined in DynamoComponent
// value is the DynamoComponentDeployment override for that service
......
......@@ -39,7 +39,6 @@ package v1alpha1
import (
"github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/dynamo/common"
"github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/dynamo/schemas"
"k8s.io/api/autoscaling/v2"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
......@@ -115,33 +114,6 @@ func (in *BaseStatus) DeepCopy() *BaseStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DynamoComponent) DeepCopyInto(out *DynamoComponent) {
*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 DynamoComponent.
func (in *DynamoComponent) DeepCopy() *DynamoComponent {
if in == nil {
return nil
}
out := new(DynamoComponent)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DynamoComponent) 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 *DynamoComponentDeployment) DeepCopyInto(out *DynamoComponentDeployment) {
*out = *in
......@@ -361,119 +333,6 @@ func (in *DynamoComponentDeploymentStatus) DeepCopy() *DynamoComponentDeployment
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DynamoComponentList) DeepCopyInto(out *DynamoComponentList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]DynamoComponent, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynamoComponentList.
func (in *DynamoComponentList) DeepCopy() *DynamoComponentList {
if in == nil {
return nil
}
out := new(DynamoComponentList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DynamoComponentList) 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 *DynamoComponentSpec) DeepCopyInto(out *DynamoComponentSpec) {
*out = *in
if in.ImageBuildTimeout != nil {
in, out := &in.ImageBuildTimeout, &out.ImageBuildTimeout
*out = new(schemas.Duration)
**out = **in
}
if in.BuildArgs != nil {
in, out := &in.BuildArgs, &out.BuildArgs
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ImageBuilderExtraPodMetadata != nil {
in, out := &in.ImageBuilderExtraPodMetadata, &out.ImageBuilderExtraPodMetadata
*out = new(common.ExtraPodMetadata)
(*in).DeepCopyInto(*out)
}
if in.ImageBuilderExtraPodSpec != nil {
in, out := &in.ImageBuilderExtraPodSpec, &out.ImageBuilderExtraPodSpec
*out = new(common.ExtraPodSpec)
(*in).DeepCopyInto(*out)
}
if in.ImageBuilderExtraContainerEnv != nil {
in, out := &in.ImageBuilderExtraContainerEnv, &out.ImageBuilderExtraContainerEnv
*out = make([]v1.EnvVar, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ImageBuilderContainerResources != nil {
in, out := &in.ImageBuilderContainerResources, &out.ImageBuilderContainerResources
*out = new(v1.ResourceRequirements)
(*in).DeepCopyInto(*out)
}
if in.ImagePullSecrets != nil {
in, out := &in.ImagePullSecrets, &out.ImagePullSecrets
*out = make([]v1.LocalObjectReference, len(*in))
copy(*out, *in)
}
if in.DownloaderContainerEnvFrom != nil {
in, out := &in.DownloaderContainerEnvFrom, &out.DownloaderContainerEnvFrom
*out = make([]v1.EnvFromSource, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynamoComponentSpec.
func (in *DynamoComponentSpec) DeepCopy() *DynamoComponentSpec {
if in == nil {
return nil
}
out := new(DynamoComponentSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DynamoComponentStatus) DeepCopyInto(out *DynamoComponentStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]metav1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynamoComponentStatus.
func (in *DynamoComponentStatus) DeepCopy() *DynamoComponentStatus {
if in == nil {
return nil
}
out := new(DynamoComponentStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DynamoGraphDeployment) DeepCopyInto(out *DynamoGraphDeployment) {
*out = *in
......
......@@ -20,6 +20,7 @@
package main
import (
"context"
"crypto/tls"
"flag"
"os"
......@@ -28,7 +29,11 @@ import (
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
clientv3 "go.etcd.io/etcd/client/v3"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
_ "k8s.io/client-go/plugin/pkg/client/auth"
k8sCache "k8s.io/client-go/tools/cache"
"sigs.k8s.io/controller-runtime/pkg/cache"
"k8s.io/apimachinery/pkg/runtime"
......@@ -47,6 +52,7 @@ import (
"github.com/ai-dynamo/dynamo/deploy/cloud/operator/internal/controller"
commonController "github.com/ai-dynamo/dynamo/deploy/cloud/operator/internal/controller_common"
"github.com/ai-dynamo/dynamo/deploy/cloud/operator/internal/etcd"
"github.com/ai-dynamo/dynamo/deploy/cloud/operator/internal/secrets"
istioclientsetscheme "istio.io/client-go/pkg/clientset/versioned/scheme"
//+kubebuilder:scaffold:imports
)
......@@ -124,6 +130,7 @@ func main() {
EnableLWS: enableLWS,
}
mainCtx := ctrl.SetupSignalHandler()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
// if the enable-http2 flag is false (the default), http/2 should be disabled
......@@ -191,6 +198,93 @@ func main() {
setupLog.Error(err, "unable to create etcd client")
os.Exit(1)
}
dockerSecretRetriever := secrets.NewDockerSecretIndexer(mgr.GetClient())
// refresh whenever a secret is created/deleted/updated
// Set up informer
var factory informers.SharedInformerFactory
if restrictedNamespace == "" {
factory = informers.NewSharedInformerFactory(kubernetes.NewForConfigOrDie(mgr.GetConfig()), time.Hour*24)
} else {
factory = informers.NewFilteredSharedInformerFactory(
kubernetes.NewForConfigOrDie(mgr.GetConfig()),
time.Hour*24,
restrictedNamespace,
nil,
)
}
secretInformer := factory.Core().V1().Secrets().Informer()
// Start the informer factory
go factory.Start(mainCtx.Done())
// Wait for the initial sync
if !k8sCache.WaitForCacheSync(mainCtx.Done(), secretInformer.HasSynced) {
setupLog.Error(nil, "Failed to sync informer cache")
os.Exit(1)
}
setupLog.Info("Secret informer cache synced and ready")
_, err = secretInformer.AddEventHandler(k8sCache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
secret := obj.(*corev1.Secret)
if secret.Type == corev1.SecretTypeDockerConfigJson {
setupLog.Info("refreshing docker secrets index after secret creation...")
err := dockerSecretRetriever.RefreshIndex(context.Background())
if err != nil {
setupLog.Error(err, "unable to refresh docker secrets index after secret creation")
} else {
setupLog.Info("docker secrets index refreshed after secret creation")
}
}
},
UpdateFunc: func(old, new interface{}) {
newSecret := new.(*corev1.Secret)
if newSecret.Type == corev1.SecretTypeDockerConfigJson {
setupLog.Info("refreshing docker secrets index after secret update...")
err := dockerSecretRetriever.RefreshIndex(context.Background())
if err != nil {
setupLog.Error(err, "unable to refresh docker secrets index after secret update")
} else {
setupLog.Info("docker secrets index refreshed after secret update")
}
}
},
DeleteFunc: func(obj interface{}) {
secret := obj.(*corev1.Secret)
if secret.Type == corev1.SecretTypeDockerConfigJson {
setupLog.Info("refreshing docker secrets index after secret deletion...")
err := dockerSecretRetriever.RefreshIndex(context.Background())
if err != nil {
setupLog.Error(err, "unable to refresh docker secrets index after secret deletion")
} else {
setupLog.Info("docker secrets index refreshed after secret deletion")
}
}
},
})
if err != nil {
setupLog.Error(err, "unable to add event handler to secret informer")
os.Exit(1)
}
// launch a goroutine to refresh the docker secret indexer in any case every minute
go func() {
// Initial refresh
if err := dockerSecretRetriever.RefreshIndex(context.Background()); err != nil {
setupLog.Error(err, "initial docker secrets index refresh failed")
}
ticker := time.NewTicker(60 * time.Second)
defer ticker.Stop()
for {
select {
case <-mainCtx.Done():
return
case <-ticker.C:
setupLog.Info("refreshing docker secrets index...")
if err := dockerSecretRetriever.RefreshIndex(mainCtx); err != nil {
setupLog.Error(err, "unable to refresh docker secrets index")
}
setupLog.Info("docker secrets index refreshed")
}
}
}()
if err = (&controller.DynamoComponentDeploymentReconciler{
Client: mgr.GetClient(),
Recorder: mgr.GetEventRecorderFor("dynamocomponentdeployment"),
......@@ -199,19 +293,11 @@ func main() {
EtcdAddr: etcdAddr,
EtcdStorage: etcd.NewStorage(cli),
UseVirtualService: istioVirtualServiceGateway != "",
DockerSecretRetriever: dockerSecretRetriever,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "DynamoComponentDeployment")
os.Exit(1)
}
if err = (&controller.DynamoComponentReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("dynamocomponent"),
Config: ctrlConfig,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "DynamoComponent")
os.Exit(1)
}
if err = (&controller.DynamoGraphDeploymentReconciler{
Client: mgr.GetClient(),
Recorder: mgr.GetEventRecorderFor("dynamographdeployment"),
......@@ -236,7 +322,7 @@ func main() {
}
setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
if err := mgr.Start(mainCtx); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment