diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c7a3407d..2369ea863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - (Feature) PongV1 Integration Service - (Feature) Custom Gateway image - (Bugfix) Fix race condition in ArangoBackup +- (Feature) Improve Gateway Config gen ## [1.2.42](https://github.com/arangodb/kube-arangodb/tree/1.2.42) (2024-07-23) - (Maintenance) Go 1.22.4 & Kubernetes 1.29.6 libraries diff --git a/Makefile b/Makefile index b09a3e097..2cadcdadb 100644 --- a/Makefile +++ b/Makefile @@ -803,10 +803,12 @@ set-typed-api-version/%: "$(ROOT)/pkg/deployment/" \ "$(ROOT)/pkg/replication/" \ "$(ROOT)/pkg/operator/" \ + "$(ROOT)/pkg/operatorV2/" \ "$(ROOT)/pkg/server/" \ "$(ROOT)/pkg/util/" \ "$(ROOT)/pkg/handlers/" \ "$(ROOT)/pkg/apis/backup/" \ + "$(ROOT)/pkg/apis/networking/" \ "$(ROOT)/pkg/upgrade/" \ | cut -d ':' -f 1 | sort | uniq \ | xargs -n 1 $(SED) -i "s#github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/typed/$*/v[A-Za-z0-9]\+#github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/typed/$*/v$(API_VERSION)#g" @@ -817,10 +819,12 @@ set-api-version/%: "$(ROOT)/pkg/deployment/" \ "$(ROOT)/pkg/replication/" \ "$(ROOT)/pkg/operator/" \ + "$(ROOT)/pkg/operatorV2/" \ "$(ROOT)/pkg/server/" \ "$(ROOT)/pkg/util/" \ "$(ROOT)/pkg/handlers/" \ "$(ROOT)/pkg/apis/backup/" \ + "$(ROOT)/pkg/apis/networking/" \ "$(ROOT)/pkg/upgrade/" \ | cut -d ':' -f 1 | sort | uniq \ | xargs -n 1 $(SED) -i "s#github.com/arangodb/kube-arangodb/pkg/apis/$*/v[A-Za-z0-9]\+#github.com/arangodb/kube-arangodb/pkg/apis/$*/v$(API_VERSION)#g" @@ -828,10 +832,12 @@ set-api-version/%: "$(ROOT)/pkg/deployment/" \ "$(ROOT)/pkg/replication/" \ "$(ROOT)/pkg/operator/" \ + "$(ROOT)/pkg/operatorV2/" \ "$(ROOT)/pkg/server/" \ "$(ROOT)/pkg/util/" \ "$(ROOT)/pkg/handlers/" \ "$(ROOT)/pkg/apis/backup/" \ + "$(ROOT)/pkg/apis/networking/" \ "$(ROOT)/pkg/upgrade/" \ | cut -d ':' -f 1 | sort | uniq \ | xargs -n 1 $(SED) -i "s#DatabaseV[A-Za-z0-9]\+()\.#DatabaseV$(API_VERSION)().#g" @@ -839,10 +845,12 @@ set-api-version/%: "$(ROOT)/pkg/deployment/" \ "$(ROOT)/pkg/replication/" \ "$(ROOT)/pkg/operator/" \ + "$(ROOT)/pkg/operatorV2/" \ "$(ROOT)/pkg/server/" \ "$(ROOT)/pkg/util/" \ "$(ROOT)/pkg/handlers" \ "$(ROOT)/pkg/apis/backup/" \ + "$(ROOT)/pkg/apis/networking/" \ "$(ROOT)/pkg/upgrade/" \ | cut -d ':' -f 1 | sort | uniq \ | xargs -n 1 $(SED) -i "s#ReplicationV[A-Za-z0-9]\+()\.#ReplicationV$(API_VERSION)().#g" diff --git a/chart/kube-arangodb-arm64/templates/crd/cluster-role.yaml b/chart/kube-arangodb-arm64/templates/crd/cluster-role.yaml index 894674ced..93ee7ad4d 100644 --- a/chart/kube-arangodb-arm64/templates/crd/cluster-role.yaml +++ b/chart/kube-arangodb-arm64/templates/crd/cluster-role.yaml @@ -13,19 +13,61 @@ metadata: app.kubernetes.io/instance: {{ .Release.Name }} release: {{ .Release.Name }} rules: - - apiGroups: ["apiextensions.k8s.io"] - resources: ["customresourcedefinitions"] - verbs: ["get", "list", "watch", "update", "delete"] - resourceNames: - - "arangodeployments.database.arangodb.com" - - "arangoclustersynchronizations.database.arangodb.com" - - "arangomembers.database.arangodb.com" - - "arangotasks.database.arangodb.com" - - "arangodeploymentreplications.replication.database.arangodb.com" - - "arangobackups.backup.arangodb.com" - - "arangobackuppolicies.backup.arangodb.com" - - "arangojobs.apps.arangodb.com" - - "arangolocalstorages.storage.arangodb.com" +# analytics.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "graphanalyticsengines.analytics.arangodb.com" +# apps.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangojobs.apps.arangodb.com" +# backup.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangobackuppolicies.backup.arangodb.com" + - "arangobackups.backup.arangodb.com" +# database.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangoclustersynchronizations.database.arangodb.com" + - "arangodeployments.database.arangodb.com" + - "arangomembers.database.arangodb.com" + - "arangotasks.database.arangodb.com" +# ml.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangomlbatchjobs.ml.arangodb.com" + - "arangomlcronjobs.ml.arangodb.com" + - "arangomlextensions.ml.arangodb.com" + - "arangomlstorages.ml.arangodb.com" +# networking.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangoroutes.networking.arangodb.com" +# replication.database.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangodeploymentreplications.replication.database.arangodb.com" +# scheduler.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangoprofiles.scheduler.arangodb.com" {{- end }} {{- end }} diff --git a/chart/kube-arangodb-enterprise-arm64/templates/crd/cluster-role.yaml b/chart/kube-arangodb-enterprise-arm64/templates/crd/cluster-role.yaml index 894674ced..93ee7ad4d 100644 --- a/chart/kube-arangodb-enterprise-arm64/templates/crd/cluster-role.yaml +++ b/chart/kube-arangodb-enterprise-arm64/templates/crd/cluster-role.yaml @@ -13,19 +13,61 @@ metadata: app.kubernetes.io/instance: {{ .Release.Name }} release: {{ .Release.Name }} rules: - - apiGroups: ["apiextensions.k8s.io"] - resources: ["customresourcedefinitions"] - verbs: ["get", "list", "watch", "update", "delete"] - resourceNames: - - "arangodeployments.database.arangodb.com" - - "arangoclustersynchronizations.database.arangodb.com" - - "arangomembers.database.arangodb.com" - - "arangotasks.database.arangodb.com" - - "arangodeploymentreplications.replication.database.arangodb.com" - - "arangobackups.backup.arangodb.com" - - "arangobackuppolicies.backup.arangodb.com" - - "arangojobs.apps.arangodb.com" - - "arangolocalstorages.storage.arangodb.com" +# analytics.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "graphanalyticsengines.analytics.arangodb.com" +# apps.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangojobs.apps.arangodb.com" +# backup.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangobackuppolicies.backup.arangodb.com" + - "arangobackups.backup.arangodb.com" +# database.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangoclustersynchronizations.database.arangodb.com" + - "arangodeployments.database.arangodb.com" + - "arangomembers.database.arangodb.com" + - "arangotasks.database.arangodb.com" +# ml.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangomlbatchjobs.ml.arangodb.com" + - "arangomlcronjobs.ml.arangodb.com" + - "arangomlextensions.ml.arangodb.com" + - "arangomlstorages.ml.arangodb.com" +# networking.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangoroutes.networking.arangodb.com" +# replication.database.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangodeploymentreplications.replication.database.arangodb.com" +# scheduler.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangoprofiles.scheduler.arangodb.com" {{- end }} {{- end }} diff --git a/chart/kube-arangodb-enterprise/templates/crd/cluster-role.yaml b/chart/kube-arangodb-enterprise/templates/crd/cluster-role.yaml index 894674ced..93ee7ad4d 100644 --- a/chart/kube-arangodb-enterprise/templates/crd/cluster-role.yaml +++ b/chart/kube-arangodb-enterprise/templates/crd/cluster-role.yaml @@ -13,19 +13,61 @@ metadata: app.kubernetes.io/instance: {{ .Release.Name }} release: {{ .Release.Name }} rules: - - apiGroups: ["apiextensions.k8s.io"] - resources: ["customresourcedefinitions"] - verbs: ["get", "list", "watch", "update", "delete"] - resourceNames: - - "arangodeployments.database.arangodb.com" - - "arangoclustersynchronizations.database.arangodb.com" - - "arangomembers.database.arangodb.com" - - "arangotasks.database.arangodb.com" - - "arangodeploymentreplications.replication.database.arangodb.com" - - "arangobackups.backup.arangodb.com" - - "arangobackuppolicies.backup.arangodb.com" - - "arangojobs.apps.arangodb.com" - - "arangolocalstorages.storage.arangodb.com" +# analytics.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "graphanalyticsengines.analytics.arangodb.com" +# apps.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangojobs.apps.arangodb.com" +# backup.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangobackuppolicies.backup.arangodb.com" + - "arangobackups.backup.arangodb.com" +# database.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangoclustersynchronizations.database.arangodb.com" + - "arangodeployments.database.arangodb.com" + - "arangomembers.database.arangodb.com" + - "arangotasks.database.arangodb.com" +# ml.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangomlbatchjobs.ml.arangodb.com" + - "arangomlcronjobs.ml.arangodb.com" + - "arangomlextensions.ml.arangodb.com" + - "arangomlstorages.ml.arangodb.com" +# networking.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangoroutes.networking.arangodb.com" +# replication.database.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangodeploymentreplications.replication.database.arangodb.com" +# scheduler.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangoprofiles.scheduler.arangodb.com" {{- end }} {{- end }} diff --git a/chart/kube-arangodb/templates/crd/cluster-role.yaml b/chart/kube-arangodb/templates/crd/cluster-role.yaml index 894674ced..93ee7ad4d 100644 --- a/chart/kube-arangodb/templates/crd/cluster-role.yaml +++ b/chart/kube-arangodb/templates/crd/cluster-role.yaml @@ -13,19 +13,61 @@ metadata: app.kubernetes.io/instance: {{ .Release.Name }} release: {{ .Release.Name }} rules: - - apiGroups: ["apiextensions.k8s.io"] - resources: ["customresourcedefinitions"] - verbs: ["get", "list", "watch", "update", "delete"] - resourceNames: - - "arangodeployments.database.arangodb.com" - - "arangoclustersynchronizations.database.arangodb.com" - - "arangomembers.database.arangodb.com" - - "arangotasks.database.arangodb.com" - - "arangodeploymentreplications.replication.database.arangodb.com" - - "arangobackups.backup.arangodb.com" - - "arangobackuppolicies.backup.arangodb.com" - - "arangojobs.apps.arangodb.com" - - "arangolocalstorages.storage.arangodb.com" +# analytics.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "graphanalyticsengines.analytics.arangodb.com" +# apps.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangojobs.apps.arangodb.com" +# backup.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangobackuppolicies.backup.arangodb.com" + - "arangobackups.backup.arangodb.com" +# database.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangoclustersynchronizations.database.arangodb.com" + - "arangodeployments.database.arangodb.com" + - "arangomembers.database.arangodb.com" + - "arangotasks.database.arangodb.com" +# ml.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangomlbatchjobs.ml.arangodb.com" + - "arangomlcronjobs.ml.arangodb.com" + - "arangomlextensions.ml.arangodb.com" + - "arangomlstorages.ml.arangodb.com" +# networking.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangoroutes.networking.arangodb.com" +# replication.database.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangodeploymentreplications.replication.database.arangodb.com" +# scheduler.arangodb.com + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "update", "delete"] + resourceNames: + - "arangoprofiles.scheduler.arangodb.com" {{- end }} {{- end }} diff --git a/docs/api/ArangoDeployment.V1.md b/docs/api/ArangoDeployment.V1.md index 4ce859c18..7c3763641 100644 --- a/docs/api/ArangoDeployment.V1.md +++ b/docs/api/ArangoDeployment.V1.md @@ -3045,7 +3045,7 @@ Type: `boolean` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1. ### .spec.gateway.enabled -Type: `boolean` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/deployment/v1/deployment_spec_gateway.go#L29) +Type: `boolean` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/deployment/v1/deployment_spec_gateway.go#L33) Enabled setting enables/disables support for gateway in the cluster. When enabled, the cluster will contain a number of `gateway` servers. @@ -3056,13 +3056,205 @@ Default Value: `false` ### .spec.gateway.image -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/deployment/v1/deployment_spec_gateway.go#L33) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/deployment/v1/deployment_spec_gateway.go#L37) Image is the image to use for the gateway. By default, the image is determined by the operator. *** +### .spec.gateway.sidecar.args + +Type: `array` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/container/resources/core.go#L50) + +Arguments to the entrypoint. +The container image's CMD is used if this is not provided. +Variable references $(VAR_NAME) are expanded using the container's environment. 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. Cannot be updated. + +Links: +* [Kubernetes Docs](https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell) + +*** + +### .spec.gateway.sidecar.command + +Type: `array` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/container/resources/core.go#L40) + +Entrypoint array. Not executed within a shell. +The container image's ENTRYPOINT is used if this is not provided. +Variable references $(VAR_NAME) are expanded using the container's environment. 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. Cannot be updated. + +Links: +* [Kubernetes Docs](https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell) + +*** + +### .spec.gateway.sidecar.controllerListenPort + +Type: `integer` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/integration.go#L36) + +ControllerListenPort defines on which port the sidecar container will be listening for controller requests + +Default Value: `9202` + +*** + +### .spec.gateway.sidecar.env + +Type: `core.EnvVar` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/container/resources/environments.go#L36) + +Env keeps the information about environment variables provided to the container + +Links: +* [Kubernetes Docs](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#envvar-v1-core) + +*** + +### .spec.gateway.sidecar.envFrom + +Type: `core.EnvFromSource` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/container/resources/environments.go#L41) + +EnvFrom keeps the information about environment variable sources provided to the container + +Links: +* [Kubernetes Docs](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#envfromsource-v1-core) + +*** + +### .spec.gateway.sidecar.image + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/container/resources/image.go#L35) + +Image define image details + +*** + +### .spec.gateway.sidecar.imagePullPolicy + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/container/resources/image.go#L39) + +ImagePullPolicy define Image pull policy + +Default Value: `IfNotPresent` + +*** + +### .spec.gateway.sidecar.lifecycle + +Type: `core.Lifecycle` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/container/resources/lifecycle.go#L35) + +Lifecycle keeps actions that the management system should take in response to container lifecycle events. + +*** + +### .spec.gateway.sidecar.listenPort + +Type: `integer` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/integration.go#L32) + +ListenPort defines on which port the sidecar container will be listening for connections + +Default Value: `9201` + +*** + +### .spec.gateway.sidecar.livenessProbe + +Type: `core.Probe` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/container/resources/probes.go#L37) + +LivenessProbe keeps configuration of periodic probe of container liveness. +Container will be restarted if the probe fails. + +Links: +* [Kubernetes docs](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes) + +*** + +### .spec.gateway.sidecar.ports + +Type: `[]core.ContainerPort` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/container/resources/networking.go#L39) + +Ports contains list of ports to expose from the container. Not specifying a port here +DOES NOT prevent that port from being exposed. Any port which is +listening on the default "0.0.0.0" address inside a container will be +accessible from the network. + +*** + +### .spec.gateway.sidecar.readinessProbe + +Type: `core.Probe` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/container/resources/probes.go#L42) + +ReadinessProbe keeps configuration of periodic probe of container service readiness. +Container will be removed from service endpoints if the probe fails. + +Links: +* [Kubernetes docs](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes) + +*** + +### .spec.gateway.sidecar.resources + +Type: `core.ResourceRequirements` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/container/resources/resources.go#L37) + +Resources holds resource requests & limits for container + +Links: +* [Documentation of core.ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#resourcerequirements-v1-core) + +*** + +### .spec.gateway.sidecar.securityContext + +Type: `core.SecurityContext` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/container/resources/security.go#L35) + +SecurityContext holds container-level security attributes and common container settings. + +Links: +* [Kubernetes docs](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/) + +*** + +### .spec.gateway.sidecar.startupProbe + +Type: `core.Probe` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/container/resources/probes.go#L50) + +StartupProbe indicates that the Pod has successfully initialized. +If specified, no other probes are executed until this completes successfully. +If this probe fails, the Pod will be restarted, just as if the livenessProbe failed. +This can be used to provide different probe parameters at the beginning of a Pod's lifecycle, +when it might take a long time to load data or warm a cache, than during steady-state operation. + +Links: +* [Kubernetes docs](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes) + +*** + +### .spec.gateway.sidecar.volumeMounts + +Type: `[]core.VolumeMount` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/container/resources/volume_mounts.go#L35) + +VolumeMounts keeps list of pod volumes to mount into the container's filesystem. + +*** + +### .spec.gateway.sidecar.workingDir + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/scheduler/v1beta1/container/resources/core.go#L55) + +Container's working directory. +If not specified, the container runtime's default will be used, which +might be configured in the container image. + +*** + ### .spec.gateways.affinity Type: `core.PodAffinity` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/deployment/v1/server_group_spec.go#L156) diff --git a/docs/api/ArangoRoute.V1Alpha1.md b/docs/api/ArangoRoute.V1Alpha1.md index 63e46c1b4..e69bf4c81 100644 --- a/docs/api/ArangoRoute.V1Alpha1.md +++ b/docs/api/ArangoRoute.V1Alpha1.md @@ -12,7 +12,15 @@ title: ArangoRoute V1Alpha1 Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_spec.go#L27) -DeploymentName specifies the ArangoDeployment object name +Deployment specifies the ArangoDeployment object name + +*** + +### .spec.destination.path + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_spec_destination.go#L36) + +Path defines service path used for overrides *** @@ -123,15 +131,29 @@ UID keeps the information about object UID *** -### .status.targets\[int\].tls.insecure +### .status.target.destinations\[int\].host + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_status_target_destination.go#L38) + +*** + +### .status.target.destinations\[int\].port + +Type: `integer` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_status_target_destination.go#L39) + +*** + +### .status.target.path + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_status_target.go#L37) + +Path specifies request path override + +*** + +### .status.target.TLS.insecure Type: `boolean` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_status_target_tls.go#L27) Insecure allows Insecure traffic -*** - -### .status.targets\[int\].url - -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_status_target.go#L34) - diff --git a/examples/single-server-route.yaml b/examples/single-server-route.yaml new file mode 100644 index 000000000..abc6aa6c3 --- /dev/null +++ b/examples/single-server-route.yaml @@ -0,0 +1,28 @@ +apiVersion: "database.arangodb.com/v1" +kind: "ArangoDeployment" +metadata: + name: "example-simple-single" +spec: + mode: Single + image: 'arangodb/arangodb:3.12.2' + gateway: + enabled: true + gateways: + count: 1 +--- +apiVersion: "networking.arangodb.com/v1alpha1" +kind: "ArangoRoute" +metadata: + name: "example-simple-single-route" +spec: + deployment: example-simple-single + destination: + service: + name: example-simple-single + port: 8529 + schema: https + tls: + insecure: true + path: "/_api/" + route: + path: "/secondary/" diff --git a/internal/docs_test.go b/internal/docs_test.go index b1c8a2003..e6b86319d 100644 --- a/internal/docs_test.go +++ b/internal/docs_test.go @@ -175,6 +175,14 @@ func Test_GenerateAPIDocs(t *testing.T) { "Spec": deploymentApi.ArangoMember{}.Spec, }, }, + Shared: []string{ + "shared/v1", + "scheduler/v1beta1", + "scheduler/v1beta1/container", + "scheduler/v1beta1/container/resources", + "scheduler/v1beta1/pod", + "scheduler/v1beta1/pod/resources", + }, }, }, "apps": map[string]inputPackage{ diff --git a/pkg/apis/deployment/v1/deployment_spec_gateway.go b/pkg/apis/deployment/v1/deployment_spec_gateway.go index 95f8bb41d..c194f1880 100644 --- a/pkg/apis/deployment/v1/deployment_spec_gateway.go +++ b/pkg/apis/deployment/v1/deployment_spec_gateway.go @@ -20,7 +20,11 @@ package v1 -import "github.com/arangodb/kube-arangodb/pkg/util" +import ( + schedulerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1" + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" + "github.com/arangodb/kube-arangodb/pkg/util" +) type DeploymentSpecGateway struct { // Enabled setting enables/disables support for gateway in the cluster. @@ -31,6 +35,9 @@ type DeploymentSpecGateway struct { // Image is the image to use for the gateway. // By default, the image is determined by the operator. Image *string `json:"image"` + + // Sidecar define the integration sidecar spec + Sidecar *schedulerApi.IntegrationSidecar `json:"sidecar,omitempty"` } // IsEnabled returns whether the gateway is enabled. @@ -42,9 +49,22 @@ func (d *DeploymentSpecGateway) IsEnabled() bool { return *d.Enabled } +func (d *DeploymentSpecGateway) GetSidecar() *schedulerApi.IntegrationSidecar { + if d == nil || d.Sidecar == nil { + return nil + } + return d.Sidecar +} + // Validate the given spec func (d *DeploymentSpecGateway) Validate() error { - return nil + if d == nil { + d = &DeploymentSpecGateway{} + } + + return shared.WithErrors( + shared.PrefixResourceErrors("integrationSidecar", d.GetSidecar().Validate()), + ) } // GetImage returns the image to use for the gateway. diff --git a/pkg/apis/deployment/v1/server_group.go b/pkg/apis/deployment/v1/server_group.go index d3795422e..9396cf780 100644 --- a/pkg/apis/deployment/v1/server_group.go +++ b/pkg/apis/deployment/v1/server_group.go @@ -230,6 +230,8 @@ func (g ServerGroup) DefaultTerminationGracePeriod() time.Duration { return time.Hour case ServerGroupCoordinators: return time.Hour + case ServerGroupGateways: + return 15 * time.Minute default: return time.Second * 30 } diff --git a/pkg/apis/deployment/v1/zz_generated.deepcopy.go b/pkg/apis/deployment/v1/zz_generated.deepcopy.go index bb26742cf..5993516d7 100644 --- a/pkg/apis/deployment/v1/zz_generated.deepcopy.go +++ b/pkg/apis/deployment/v1/zz_generated.deepcopy.go @@ -28,6 +28,7 @@ package v1 import ( time "time" + v1beta1 "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1" sharedv1 "github.com/arangodb/kube-arangodb/pkg/apis/shared/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -1184,6 +1185,11 @@ func (in *DeploymentSpecGateway) DeepCopyInto(out *DeploymentSpecGateway) { *out = new(string) **out = **in } + if in.Sidecar != nil { + in, out := &in.Sidecar, &out.Sidecar + *out = new(v1beta1.IntegrationSidecar) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/apis/deployment/v2alpha1/deployment_spec_gateway.go b/pkg/apis/deployment/v2alpha1/deployment_spec_gateway.go index 5909fecf1..166916462 100644 --- a/pkg/apis/deployment/v2alpha1/deployment_spec_gateway.go +++ b/pkg/apis/deployment/v2alpha1/deployment_spec_gateway.go @@ -20,7 +20,11 @@ package v2alpha1 -import "github.com/arangodb/kube-arangodb/pkg/util" +import ( + schedulerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1" + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" + "github.com/arangodb/kube-arangodb/pkg/util" +) type DeploymentSpecGateway struct { // Enabled setting enables/disables support for gateway in the cluster. @@ -31,6 +35,9 @@ type DeploymentSpecGateway struct { // Image is the image to use for the gateway. // By default, the image is determined by the operator. Image *string `json:"image"` + + // Sidecar define the integration sidecar spec + Sidecar *schedulerApi.IntegrationSidecar `json:"sidecar,omitempty"` } // IsEnabled returns whether the gateway is enabled. @@ -42,9 +49,22 @@ func (d *DeploymentSpecGateway) IsEnabled() bool { return *d.Enabled } +func (d *DeploymentSpecGateway) GetSidecar() *schedulerApi.IntegrationSidecar { + if d == nil || d.Sidecar == nil { + return nil + } + return d.Sidecar +} + // Validate the given spec func (d *DeploymentSpecGateway) Validate() error { - return nil + if d == nil { + d = &DeploymentSpecGateway{} + } + + return shared.WithErrors( + shared.PrefixResourceErrors("integrationSidecar", d.GetSidecar().Validate()), + ) } // GetImage returns the image to use for the gateway. diff --git a/pkg/apis/deployment/v2alpha1/server_group.go b/pkg/apis/deployment/v2alpha1/server_group.go index 1de521e7e..2051ec370 100644 --- a/pkg/apis/deployment/v2alpha1/server_group.go +++ b/pkg/apis/deployment/v2alpha1/server_group.go @@ -230,6 +230,8 @@ func (g ServerGroup) DefaultTerminationGracePeriod() time.Duration { return time.Hour case ServerGroupCoordinators: return time.Hour + case ServerGroupGateways: + return 15 * time.Minute default: return time.Second * 30 } diff --git a/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go b/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go index bc6b39d1d..f0ed3e2e3 100644 --- a/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go @@ -28,6 +28,7 @@ package v2alpha1 import ( time "time" + v1beta1 "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1" sharedv1 "github.com/arangodb/kube-arangodb/pkg/apis/shared/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -1184,6 +1185,11 @@ func (in *DeploymentSpecGateway) DeepCopyInto(out *DeploymentSpecGateway) { *out = new(string) **out = **in } + if in.Sidecar != nil { + in, out := &in.Sidecar, &out.Sidecar + *out = new(v1beta1.IntegrationSidecar) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/apis/networking/v1alpha1/route_spec.go b/pkg/apis/networking/v1alpha1/route_spec.go index a51f5eba2..6f4fc16e5 100644 --- a/pkg/apis/networking/v1alpha1/route_spec.go +++ b/pkg/apis/networking/v1alpha1/route_spec.go @@ -23,8 +23,8 @@ package v1alpha1 import shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" type ArangoRouteSpec struct { - // DeploymentName specifies the ArangoDeployment object name - DeploymentName *string `json:"deployment,omitempty"` + // Deployment specifies the ArangoDeployment object name + Deployment *string `json:"deployment,omitempty"` // Destination defines the route destination Destination *ArangoRouteSpecDestination `json:"destination,omitempty"` @@ -33,6 +33,14 @@ type ArangoRouteSpec struct { Route *ArangoRouteSpecRoute `json:"route,omitempty"` } +func (s *ArangoRouteSpec) GetDeployment() string { + if s == nil || s.Destination == nil { + return "" + } + + return *s.Deployment +} + func (s *ArangoRouteSpec) GetDestination() *ArangoRouteSpecDestination { if s == nil || s.Destination == nil { return nil @@ -54,7 +62,7 @@ func (s *ArangoRouteSpec) Validate() error { } if err := shared.WithErrors(shared.PrefixResourceErrors("spec", - shared.PrefixResourceErrors("deployment", shared.ValidateResourceNamePointer(s.DeploymentName)), + shared.PrefixResourceErrors("deployment", shared.ValidateResourceNamePointer(s.Deployment)), shared.ValidateRequiredInterfacePath("destination", s.Destination), shared.ValidateOptionalInterfacePath("route", s.Route), )); err != nil { diff --git a/pkg/apis/networking/v1alpha1/route_spec_destination.go b/pkg/apis/networking/v1alpha1/route_spec_destination.go index c8dbb6a74..ff1c58018 100644 --- a/pkg/apis/networking/v1alpha1/route_spec_destination.go +++ b/pkg/apis/networking/v1alpha1/route_spec_destination.go @@ -31,30 +31,41 @@ type ArangoRouteSpecDestination struct { // TLS defines TLS Configuration TLS *ArangoRouteSpecDestinationTLS `json:"tls,omitempty"` + + // Path defines service path used for overrides + Path *string `json:"path,omitempty"` } -func (s *ArangoRouteSpecDestination) GetService() *ArangoRouteSpecDestinationService { - if s == nil || s.Service == nil { +func (a *ArangoRouteSpecDestination) GetService() *ArangoRouteSpecDestinationService { + if a == nil || a.Service == nil { return nil } - return s.Service + return a.Service } -func (s *ArangoRouteSpecDestination) GetSchema() *ArangoRouteSpecDestinationSchema { - if s == nil || s.Schema == nil { +func (a *ArangoRouteSpecDestination) GetSchema() *ArangoRouteSpecDestinationSchema { + if a == nil || a.Schema == nil { return nil } - return s.Schema + return a.Schema } -func (s *ArangoRouteSpecDestination) GetTLS() *ArangoRouteSpecDestinationTLS { - if s == nil || s.TLS == nil { +func (a *ArangoRouteSpecDestination) GetPath() string { + if a == nil || a.Path == nil { + return "/" + } + + return *a.Path +} + +func (a *ArangoRouteSpecDestination) GetTLS() *ArangoRouteSpecDestinationTLS { + if a == nil || a.TLS == nil { return nil } - return s.TLS + return a.TLS } func (a *ArangoRouteSpecDestination) Validate() error { @@ -66,6 +77,7 @@ func (a *ArangoRouteSpecDestination) Validate() error { shared.ValidateOptionalInterfacePath("service", a.Service), shared.ValidateOptionalInterfacePath("schema", a.Schema), shared.ValidateOptionalInterfacePath("tls", a.TLS), + shared.PrefixResourceError("path", shared.ValidateAPIPath(a.GetPath())), ); err != nil { return err } diff --git a/pkg/apis/networking/v1alpha1/route_spec_route.go b/pkg/apis/networking/v1alpha1/route_spec_route.go index 9537b6abd..48563a511 100644 --- a/pkg/apis/networking/v1alpha1/route_spec_route.go +++ b/pkg/apis/networking/v1alpha1/route_spec_route.go @@ -31,7 +31,7 @@ type ArangoRouteSpecRoute struct { func (a *ArangoRouteSpecRoute) GetPath() string { if a == nil || a.Path == nil { - return "/" + return "" } return *a.Path @@ -43,7 +43,7 @@ func (a *ArangoRouteSpecRoute) Validate() error { } if err := shared.WithErrors( - shared.PrefixResourceError("path", shared.ValidateAPIPath(a.GetPath())), + shared.ValidateRequiredPath("path", a.Path, shared.ValidateAPIPath), ); err != nil { return err } diff --git a/pkg/apis/networking/v1alpha1/route_status.go b/pkg/apis/networking/v1alpha1/route_status.go index 8829c5e11..5fd3108b1 100644 --- a/pkg/apis/networking/v1alpha1/route_status.go +++ b/pkg/apis/networking/v1alpha1/route_status.go @@ -33,6 +33,6 @@ type ArangoRouteStatus struct { // Deployment keeps the ArangoDeployment reference Deployment *sharedApi.Object `json:"deployment,omitempty"` - // Targets keeps the target details - Targets ArangoRouteStatusTargets `json:"targets,omitempty"` + // Target keeps the target details + Target *ArangoRouteStatusTarget `json:"target,omitempty"` } diff --git a/pkg/apis/networking/v1alpha1/route_status_target.go b/pkg/apis/networking/v1alpha1/route_status_target.go index bd4b9152a..89f1b7a0d 100644 --- a/pkg/apis/networking/v1alpha1/route_status_target.go +++ b/pkg/apis/networking/v1alpha1/route_status_target.go @@ -20,25 +20,46 @@ package v1alpha1 -import "github.com/arangodb/kube-arangodb/pkg/util" +import ( + "fmt" -type ArangoRouteStatusTargets []ArangoRouteStatusTarget - -func (a ArangoRouteStatusTargets) Hash() string { - return util.SHA256FromExtract(func(t ArangoRouteStatusTarget) string { - return t.Hash() - }, a...) -} + "github.com/arangodb/kube-arangodb/pkg/util" +) type ArangoRouteStatusTarget struct { - Url string `json:"url,omitempty"` + // Destinations keeps target destinations + Destinations ArangoRouteStatusTargetDestinations `json:"destinations,omitempty"` - TLS ArangoRouteStatusTargetTLS `json:"tls,omitempty"` + // TLS Keeps target TLS Settings (if not nil, TLS is enabled) + TLS *ArangoRouteStatusTargetTLS `json:"TLS,omitempty"` + + // Path specifies request path override + Path string `json:"path,omitempty"` +} + +func (a *ArangoRouteStatusTarget) RenderURLs() []string { + if a == nil { + return nil + } + + var urls = make([]string, len(a.Destinations)) + + proto := "http" + + if a.TLS != nil { + proto = "https" + } + + for id, dest := range a.Destinations { + urls[id] = fmt.Sprintf("%s://%s:%d%s", proto, dest.Host, dest.Port, a.Path) + } + + return urls } func (a *ArangoRouteStatusTarget) Hash() string { if a == nil { return "" } - return util.SHA256FromStringArray(a.Url, a.TLS.Hash()) + return util.SHA256FromStringArray(a.Destinations.Hash(), a.TLS.Hash(), a.Path) } diff --git a/pkg/apis/networking/v1alpha1/route_status_target_destination.go b/pkg/apis/networking/v1alpha1/route_status_target_destination.go new file mode 100644 index 000000000..2fd61456d --- /dev/null +++ b/pkg/apis/networking/v1alpha1/route_status_target_destination.go @@ -0,0 +1,47 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package v1alpha1 + +import ( + "fmt" + + "github.com/arangodb/kube-arangodb/pkg/util" +) + +type ArangoRouteStatusTargetDestinations []ArangoRouteStatusTargetDestination + +func (a ArangoRouteStatusTargetDestinations) Hash() string { + return util.SHA256FromExtract(func(t ArangoRouteStatusTargetDestination) string { + return t.Hash() + }, a...) +} + +type ArangoRouteStatusTargetDestination struct { + Host string `json:"host,omitempty"` + Port int32 `json:"port,omitempty"` +} + +func (a *ArangoRouteStatusTargetDestination) Hash() string { + if a == nil { + return "" + } + return util.SHA256FromStringArray(fmt.Sprintf("%s:%d", a.Host, a.Port)) +} diff --git a/pkg/apis/networking/v1alpha1/route_status_target_tls.go b/pkg/apis/networking/v1alpha1/route_status_target_tls.go index c813655c1..b230c7b26 100644 --- a/pkg/apis/networking/v1alpha1/route_status_target_tls.go +++ b/pkg/apis/networking/v1alpha1/route_status_target_tls.go @@ -24,7 +24,7 @@ import "github.com/arangodb/kube-arangodb/pkg/util" type ArangoRouteStatusTargetTLS struct { // Insecure allows Insecure traffic - Insecure bool `json:"insecure"` + Insecure *bool `json:"insecure"` } func (a *ArangoRouteStatusTargetTLS) Hash() string { @@ -32,5 +32,13 @@ func (a *ArangoRouteStatusTargetTLS) Hash() string { return "" } - return util.SHA256FromStringArray(util.BoolSwitch(a.Insecure, "true", "false")) + return util.SHA256FromStringArray(util.BoolSwitch(a.IsInsecure(), "true", "false")) +} + +func (a *ArangoRouteStatusTargetTLS) IsInsecure() bool { + if a == nil || a.Insecure == nil { + return false + } + + return *a.Insecure } diff --git a/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go index 1dfa0daa4..ba179a20b 100644 --- a/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go @@ -26,7 +26,7 @@ package v1alpha1 import ( - deploymentv1 "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + v2alpha1 "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" v1 "github.com/arangodb/kube-arangodb/pkg/apis/shared/v1" runtime "k8s.io/apimachinery/pkg/runtime" intstr "k8s.io/apimachinery/pkg/util/intstr" @@ -96,8 +96,8 @@ func (in *ArangoRouteList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ArangoRouteSpec) DeepCopyInto(out *ArangoRouteSpec) { *out = *in - if in.DeploymentName != nil { - in, out := &in.DeploymentName, &out.DeploymentName + if in.Deployment != nil { + in, out := &in.Deployment, &out.Deployment *out = new(string) **out = **in } @@ -142,6 +142,11 @@ func (in *ArangoRouteSpecDestination) DeepCopyInto(out *ArangoRouteSpecDestinati *out = new(ArangoRouteSpecDestinationTLS) (*in).DeepCopyInto(*out) } + if in.Path != nil { + in, out := &in.Path, &out.Path + *out = new(string) + **out = **in + } return } @@ -228,7 +233,7 @@ func (in *ArangoRouteStatus) DeepCopyInto(out *ArangoRouteStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make(deploymentv1.ConditionList, len(*in)) + *out = make(v2alpha1.ConditionList, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -238,10 +243,10 @@ func (in *ArangoRouteStatus) DeepCopyInto(out *ArangoRouteStatus) { *out = new(v1.Object) (*in).DeepCopyInto(*out) } - if in.Targets != nil { - in, out := &in.Targets, &out.Targets - *out = make(ArangoRouteStatusTargets, len(*in)) - copy(*out, *in) + if in.Target != nil { + in, out := &in.Target, &out.Target + *out = new(ArangoRouteStatusTarget) + (*in).DeepCopyInto(*out) } return } @@ -259,7 +264,16 @@ func (in *ArangoRouteStatus) DeepCopy() *ArangoRouteStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ArangoRouteStatusTarget) DeepCopyInto(out *ArangoRouteStatusTarget) { *out = *in - out.TLS = in.TLS + if in.Destinations != nil { + in, out := &in.Destinations, &out.Destinations + *out = make(ArangoRouteStatusTargetDestinations, len(*in)) + copy(*out, *in) + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(ArangoRouteStatusTargetTLS) + (*in).DeepCopyInto(*out) + } return } @@ -273,9 +287,50 @@ func (in *ArangoRouteStatusTarget) DeepCopy() *ArangoRouteStatusTarget { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ArangoRouteStatusTargetDestination) DeepCopyInto(out *ArangoRouteStatusTargetDestination) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArangoRouteStatusTargetDestination. +func (in *ArangoRouteStatusTargetDestination) DeepCopy() *ArangoRouteStatusTargetDestination { + if in == nil { + return nil + } + out := new(ArangoRouteStatusTargetDestination) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ArangoRouteStatusTargetDestinations) DeepCopyInto(out *ArangoRouteStatusTargetDestinations) { + { + in := &in + *out = make(ArangoRouteStatusTargetDestinations, len(*in)) + copy(*out, *in) + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArangoRouteStatusTargetDestinations. +func (in ArangoRouteStatusTargetDestinations) DeepCopy() ArangoRouteStatusTargetDestinations { + if in == nil { + return nil + } + out := new(ArangoRouteStatusTargetDestinations) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ArangoRouteStatusTargetTLS) DeepCopyInto(out *ArangoRouteStatusTargetTLS) { *out = *in + if in.Insecure != nil { + in, out := &in.Insecure, &out.Insecure + *out = new(bool) + **out = **in + } return } @@ -288,23 +343,3 @@ func (in *ArangoRouteStatusTargetTLS) DeepCopy() *ArangoRouteStatusTargetTLS { in.DeepCopyInto(out) return out } - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in ArangoRouteStatusTargets) DeepCopyInto(out *ArangoRouteStatusTargets) { - { - in := &in - *out = make(ArangoRouteStatusTargets, len(*in)) - copy(*out, *in) - return - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArangoRouteStatusTargets. -func (in ArangoRouteStatusTargets) DeepCopy() ArangoRouteStatusTargets { - if in == nil { - return nil - } - out := new(ArangoRouteStatusTargets) - in.DeepCopyInto(out) - return *out -} diff --git a/pkg/apis/scheduler/v1alpha1/pod/resources/metadata.go b/pkg/apis/scheduler/v1alpha1/pod/resources/metadata.go index bb557305d..805402426 100644 --- a/pkg/apis/scheduler/v1alpha1/pod/resources/metadata.go +++ b/pkg/apis/scheduler/v1alpha1/pod/resources/metadata.go @@ -59,9 +59,9 @@ func (m *Metadata) Apply(template *core.PodTemplateSpec) error { z := m.DeepCopy() - template.Labels = z.Labels - template.Annotations = z.Annotations - template.OwnerReferences = z.OwnerReferences + template.Labels = util.MergeMaps(true, template.Labels, z.Labels) + template.Annotations = util.MergeMaps(true, template.Annotations, z.Annotations) + template.OwnerReferences = append(template.OwnerReferences, z.OwnerReferences...) return nil } diff --git a/pkg/apis/scheduler/v1alpha1/pod/resources/metadata_test.go b/pkg/apis/scheduler/v1alpha1/pod/resources/metadata_test.go index ad4558078..0c23b7464 100644 --- a/pkg/apis/scheduler/v1alpha1/pod/resources/metadata_test.go +++ b/pkg/apis/scheduler/v1alpha1/pod/resources/metadata_test.go @@ -131,6 +131,49 @@ func Test_Metadata(t *testing.T) { require.Contains(t, pod.Annotations, "B2") require.EqualValues(t, "4", pod.Annotations["B2"]) + require.Len(t, pod.OwnerReferences, 2) + require.EqualValues(t, "test", pod.OwnerReferences[0].UID) + require.EqualValues(t, "test2", pod.OwnerReferences[1].UID) + }) + }) + t.Run("Update Templat", func(t *testing.T) { + applyMetadata(t, &core.PodTemplateSpec{ + ObjectMeta: meta.ObjectMeta{ + Labels: map[string]string{ + "A": "1", + }, + Annotations: map[string]string{ + "B": "2", + }, + OwnerReferences: []meta.OwnerReference{ + { + UID: "test", + }, + }, + }, + }, &Metadata{ + Labels: map[string]string{ + "A": "3", + }, + Annotations: map[string]string{ + "B2": "4", + }, + OwnerReferences: []meta.OwnerReference{ + { + UID: "test2", + }, + }, + })(func(t *testing.T, pod *core.PodTemplateSpec) { + require.Len(t, pod.Labels, 1) + require.Contains(t, pod.Labels, "A") + require.EqualValues(t, "3", pod.Labels["A"]) + + require.Len(t, pod.Annotations, 2) + require.Contains(t, pod.Annotations, "B") + require.EqualValues(t, "2", pod.Annotations["B"]) + require.Contains(t, pod.Annotations, "B2") + require.EqualValues(t, "4", pod.Annotations["B2"]) + require.Len(t, pod.OwnerReferences, 2) require.EqualValues(t, "test", pod.OwnerReferences[0].UID) require.EqualValues(t, "test2", pod.OwnerReferences[1].UID) diff --git a/pkg/apis/scheduler/v1beta1/pod/resources/metadata.go b/pkg/apis/scheduler/v1beta1/pod/resources/metadata.go index 674fb8278..1f2f6f4c8 100644 --- a/pkg/apis/scheduler/v1beta1/pod/resources/metadata.go +++ b/pkg/apis/scheduler/v1beta1/pod/resources/metadata.go @@ -59,9 +59,9 @@ func (m *Metadata) Apply(template *core.PodTemplateSpec) error { z := m.DeepCopy() - template.Labels = z.Labels - template.Annotations = z.Annotations - template.OwnerReferences = z.OwnerReferences + template.Labels = util.MergeMaps(true, template.Labels, z.Labels) + template.Annotations = util.MergeMaps(true, template.Annotations, z.Annotations) + template.OwnerReferences = append(template.OwnerReferences, z.OwnerReferences...) return nil } diff --git a/pkg/apis/scheduler/v1beta1/pod/resources/metadata_test.go b/pkg/apis/scheduler/v1beta1/pod/resources/metadata_test.go index ad4558078..42d9428d7 100644 --- a/pkg/apis/scheduler/v1beta1/pod/resources/metadata_test.go +++ b/pkg/apis/scheduler/v1beta1/pod/resources/metadata_test.go @@ -131,6 +131,49 @@ func Test_Metadata(t *testing.T) { require.Contains(t, pod.Annotations, "B2") require.EqualValues(t, "4", pod.Annotations["B2"]) + require.Len(t, pod.OwnerReferences, 2) + require.EqualValues(t, "test", pod.OwnerReferences[0].UID) + require.EqualValues(t, "test2", pod.OwnerReferences[1].UID) + }) + }) + t.Run("Update Template", func(t *testing.T) { + applyMetadata(t, &core.PodTemplateSpec{ + ObjectMeta: meta.ObjectMeta{ + Labels: map[string]string{ + "A": "1", + }, + Annotations: map[string]string{ + "B": "2", + }, + OwnerReferences: []meta.OwnerReference{ + { + UID: "test", + }, + }, + }, + }, &Metadata{ + Labels: map[string]string{ + "A": "3", + }, + Annotations: map[string]string{ + "B2": "4", + }, + OwnerReferences: []meta.OwnerReference{ + { + UID: "test2", + }, + }, + })(func(t *testing.T, pod *core.PodTemplateSpec) { + require.Len(t, pod.Labels, 1) + require.Contains(t, pod.Labels, "A") + require.EqualValues(t, "3", pod.Labels["A"]) + + require.Len(t, pod.Annotations, 2) + require.Contains(t, pod.Annotations, "B") + require.EqualValues(t, "2", pod.Annotations["B"]) + require.Contains(t, pod.Annotations, "B2") + require.EqualValues(t, "4", pod.Annotations["B2"]) + require.Len(t, pod.OwnerReferences, 2) require.EqualValues(t, "test", pod.OwnerReferences[0].UID) require.EqualValues(t, "test2", pod.OwnerReferences[1].UID) diff --git a/pkg/apis/shared/validate.go b/pkg/apis/shared/validate.go index 47e059533..2c746f63e 100644 --- a/pkg/apis/shared/validate.go +++ b/pkg/apis/shared/validate.go @@ -34,7 +34,7 @@ import ( var ( resourceNameRE = regexp.MustCompile(`^([0-9\-\.a-z])+$`) - apiPathRE = regexp.MustCompile(`^/([A-Za-z0-9\-]+/)*$`) + apiPathRE = regexp.MustCompile(`^/([_A-Za-z0-9\-]+/)*$`) ) const ( @@ -190,6 +190,19 @@ func ValidateList[T any](in []T, validator func(T) error) error { return WithErrors(errors...) } +// ValidateMap validates all elements on the list +func ValidateMap[T any](in map[string]T, validator func(string, T) error) error { + errors := make([]error, 0, len(in)) + + for id := range in { + if err := PrefixResourceError(fmt.Sprintf("`%s`", id), validator(id, in[id])); err != nil { + errors = append(errors, err) + } + } + + return WithErrors(errors...) +} + // ValidateImage Validates if provided image is valid func ValidateImage(image string) error { if image == "" { diff --git a/pkg/crd/crds/database-deployment.schema.generated.yaml b/pkg/crd/crds/database-deployment.schema.generated.yaml index be717c42f..af86e39dc 100644 --- a/pkg/crd/crds/database-deployment.schema.generated.yaml +++ b/pkg/crd/crds/database-deployment.schema.generated.yaml @@ -6577,6 +6577,496 @@ v1: Image is the image to use for the gateway. By default, the image is determined by the operator. type: string + sidecar: + description: Sidecar define the integration sidecar spec + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + controllerListenPort: + format: int32 + type: integer + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + type: object + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + resourceFieldRef: + properties: + containerName: + type: string + divisor: + type: string + resource: + type: string + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + type: object + type: object + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + type: string + x-kubernetes-int-or-string: true + scheme: + type: string + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + type: object + tcpSocket: + properties: + host: + type: string + port: + type: string + x-kubernetes-int-or-string: true + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + type: string + x-kubernetes-int-or-string: true + scheme: + type: string + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + type: object + tcpSocket: + properties: + host: + type: string + port: + type: string + x-kubernetes-int-or-string: true + type: object + type: object + type: object + listenPort: + format: int32 + type: integer + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + type: string + x-kubernetes-int-or-string: true + scheme: + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + type: string + type: object + type: array + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + type: string + x-kubernetes-int-or-string: true + scheme: + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resources: + properties: + claims: + items: + properties: + name: + type: string + type: object + type: array + limits: + additionalProperties: + type: string + type: object + requests: + additionalProperties: + type: string + type: object + type: object + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + type: string + x-kubernetes-int-or-string: true + scheme: + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + type: object + type: array + workingDir: + type: string + type: object type: object gateways: description: Gateways contain specification for Gateway pods running in deployment mode `Single` or `Cluster`. @@ -22604,6 +23094,496 @@ v1alpha: Image is the image to use for the gateway. By default, the image is determined by the operator. type: string + sidecar: + description: Sidecar define the integration sidecar spec + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + controllerListenPort: + format: int32 + type: integer + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + type: object + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + resourceFieldRef: + properties: + containerName: + type: string + divisor: + type: string + resource: + type: string + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + type: object + type: object + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + type: string + x-kubernetes-int-or-string: true + scheme: + type: string + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + type: object + tcpSocket: + properties: + host: + type: string + port: + type: string + x-kubernetes-int-or-string: true + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + type: string + x-kubernetes-int-or-string: true + scheme: + type: string + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + type: object + tcpSocket: + properties: + host: + type: string + port: + type: string + x-kubernetes-int-or-string: true + type: object + type: object + type: object + listenPort: + format: int32 + type: integer + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + type: string + x-kubernetes-int-or-string: true + scheme: + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + type: string + type: object + type: array + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + type: string + x-kubernetes-int-or-string: true + scheme: + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resources: + properties: + claims: + items: + properties: + name: + type: string + type: object + type: array + limits: + additionalProperties: + type: string + type: object + requests: + additionalProperties: + type: string + type: object + type: object + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + type: string + x-kubernetes-int-or-string: true + scheme: + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + type: object + type: array + workingDir: + type: string + type: object type: object gateways: description: Gateways contain specification for Gateway pods running in deployment mode `Single` or `Cluster`. @@ -38622,9 +39602,505 @@ v2alpha1: description: Gateway defined main Gateway configuration. properties: enabled: + description: |- + Enabled setting enables/disables support for gateway in the cluster. + When enabled, the cluster will contain a number of `gateway` servers. type: boolean image: + description: |- + Image is the image to use for the gateway. + By default, the image is determined by the operator. type: string + sidecar: + description: Sidecar define the integration sidecar spec + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + controllerListenPort: + format: int32 + type: integer + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + type: object + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + resourceFieldRef: + properties: + containerName: + type: string + divisor: + type: string + resource: + type: string + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + type: object + type: object + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + type: string + x-kubernetes-int-or-string: true + scheme: + type: string + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + type: object + tcpSocket: + properties: + host: + type: string + port: + type: string + x-kubernetes-int-or-string: true + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + type: string + x-kubernetes-int-or-string: true + scheme: + type: string + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + type: object + tcpSocket: + properties: + host: + type: string + port: + type: string + x-kubernetes-int-or-string: true + type: object + type: object + type: object + listenPort: + format: int32 + type: integer + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + type: string + x-kubernetes-int-or-string: true + scheme: + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + type: string + type: object + type: array + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + type: string + x-kubernetes-int-or-string: true + scheme: + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resources: + properties: + claims: + items: + properties: + name: + type: string + type: object + type: array + limits: + additionalProperties: + type: string + type: object + requests: + additionalProperties: + type: string + type: object + type: object + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + type: string + x-kubernetes-int-or-string: true + scheme: + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + type: object + type: array + workingDir: + type: string + type: object type: object gateways: description: Gateways contain specification for Gateway pods running in deployment mode `Single` or `Cluster`. diff --git a/pkg/crd/crds/networking-route.schema.generated.yaml b/pkg/crd/crds/networking-route.schema.generated.yaml index 796f3be0a..49d93b356 100644 --- a/pkg/crd/crds/networking-route.schema.generated.yaml +++ b/pkg/crd/crds/networking-route.schema.generated.yaml @@ -4,11 +4,14 @@ v1alpha1: spec: properties: deployment: - description: DeploymentName specifies the ArangoDeployment object name + description: Deployment specifies the ArangoDeployment object name type: string destination: description: Destination defines the route destination properties: + path: + description: Path defines service path used for overrides + type: string schema: description: Schema defines HTTP/S schema used for connection type: string diff --git a/pkg/deployment/images.go b/pkg/deployment/images.go index 2f1e016e0..21caa5a80 100644 --- a/pkg/deployment/images.go +++ b/pkg/deployment/images.go @@ -31,6 +31,7 @@ import ( meta "k8s.io/apimachinery/pkg/apis/meta/v1" api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + schedulerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1" shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" "github.com/arangodb/kube-arangodb/pkg/deployment/pod" "github.com/arangodb/kube-arangodb/pkg/deployment/resources" @@ -283,7 +284,7 @@ func (i *ImageUpdatePod) GetRole() string { return "id" } -func (i *ImageUpdatePod) Init(_ context.Context, _ interfaces.Inspector, pod *core.Pod) error { +func (i *ImageUpdatePod) Init(_ context.Context, _ interfaces.Inspector, pod *core.PodTemplateSpec) error { terminationGracePeriodSeconds := int64((time.Second * 30).Seconds()) pod.Spec.TerminationGracePeriodSeconds = &terminationGracePeriodSeconds pod.Spec.PriorityClassName = i.spec.ID.Get().PriorityClassName @@ -311,7 +312,7 @@ func (i *ImageUpdatePod) GetVolumes() []core.Volume { return getVolumes(i.AsInput()).Volumes() } -func (i *ImageUpdatePod) GetSidecars(*core.Pod) error { +func (i *ImageUpdatePod) GetSidecars(spec *core.PodTemplateSpec) error { return nil } @@ -503,6 +504,10 @@ func (a *ImageUpdatePod) AsInput() pod.Input { } } +func (i *ImageUpdatePod) Profiles() (schedulerApi.ProfileTemplates, error) { + return nil, nil +} + // GetExecutor returns the fixed path to the ArangoSync binary in the container. func (a *ArangoSyncIdentity) GetExecutor() string { return resources.ArangoSyncExecutor diff --git a/pkg/deployment/member/state.go b/pkg/deployment/member/state.go index 0738a9441..127a03051 100644 --- a/pkg/deployment/member/state.go +++ b/pkg/deployment/member/state.go @@ -132,6 +132,8 @@ func (s *stateInspector) RefreshState(ctx context.Context, members api.Deploymen if results[id].IsServing() { client = results[id].client } + case api.ServerGroupTypeGateway: + results[id] = s.fetchGatewayMemberState(ctxChild, members[id]) default: assertion.InvalidGroupKey.Assert(true, "Unable to fetch Health for an unknown group: %s", members[id].Group.AsRole()) results[id] = State{ @@ -178,6 +180,9 @@ func (s *stateInspector) RefreshState(ctx context.Context, members api.Deploymen case api.ServerGroupTypeArangoSync: // ArangoSync is considered as healthy when it is possible to fetch version. results[i].IsClusterHealthy = true + case api.ServerGroupTypeGateway: + // Gateway is considered as healthy when it is possible to fetch version. + results[i].IsClusterHealthy = true default: assertion.InvalidGroupKey.Assert(true, "Unable to fetch Health for an unknown group: %s", members[i].Group.AsRole()) results[i].IsClusterHealthy = false @@ -228,6 +233,26 @@ func (s *stateInspector) fetchArangosyncMemberState(ctx context.Context, m api.D return state } +func (s *stateInspector) fetchGatewayMemberState(ctx context.Context, m api.DeploymentStatusMemberElement) State { + // by default, it is not serving. It will be changed if it serves. + var state State + c, err := s.deployment.GetServerClient(ctx, m.Group, m.Member.ID) + if err != nil { + state.NotReachableErr = err + return state + } + + if v, err := c.Version(ctx); err != nil { + state.NotReachableErr = err + return state + } else { + state.Version = v + state.client = c + } + + return state +} + func (s *stateInspector) fetchServerMemberState(ctx context.Context, m api.DeploymentStatusMemberElement, servingGroup api.ServerGroup) State { // by default, it is not serving. It will be changed if it serves. diff --git a/pkg/deployment/pod/probes.go b/pkg/deployment/pod/probes.go index d114644e7..c91d4354b 100644 --- a/pkg/deployment/pod/probes.go +++ b/pkg/deployment/pod/probes.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2024 ArangoDB GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -81,4 +81,9 @@ var probeMap = map[api.ServerGroup]probes{ liveness: newProbe(true, true), readiness: newProbe(false, false), }, + api.ServerGroupGateways: { // TODO: Enable Probes + startup: newProbe(false, false), + liveness: newProbe(false, false), + readiness: newProbe(false, false), + }, } diff --git a/pkg/deployment/reconcile/action_runtime_sync_tolerations.go b/pkg/deployment/reconcile/action_runtime_sync_tolerations.go index 532ef2cca..39aeb0741 100644 --- a/pkg/deployment/reconcile/action_runtime_sync_tolerations.go +++ b/pkg/deployment/reconcile/action_runtime_sync_tolerations.go @@ -29,6 +29,7 @@ import ( api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" "github.com/arangodb/kube-arangodb/pkg/deployment/patch" + "github.com/arangodb/kube-arangodb/pkg/deployment/resources" "github.com/arangodb/kube-arangodb/pkg/util/errors" "github.com/arangodb/kube-arangodb/pkg/util/globals" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/tolerations" @@ -78,7 +79,7 @@ func (a actionRuntimeContainerSyncTolerations) Start(ctx context.Context) (bool, expectedTolerations := member.Spec.Template.PodSpec.Spec.Tolerations - origTolerations := tolerations.CreatePodTolerations(a.actionCtx.GetMode(), a.action.Group) + origTolerations := resources.CreatePodTolerations(a.actionCtx.GetMode(), a.action.Group) calculatedTolerations := tolerations.MergeTolerationsIfNotFound(currentTolerations, origTolerations, expectedTolerations) diff --git a/pkg/deployment/resources/config_map_gateway.go b/pkg/deployment/resources/config_map_gateway.go new file mode 100644 index 000000000..6f1668696 --- /dev/null +++ b/pkg/deployment/resources/config_map_gateway.go @@ -0,0 +1,182 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package resources + +import ( + "context" + "fmt" + "path/filepath" + + core "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + networkingApi "github.com/arangodb/kube-arangodb/pkg/apis/networking/v1alpha1" + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" + "github.com/arangodb/kube-arangodb/pkg/deployment/resources/gateway" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/constants" + "github.com/arangodb/kube-arangodb/pkg/util/errors" + "github.com/arangodb/kube-arangodb/pkg/util/globals" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" + inspectorInterface "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector" + configMapsV1 "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/configmap/v1" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/patcher" +) + +func (r *Resources) ensureGatewayConfig(ctx context.Context, cachedStatus inspectorInterface.Inspector, configMaps configMapsV1.ModInterface) error { + deploymentName := r.context.GetAPIObject().GetName() + configMapName := GetGatewayConfigMapName(deploymentName) + + log := r.log.Str("section", "gateway-config").Str("name", configMapName) + + cfg, err := r.renderGatewayConfig(cachedStatus) + if err != nil { + return errors.WithStack(errors.Wrapf(err, "Failed to generate gateway config")) + } + + gatewayCfgYaml, gatewayCfgChecksum, _, err := cfg.RenderYAML() + if err != nil { + return errors.WithStack(errors.Wrapf(err, "Failed to render gateway config")) + } + + if cm, exists := cachedStatus.ConfigMap().V1().GetSimple(configMapName); !exists { + // Create + cm = &core.ConfigMap{ + ObjectMeta: meta.ObjectMeta{ + Name: configMapName, + }, + Data: map[string]string{ + GatewayConfigFileName: string(gatewayCfgYaml), + }, + } + + owner := r.context.GetAPIObject().AsOwner() + + err = globals.GetGlobalTimeouts().Kubernetes().RunWithTimeout(ctx, func(ctxChild context.Context) error { + return k8sutil.CreateConfigMap(ctxChild, configMaps, cm, &owner) + }) + if kerrors.IsAlreadyExists(err) { + // CM added while we tried it also + return nil + } else if err != nil { + // Failed to create + return errors.WithStack(err) + } + + return errors.Reconcile() + } else { + // CM Exists, checks checksum - if key is not in the map we return empty string + if existingSha := util.SHA256FromString(cm.Data[GatewayConfigFileName]); existingSha != gatewayCfgChecksum { + // We need to do the update + if _, changed, err := patcher.Patcher[*core.ConfigMap](ctx, cachedStatus.ConfigMapsModInterface().V1(), cm, meta.PatchOptions{}, + patcher.PatchConfigMapData(map[string]string{ + GatewayConfigFileName: string(gatewayCfgYaml), + })); err != nil { + log.Err(err).Debug("Failed to patch GatewayConfig ConfigMap") + return errors.WithStack(err) + } else if changed { + log.Str("service", cm.GetName()).Str("before", existingSha).Str("after", gatewayCfgChecksum).Info("Updated GatewayConfig") + } + } + } + return nil +} + +func (r *Resources) renderGatewayConfig(cachedStatus inspectorInterface.Inspector) (gateway.Config, error) { + deploymentName := r.context.GetAPIObject().GetName() + + log := r.log.Str("section", "gateway-config-render") + + spec := r.context.GetSpec() + svcServingName := fmt.Sprintf("%s-%s", deploymentName, spec.Mode.Get().ServingGroup().AsRole()) + + svc, svcExist := cachedStatus.Service().V1().GetSimple(svcServingName) + if !svcExist { + return gateway.Config{}, errors.Errorf("Service %s not found", svcServingName) + } + + var cfg gateway.Config + + cfg.DefaultDestination = gateway.ConfigDestination{ + Targets: []gateway.ConfigDestinationTarget{ + { + Host: svc.Spec.ClusterIP, + Port: shared.ArangoPort, + }, + }, + } + + if spec.TLS.IsSecure() { + // Enabled TLS, add config + keyPath := filepath.Join(shared.TLSKeyfileVolumeMountDir, constants.SecretTLSKeyfile) + cfg.DefaultTLS = &gateway.ConfigTLS{ + CertificatePath: keyPath, + PrivateKeyPath: keyPath, + } + cfg.DefaultDestination.Type = util.NewType(gateway.ConfigDestinationTypeHTTPS) + } + + // Check ArangoRoutes + if c, err := cachedStatus.ArangoRoute().V1Alpha1(); err == nil { + cfg.Destinations = gateway.ConfigDestinations{} + if err := c.Iterate(func(at *networkingApi.ArangoRoute) error { + log := log.Str("ArangoRoute", at.GetName()) + if !at.Status.Conditions.IsTrue(networkingApi.ReadyCondition) { + l := log + if c, ok := at.Status.Conditions.Get(networkingApi.ReadyCondition); ok { + l.Str("message", c.Message) + } + l.Warn("ArangoRoute is not ready") + + return nil + } + + if target := at.Status.Target; target != nil { + var dest gateway.ConfigDestination + if destinations := target.Destinations; len(destinations) > 0 { + for _, destination := range destinations { + var t gateway.ConfigDestinationTarget + + t.Host = destination.Host + t.Port = destination.Port + + dest.Targets = append(dest.Targets, t) + } + } + if tls := target.TLS; tls != nil { + dest.Type = util.NewType(gateway.ConfigDestinationTypeHTTPS) + } + dest.Path = util.NewType(target.Path) + cfg.Destinations[at.Spec.GetRoute().GetPath()] = dest + } + + return nil + + }, func(at *networkingApi.ArangoRoute) bool { + return at.Spec.GetDeployment() == deploymentName + }); err != nil { + return gateway.Config{}, errors.Wrapf(err, "Unable to iterate over ArangoRoutes") + } + } + + return cfg, nil +} diff --git a/pkg/deployment/resources/config_maps.go b/pkg/deployment/resources/config_maps.go index f1e5cf6d0..29a7f7317 100644 --- a/pkg/deployment/resources/config_maps.go +++ b/pkg/deployment/resources/config_maps.go @@ -22,21 +22,13 @@ package resources import ( "context" - "fmt" "time" - core "k8s.io/api/core/v1" - meta "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/arangodb/kube-arangodb/pkg/deployment/features" "github.com/arangodb/kube-arangodb/pkg/metrics" - "github.com/arangodb/kube-arangodb/pkg/util" "github.com/arangodb/kube-arangodb/pkg/util/errors" - "github.com/arangodb/kube-arangodb/pkg/util/globals" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" inspectorInterface "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector" - configMapsV1 "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/configmap/v1" - "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/kerrors" ) var ( @@ -65,49 +57,3 @@ func (r *Resources) EnsureConfigMaps(ctx context.Context, cachedStatus inspector } return reconcileRequired.Reconcile(ctx) } - -func (r *Resources) ensureGatewayConfig(ctx context.Context, cachedStatus inspectorInterface.Inspector, configMaps configMapsV1.ModInterface) error { - deploymentName := r.context.GetAPIObject().GetName() - configMapName := GetGatewayConfigMapName(deploymentName) - - if _, exists := cachedStatus.ConfigMap().V1().GetSimple(configMapName); !exists { - // Find serving service (single/crdn) - spec := r.context.GetSpec() - svcServingName := fmt.Sprintf("%s-%s", deploymentName, spec.Mode.Get().ServingGroup().AsRole()) - - svc, svcExist := cachedStatus.Service().V1().GetSimple(svcServingName) - if !svcExist { - return errors.Errorf("Service %s not found", svcServingName) - } - - gatewayCfgYaml, err := RenderGatewayConfigYAML(svc.Spec.ClusterIP) - if err != nil { - return errors.WithStack(errors.Wrapf(err, "Failed to render gateway config")) - } - cm := &core.ConfigMap{ - ObjectMeta: meta.ObjectMeta{ - Name: configMapName, - }, - Data: map[string]string{ - GatewayConfigFileName: string(gatewayCfgYaml), - GatewayConfigChecksumName: util.SHA256(gatewayCfgYaml), - }, - } - - owner := r.context.GetAPIObject().AsOwner() - - err = globals.GetGlobalTimeouts().Kubernetes().RunWithTimeout(ctx, func(ctxChild context.Context) error { - return k8sutil.CreateConfigMap(ctxChild, configMaps, cm, &owner) - }) - if kerrors.IsAlreadyExists(err) { - // CM added while we tried it also - return nil - } else if err != nil { - // Failed to create - return errors.WithStack(err) - } - - return errors.Reconcile() - } - return nil -} diff --git a/pkg/deployment/resources/gateway/gateway_config.go b/pkg/deployment/resources/gateway/gateway_config.go new file mode 100644 index 000000000..465ee41f1 --- /dev/null +++ b/pkg/deployment/resources/gateway/gateway_config.go @@ -0,0 +1,259 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package gateway + +import ( + "fmt" + "sort" + + bootstrapAPI "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3" + clusterAPI "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + coreAPI "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + listenerAPI "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + routeAPI "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + routerAPI "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3" + httpConnectionManagerAPI "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/anypb" + "sigs.k8s.io/yaml" + + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +type Config struct { + DefaultDestination ConfigDestination `json:"defaultDestination,omitempty"` + + Destinations ConfigDestinations `json:"destinations,omitempty"` + + DefaultTLS *ConfigTLS `json:"defaultTLS,omitempty"` +} + +func (c Config) Validate() error { + return errors.Errors( + shared.PrefixResourceErrors("defaultDestination", c.DefaultDestination.Validate()), + shared.PrefixResourceErrors("destinations", c.Destinations.Validate()), + ) +} + +func (c Config) RenderYAML() ([]byte, string, *bootstrapAPI.Bootstrap, error) { + cfg, err := c.Render() + if err != nil { + return nil, "", nil, err + } + + data, err := protojson.MarshalOptions{ + UseProtoNames: true, + }.Marshal(cfg) + if err != nil { + return nil, "", nil, err + } + + data, err = yaml.JSONToYAML(data) + return data, util.SHA256(data), cfg, err +} + +func (c Config) Render() (*bootstrapAPI.Bootstrap, error) { + if err := c.Validate(); err != nil { + return nil, errors.Wrapf(err, "Validation failed") + } + + clusters, err := c.RenderClusters() + if err != nil { + return nil, errors.Wrapf(err, "Unable to render clusters") + } + + listener, err := c.RenderListener() + if err != nil { + return nil, errors.Wrapf(err, "Unable to render listener") + } + + return &bootstrapAPI.Bootstrap{ + Admin: &bootstrapAPI.Admin{ + Address: &coreAPI.Address{ + Address: &coreAPI.Address_SocketAddress{ + SocketAddress: &coreAPI.SocketAddress{ + Address: "127.0.0.1", + PortSpecifier: &coreAPI.SocketAddress_PortValue{PortValue: 9901}, + }, + }, + }, + }, + StaticResources: &bootstrapAPI.Bootstrap_StaticResources{ + Listeners: []*listenerAPI.Listener{ + listener, + }, + Clusters: clusters, + }, + }, nil +} + +func (c Config) RenderClusters() ([]*clusterAPI.Cluster, error) { + def, err := c.DefaultDestination.RenderCluster("default") + if err != nil { + return nil, err + } + clusters := []*clusterAPI.Cluster{ + def, + } + + for k, v := range c.Destinations { + name := fmt.Sprintf("cluster_%s", util.SHA256FromString(k)) + c, err := v.RenderCluster(name) + if err != nil { + return nil, err + } + + clusters = append(clusters, c) + } + + sort.Slice(clusters, func(i, j int) bool { + return clusters[i].Name < clusters[j].Name + }) + + return clusters, nil +} + +func (c Config) RenderRoutes() ([]*routeAPI.Route, error) { + def, err := c.DefaultDestination.RenderRoute("default", "/") + if err != nil { + return nil, err + } + routes := []*routeAPI.Route{ + def, + } + + for k, v := range c.Destinations { + name := fmt.Sprintf("cluster_%s", util.SHA256FromString(k)) + c, err := v.RenderRoute(name, k) + if err != nil { + return nil, err + } + + routes = append(routes, c) + } + + sort.Slice(routes, func(i, j int) bool { + return routes[i].GetMatch().GetPrefix() > routes[j].GetMatch().GetPrefix() + }) + + return routes, nil +} + +func (c Config) RenderFilters() ([]*listenerAPI.Filter, error) { + httpFilterConfigType, err := anypb.New(&routerAPI.Router{}) + if err != nil { + return nil, errors.Wrapf(err, "Unable to render route config") + } + + routes, err := c.RenderRoutes() + if err != nil { + return nil, errors.Wrapf(err, "Unable to render routes") + } + + filterConfigType, err := anypb.New(&httpConnectionManagerAPI.HttpConnectionManager{ + StatPrefix: "ingress_http", + CodecType: httpConnectionManagerAPI.HttpConnectionManager_AUTO, + RouteSpecifier: &httpConnectionManagerAPI.HttpConnectionManager_RouteConfig{ + RouteConfig: &routeAPI.RouteConfiguration{ + Name: "default", + VirtualHosts: []*routeAPI.VirtualHost{ + { + Name: "default", + Domains: []string{"*"}, + Routes: routes, + }, + }, + }, + }, + HttpFilters: []*httpConnectionManagerAPI.HttpFilter{ + { + Name: "envoy.filters.http.routerAPI", + ConfigType: &httpConnectionManagerAPI.HttpFilter_TypedConfig{ + TypedConfig: httpFilterConfigType, + }, + }, + }, + }) + if err != nil { + return nil, errors.Wrapf(err, "Unable to render http connection manager") + } + + return []*listenerAPI.Filter{ + { + Name: "envoy.filters.network.httpConnectionManagerAPI", + ConfigType: &listenerAPI.Filter_TypedConfig{ + TypedConfig: filterConfigType, + }, + }, + }, nil +} + +func (c Config) RenderDefaultFilterChain() (*listenerAPI.FilterChain, error) { + filters, err := c.RenderFilters() + if err != nil { + return nil, errors.Wrapf(err, "Unable to render filters") + } + + ret := &listenerAPI.FilterChain{ + Filters: filters, + } + + if tls, err := c.DefaultTLS.RenderListenerTransportSocket(); err != nil { + return nil, err + } else { + ret.TransportSocket = tls + } + + return ret, nil +} + +func (c Config) RenderSecondaryFilterChains() ([]*listenerAPI.FilterChain, error) { + return nil, nil +} + +func (c Config) RenderListener() (*listenerAPI.Listener, error) { + filterChains, err := c.RenderSecondaryFilterChains() + if err != nil { + return nil, errors.Wrapf(err, "Unable to render secondary filter chains") + } + + defaultFilterChain, err := c.RenderDefaultFilterChain() + if err != nil { + return nil, errors.Wrapf(err, "Unable to render default filter") + } + + return &listenerAPI.Listener{ + Name: "default", + Address: &coreAPI.Address{ + Address: &coreAPI.Address_SocketAddress{ + SocketAddress: &coreAPI.SocketAddress{ + Address: "0.0.0.0", + PortSpecifier: &coreAPI.SocketAddress_PortValue{PortValue: shared.ArangoPort}, + }, + }, + }, + FilterChains: filterChains, + + DefaultFilterChain: defaultFilterChain, + }, nil +} diff --git a/pkg/deployment/resources/gateway/gateway_config_destination.go b/pkg/deployment/resources/gateway/gateway_config_destination.go new file mode 100644 index 000000000..29dbcace1 --- /dev/null +++ b/pkg/deployment/resources/gateway/gateway_config_destination.go @@ -0,0 +1,122 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package gateway + +import ( + "time" + + clusterAPI "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + endpointAPI "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" + routeAPI "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + "google.golang.org/protobuf/types/known/durationpb" + + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +type ConfigDestinations map[string]ConfigDestination + +func (c ConfigDestinations) Validate() error { + if len(c) == 0 { + return nil + } + return shared.WithErrors( + shared.ValidateMap(c, func(k string, destination ConfigDestination) error { + var errs []error + if k == "/" { + errs = append(errs, errors.Errorf("Route for `/` is reserved")) + } + if err := shared.ValidateAPIPath(k); err != nil { + errs = append(errs, err) + } + if err := destination.Validate(); err != nil { + errs = append(errs, err) + } + return shared.WithErrors(errs...) + }), + ) +} + +type ConfigDestination struct { + Targets ConfigDestinationTargets `json:"targets,omitempty"` + + Type *ConfigDestinationType `json:"type,omitempty"` + + Path *string `json:"path,omitempty"` +} + +func (c ConfigDestination) Validate() error { + return shared.WithErrors( + shared.PrefixResourceError("targets", c.Targets.Validate()), + shared.PrefixResourceError("type", c.Type.Validate()), + shared.PrefixResourceError("path", shared.ValidateAPIPath(c.GetPath())), + ) +} + +func (c ConfigDestination) GetPath() string { + if c.Path == nil { + return "/" + } + + return *c.Path +} + +func (c ConfigDestination) RenderRoute(name, prefix string) (*routeAPI.Route, error) { + return &routeAPI.Route{ + Match: &routeAPI.RouteMatch{ + PathSpecifier: &routeAPI.RouteMatch_Prefix{ + Prefix: prefix, + }, + }, + Action: &routeAPI.Route_Route{ + Route: &routeAPI.RouteAction{ + ClusterSpecifier: &routeAPI.RouteAction_Cluster{ + Cluster: name, + }, + PrefixRewrite: c.GetPath(), + }, + }, + }, nil +} + +func (c ConfigDestination) RenderCluster(name string) (*clusterAPI.Cluster, error) { + cluster := &clusterAPI.Cluster{ + Name: name, + ConnectTimeout: durationpb.New(time.Second), + LbPolicy: clusterAPI.Cluster_ROUND_ROBIN, + LoadAssignment: &endpointAPI.ClusterLoadAssignment{ + ClusterName: name, + Endpoints: []*endpointAPI.LocalityLbEndpoints{ + { + LbEndpoints: c.Targets.RenderEndpoints(), + }, + }, + }, + } + + if t, err := c.Type.RenderUpstreamTransportSocket(); err != nil { + return nil, err + } else { + cluster.TransportSocket = t + } + + return cluster, nil +} diff --git a/pkg/deployment/resources/gateway/gateway_config_destination_target.go b/pkg/deployment/resources/gateway/gateway_config_destination_target.go new file mode 100644 index 000000000..4f2d974c9 --- /dev/null +++ b/pkg/deployment/resources/gateway/gateway_config_destination_target.go @@ -0,0 +1,92 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package gateway + +import ( + coreAPI "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + endpointAPI "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" + + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +type ConfigDestinationTargets []ConfigDestinationTarget + +func (c ConfigDestinationTargets) RenderEndpoints() []*endpointAPI.LbEndpoint { + var endpoints = make([]*endpointAPI.LbEndpoint, len(c)) + + for id := range c { + endpoints[id] = c[id].RenderEndpoint() + } + + return endpoints +} + +func (c ConfigDestinationTargets) Validate() error { + if len(c) == 0 { + return errors.Errorf("Empty Target not allowed") + } + return shared.ValidateList(c, func(target ConfigDestinationTarget) error { + return target.Validate() + }) +} + +type ConfigDestinationTarget struct { + Host string `json:"ip,omitempty"` + Port int32 `json:"port,omitempty"` +} + +func (c ConfigDestinationTarget) Validate() error { + return shared.WithErrors( + shared.ValidateRequiredPath("ip", &c.Host, func(t string) error { + if t == "" { + return errors.Errorf("Empty string not allowed") + } + return nil + }), + shared.ValidateRequiredPath("ip", &c.Port, func(t int32) error { + if t <= 0 { + return errors.Errorf("Port needs to be greater than 0") + } + return nil + }), + ) +} + +func (c ConfigDestinationTarget) RenderEndpoint() *endpointAPI.LbEndpoint { + return &endpointAPI.LbEndpoint{ + HostIdentifier: &endpointAPI.LbEndpoint_Endpoint{ + Endpoint: &endpointAPI.Endpoint{ + Address: &coreAPI.Address{ + Address: &coreAPI.Address_SocketAddress{ + SocketAddress: &coreAPI.SocketAddress{ + Protocol: coreAPI.SocketAddress_TCP, + Address: c.Host, + PortSpecifier: &coreAPI.SocketAddress_PortValue{ + PortValue: uint32(c.Port), + }, + }, + }, + }, + }, + }, + } +} diff --git a/pkg/deployment/resources/gateway/gateway_config_destination_type.go b/pkg/deployment/resources/gateway/gateway_config_destination_type.go new file mode 100644 index 000000000..590a42a08 --- /dev/null +++ b/pkg/deployment/resources/gateway/gateway_config_destination_type.go @@ -0,0 +1,80 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package gateway + +import ( + coreAPI "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + tlsApi "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +type ConfigDestinationType int + +const ( + ConfigDestinationTypeHTTP ConfigDestinationType = iota + ConfigDestinationTypeHTTPS +) + +func (c *ConfigDestinationType) Get() ConfigDestinationType { + if c == nil { + return ConfigDestinationTypeHTTP + } + + switch v := *c; v { + case ConfigDestinationTypeHTTP, ConfigDestinationTypeHTTPS: + return v + default: + return ConfigDestinationTypeHTTP + } +} + +func (c *ConfigDestinationType) RenderUpstreamTransportSocket() (*coreAPI.TransportSocket, error) { + if c.Get() == ConfigDestinationTypeHTTPS { + tlsConfig, err := anypb.New(&tlsApi.UpstreamTlsContext{ + CommonTlsContext: &tlsApi.CommonTlsContext{ + ValidationContextType: &tlsApi.CommonTlsContext_ValidationContext{}, + }, + }) + if err != nil { + return nil, err + } + + return &coreAPI.TransportSocket{ + Name: "envoy.transport_sockets.tls", + ConfigType: &coreAPI.TransportSocket_TypedConfig{ + TypedConfig: tlsConfig, + }, + }, nil + } + + return nil, nil +} + +func (c *ConfigDestinationType) Validate() error { + switch c.Get() { + case ConfigDestinationTypeHTTP, ConfigDestinationTypeHTTPS: + return nil + default: + return errors.Errorf("Invalid destination type") + } +} diff --git a/pkg/deployment/resources/gateway/gateway_config_test.go b/pkg/deployment/resources/gateway/gateway_config_test.go new file mode 100644 index 000000000..d8655f03f --- /dev/null +++ b/pkg/deployment/resources/gateway/gateway_config_test.go @@ -0,0 +1,150 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package gateway + +import ( + "testing" + + bootstrapAPI "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3" + "github.com/stretchr/testify/require" + + "github.com/arangodb/kube-arangodb/pkg/util" +) + +func renderAndPrintGatewayConfig(t *testing.T, cfg Config) *bootstrapAPI.Bootstrap { + data, checksum, obj, err := cfg.RenderYAML() + require.NoError(t, err) + + t.Logf("Checksum: %s", checksum) + t.Log(string(data)) + + return obj +} + +func Test_GatewayConfig(t *testing.T) { + t.Run("Default", func(t *testing.T) { + renderAndPrintGatewayConfig(t, Config{ + DefaultDestination: ConfigDestination{ + Targets: []ConfigDestinationTarget{ + { + Host: "127.0.0.1", + Port: 12345, + }, + }, + }, + }) + }) + t.Run("Default", func(t *testing.T) { + renderAndPrintGatewayConfig(t, Config{ + DefaultDestination: ConfigDestination{ + Targets: []ConfigDestinationTarget{ + { + Host: "127.0.0.1", + Port: 12345, + }, + }, + Type: util.NewType(ConfigDestinationTypeHTTPS), + }, + DefaultTLS: &ConfigTLS{ + CertificatePath: "/test", + PrivateKeyPath: "/test12", + }, + }) + }) + t.Run("Default", func(t *testing.T) { + renderAndPrintGatewayConfig(t, Config{ + DefaultDestination: ConfigDestination{ + Targets: []ConfigDestinationTarget{ + { + Host: "127.0.0.1", + Port: 12345, + }, + }, + Path: util.NewType("/test/path/"), + Type: util.NewType(ConfigDestinationTypeHTTPS), + }, + DefaultTLS: &ConfigTLS{ + CertificatePath: "/test", + PrivateKeyPath: "/test12", + }, + }) + }) + t.Run("Default", func(t *testing.T) { + renderAndPrintGatewayConfig(t, Config{ + DefaultDestination: ConfigDestination{ + Targets: []ConfigDestinationTarget{ + { + Host: "127.0.0.1", + Port: 12345, + }, + }, + Path: util.NewType("/test/path/"), + Type: util.NewType(ConfigDestinationTypeHTTPS), + }, + DefaultTLS: &ConfigTLS{ + CertificatePath: "/test", + PrivateKeyPath: "/test12", + }, + Destinations: ConfigDestinations{ + "/test/": { + Targets: []ConfigDestinationTarget{ + { + Host: "127.0.0.1", + Port: 12346, + }, + }, + Path: util.NewType("/test/path/"), + Type: util.NewType(ConfigDestinationTypeHTTPS), + }, + }, + }) + }) + t.Run("Default", func(t *testing.T) { + renderAndPrintGatewayConfig(t, Config{ + DefaultDestination: ConfigDestination{ + Targets: []ConfigDestinationTarget{ + { + Host: "127.0.0.1", + Port: 12345, + }, + }, + Path: util.NewType("/test/path/"), + Type: util.NewType(ConfigDestinationTypeHTTPS), + }, + DefaultTLS: &ConfigTLS{ + CertificatePath: "/test", + PrivateKeyPath: "/test12", + }, + Destinations: ConfigDestinations{ + "/_test/": { + Targets: []ConfigDestinationTarget{ + { + Host: "127.0.0.1", + Port: 12346, + }, + }, + Path: util.NewType("/test/path/"), + Type: util.NewType(ConfigDestinationTypeHTTP), + }, + }, + }) + }) +} diff --git a/pkg/deployment/resources/gateway/gateway_config_tls.go b/pkg/deployment/resources/gateway/gateway_config_tls.go new file mode 100644 index 000000000..98f90fd3a --- /dev/null +++ b/pkg/deployment/resources/gateway/gateway_config_tls.go @@ -0,0 +1,69 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package gateway + +import ( + coreAPI "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + tlsApi "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +type ConfigTLS struct { + CertificatePath string `json:"certificatePath,omitempty"` + PrivateKeyPath string `json:"privateKeyPath,omitempty"` +} + +func (c *ConfigTLS) RenderListenerTransportSocket() (*coreAPI.TransportSocket, error) { + if c == nil { + return nil, nil + } + + tlsContext, err := anypb.New(&tlsApi.DownstreamTlsContext{ + CommonTlsContext: &tlsApi.CommonTlsContext{ + TlsCertificates: []*tlsApi.TlsCertificate{ + { + CertificateChain: &coreAPI.DataSource{ + Specifier: &coreAPI.DataSource_Filename{ + Filename: c.CertificatePath, + }, + }, + PrivateKey: &coreAPI.DataSource{ + Specifier: &coreAPI.DataSource_Filename{ + Filename: c.PrivateKeyPath, + }, + }, + }, + }, + }, + }) + if err != nil { + return nil, errors.Wrapf(err, "Unable to render tls context") + } + + return &coreAPI.TransportSocket{ + Name: "envoy.transport_sockets.tls", + ConfigType: &coreAPI.TransportSocket_TypedConfig{ + TypedConfig: tlsContext, + }, + }, nil +} diff --git a/pkg/deployment/resources/gateway_config.go b/pkg/deployment/resources/gateway_config.go deleted file mode 100644 index 38408e4ea..000000000 --- a/pkg/deployment/resources/gateway_config.go +++ /dev/null @@ -1,268 +0,0 @@ -// -// DISCLAIMER -// -// Copyright 2024 ArangoDB GmbH, Cologne, Germany -// -// 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. -// -// Copyright holder is ArangoDB GmbH, Cologne, Germany -// - -package resources - -import ( - "fmt" - "net/url" - "strconv" - "time" - - bootstrapAPI "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3" - clusterAPI "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" - coreAPI "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" - endpointAPI "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" - listenerAPI "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" - routeAPI "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" - routerAPI "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3" - httpConnectionManagerAPI "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/types/known/anypb" - "google.golang.org/protobuf/types/known/durationpb" - "sigs.k8s.io/yaml" - - shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" - "github.com/arangodb/kube-arangodb/pkg/util" -) - -type Redirect util.KV[string, []string] - -func WithRedirect(prefix string, target ...string) Redirect { - return Redirect{ - K: prefix, - V: target, - } -} - -func RenderGatewayConfigYAML(dbServiceAddress string, redirects ...Redirect) ([]byte, error) { - cfg, err := RenderConfig(dbServiceAddress, redirects...) - if err != nil { - return nil, err - } - - data, err := protojson.MarshalOptions{ - UseProtoNames: true, - }.Marshal(cfg) - if err != nil { - return nil, err - } - - data, err = yaml.JSONToYAML(data) - return data, err -} - -func RenderConfig(dbServiceAddress string, redirects ...Redirect) (*bootstrapAPI.Bootstrap, error) { - clusters := []*clusterAPI.Cluster{ - { - Name: "arangodb", - ConnectTimeout: durationpb.New(250 * time.Millisecond), - LbPolicy: clusterAPI.Cluster_ROUND_ROBIN, - LoadAssignment: &endpointAPI.ClusterLoadAssignment{ - ClusterName: "arangodb", - Endpoints: []*endpointAPI.LocalityLbEndpoints{ - { - LbEndpoints: []*endpointAPI.LbEndpoint{ - { - HostIdentifier: &endpointAPI.LbEndpoint_Endpoint{ - Endpoint: &endpointAPI.Endpoint{ - Address: &coreAPI.Address{ - Address: &coreAPI.Address_SocketAddress{ - SocketAddress: &coreAPI.SocketAddress{ - Address: dbServiceAddress, - PortSpecifier: &coreAPI.SocketAddress_PortValue{ - PortValue: shared.ArangoPort, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - - routes := []*routeAPI.Route{ - { - Match: &routeAPI.RouteMatch{ - PathSpecifier: &routeAPI.RouteMatch_Prefix{ - Prefix: "/", - }, - }, - Action: &routeAPI.Route_Route{ - Route: &routeAPI.RouteAction{ - ClusterSpecifier: &routeAPI.RouteAction_Cluster{ - Cluster: "arangodb", - }, - PrefixRewrite: "/", - }, - }, - }, - } - - for id, redirect := range redirects { - var endpoints []*endpointAPI.LbEndpoint - - for _, target := range redirect.V { - req, err := url.Parse(target) - if err != nil { - return nil, err - } - - port, err := strconv.Atoi(req.Port()) - if err != nil { - return nil, err - } - - endpoints = append(endpoints, &endpointAPI.LbEndpoint{ - HostIdentifier: &endpointAPI.LbEndpoint_Endpoint{ - Endpoint: &endpointAPI.Endpoint{ - Address: &coreAPI.Address{ - Address: &coreAPI.Address_SocketAddress{ - SocketAddress: &coreAPI.SocketAddress{ - Address: req.Hostname(), - PortSpecifier: &coreAPI.SocketAddress_PortValue{ - PortValue: uint32(port), - }, - }, - }, - }, - }, - }, - }, - ) - } - - cluster := &clusterAPI.Cluster{ - Name: fmt.Sprintf("cluster_%05d", id), - ConnectTimeout: durationpb.New(250 * time.Millisecond), - LbPolicy: clusterAPI.Cluster_ROUND_ROBIN, - LoadAssignment: &endpointAPI.ClusterLoadAssignment{ - ClusterName: fmt.Sprintf("cluster_%05d", id), - Endpoints: []*endpointAPI.LocalityLbEndpoints{ - { - LbEndpoints: endpoints, - }, - }, - }, - } - - route := &routeAPI.Route{ - Match: &routeAPI.RouteMatch{ - PathSpecifier: &routeAPI.RouteMatch_Prefix{ - Prefix: redirect.K, - }, - }, - Action: &routeAPI.Route_Route{ - Route: &routeAPI.RouteAction{ - ClusterSpecifier: &routeAPI.RouteAction_Cluster{ - Cluster: fmt.Sprintf("cluster_%05d", id), - }, - PrefixRewrite: "/", - }, - }, - } - - clusters = append(clusters, cluster) - routes = append(routes, route) - } - - routes = util.Sort(routes, func(i, j *routeAPI.Route) bool { - return i.Match.GetPrefix() > j.Match.GetPrefix() - }) - - httpFilterConfigType, err := anypb.New(&routerAPI.Router{}) - if err != nil { - return nil, err - } - - filterConfigType, err := anypb.New(&httpConnectionManagerAPI.HttpConnectionManager{ - StatPrefix: "ingress_http", - CodecType: httpConnectionManagerAPI.HttpConnectionManager_AUTO, - RouteSpecifier: &httpConnectionManagerAPI.HttpConnectionManager_RouteConfig{ - RouteConfig: &routeAPI.RouteConfiguration{ - Name: "local_route", - VirtualHosts: []*routeAPI.VirtualHost{ - { - Name: "local_service", - Domains: []string{"*"}, - Routes: routes, - }, - }, - }, - }, - HttpFilters: []*httpConnectionManagerAPI.HttpFilter{ - { - Name: "envoy.filters.http.routerAPI", - ConfigType: &httpConnectionManagerAPI.HttpFilter_TypedConfig{ - TypedConfig: httpFilterConfigType, - }, - }, - }, - }) - if err != nil { - return nil, err - } - - return &bootstrapAPI.Bootstrap{ - Admin: &bootstrapAPI.Admin{ - Address: &coreAPI.Address{ - Address: &coreAPI.Address_SocketAddress{ - SocketAddress: &coreAPI.SocketAddress{ - Address: "127.0.0.1", - PortSpecifier: &coreAPI.SocketAddress_PortValue{PortValue: 9901}, - }, - }, - }, - }, - StaticResources: &bootstrapAPI.Bootstrap_StaticResources{ - Listeners: []*listenerAPI.Listener{ - { - Name: "listener_0", - Address: &coreAPI.Address{ - Address: &coreAPI.Address_SocketAddress{ - SocketAddress: &coreAPI.SocketAddress{ - Address: "0.0.0.0", - PortSpecifier: &coreAPI.SocketAddress_PortValue{PortValue: shared.ArangoPort}, - }, - }, - }, - FilterChains: []*listenerAPI.FilterChain{ - { - Filters: []*listenerAPI.Filter{ - { - Name: "envoy.filters.network.httpConnectionManagerAPI", - ConfigType: &listenerAPI.Filter_TypedConfig{ - TypedConfig: filterConfigType, - }, - }, - }, - }, - }, - }, - }, - Clusters: clusters, - }, - }, nil -} diff --git a/pkg/deployment/resources/pod_creator.go b/pkg/deployment/resources/pod_creator.go index a55157c5b..d70c91874 100644 --- a/pkg/deployment/resources/pod_creator.go +++ b/pkg/deployment/resources/pod_creator.go @@ -286,10 +286,15 @@ func createArangoSyncArgs(apiObject meta.Object, spec api.DeploymentSpec, group return args } -func createArangoGatewayArgs(groupSpec api.ServerGroupSpec) []string { - args := []string{"--config-path", GatewayConfigFilePath} - if len(groupSpec.Args) > 0 { - args = append(args, groupSpec.Args...) +func createArangoGatewayArgs(input pod.Input, additionalOptions ...k8sutil.OptionPair) []string { + options := k8sutil.CreateOptionPairs(64) + options.Add("--config-path", GatewayConfigFilePath) + + options.Append(additionalOptions...) + + args := options.Sort().AsSplittedArgs() + if len(input.GroupSpec.Args) > 0 { + args = append(args, input.GroupSpec.Args...) } return args @@ -297,7 +302,7 @@ func createArangoGatewayArgs(groupSpec api.ServerGroupSpec) []string { // CreatePodTolerations creates a list of tolerations for a pod created for the given group. func (r *Resources) CreatePodTolerations(group api.ServerGroup, groupSpec api.ServerGroupSpec) []core.Toleration { - return tolerations.MergeTolerationsIfNotFound(tolerations.CreatePodTolerations(r.context.GetMode(), group), groupSpec.GetTolerations()) + return tolerations.MergeTolerationsIfNotFound(CreatePodTolerations(r.context.GetMode(), group), groupSpec.GetTolerations()) } func (r *Resources) RenderPodTemplateForMember(ctx context.Context, acs sutil.ACS, spec api.DeploymentSpec, status api.DeploymentStatus, memberID string, imageInfo api.ImageInfo) (*core.PodTemplateSpec, error) { @@ -395,16 +400,17 @@ func (r *Resources) RenderPodForMember(ctx context.Context, acs sutil.ACS, spec } podCreator = &MemberGatewayPod{ - podName: podName, - groupSpec: groupSpec, - spec: spec, - group: group, - resources: r, - imageInfo: imageInfo, - arangoMember: *member, - apiObject: apiObject, - memberStatus: m, - cachedStatus: cache, + podName: podName, + status: m, + groupSpec: groupSpec, + spec: spec, + group: group, + resources: r, + imageInfo: imageInfo, + context: r.context, + deploymentStatus: status, + arangoMember: *member, + cachedStatus: cache, } default: return nil, assertion.InvalidGroupKey.Assert(true, "Unable to render pod for an unknown group: %s", group.AsRole()) @@ -687,7 +693,18 @@ func RenderArangoPod(ctx context.Context, cachedStatus inspectorInterface.Inspec PodAffinity: podCreator.GetPodAffinity(), } - return &p, nil + if profiles, err := podCreator.Profiles(); err != nil { + return nil, err + } else if len(profiles) > 0 { + if err := profiles.RenderOnTemplate(&p); err != nil { + return nil, err + } + } + + return &core.Pod{ + ObjectMeta: p.ObjectMeta, + Spec: p.Spec, + }, nil } // CreateArangoPod creates a new Pod with container provided by parameter 'containerCreator' diff --git a/pkg/deployment/resources/pod_creator_arangod.go b/pkg/deployment/resources/pod_creator_arangod.go index 092a53d14..8d5bf4b58 100644 --- a/pkg/deployment/resources/pod_creator_arangod.go +++ b/pkg/deployment/resources/pod_creator_arangod.go @@ -30,6 +30,7 @@ import ( core "k8s.io/api/core/v1" api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + schedulerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1" shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" "github.com/arangodb/kube-arangodb/pkg/deployment/features" "github.com/arangodb/kube-arangodb/pkg/deployment/pod" @@ -321,7 +322,7 @@ func (m *MemberArangoDPod) AsInput() pod.Input { } } -func (m *MemberArangoDPod) Init(_ context.Context, _ interfaces.Inspector, pod *core.Pod) error { +func (m *MemberArangoDPod) Init(_ context.Context, _ interfaces.Inspector, pod *core.PodTemplateSpec) error { terminationGracePeriodSeconds := int64(math.Ceil(m.groupSpec.GetTerminationGracePeriod(m.group).Seconds())) pod.Spec.TerminationGracePeriodSeconds = &terminationGracePeriodSeconds pod.Spec.PriorityClassName = m.groupSpec.PriorityClassName @@ -409,7 +410,7 @@ func (m *MemberArangoDPod) GetServiceAccountName() string { return m.groupSpec.GetServiceAccountName() } -func (m *MemberArangoDPod) GetSidecars(pod *core.Pod) error { +func (m *MemberArangoDPod) GetSidecars(pod *core.PodTemplateSpec) error { //nolint:staticcheck if m.spec.Metrics.IsEnabled() && m.spec.Metrics.Mode.Get() != api.MetricsModeInternal { var c *core.Container @@ -595,6 +596,10 @@ func (m *MemberArangoDPod) Annotations() map[string]string { return collection.MergeAnnotations(m.spec.Annotations, m.groupSpec.Annotations) } +func (m *MemberArangoDPod) Profiles() (schedulerApi.ProfileTemplates, error) { + return nil, nil +} + func (m *MemberArangoDPod) Labels() map[string]string { l := collection.ReservedLabels().Filter(collection.MergeAnnotations(m.spec.Labels, m.groupSpec.Labels)) diff --git a/pkg/deployment/resources/pod_creator_gateway.go b/pkg/deployment/resources/pod_creator_gateway.go index 30988305c..993e6f820 100644 --- a/pkg/deployment/resources/pod_creator_gateway.go +++ b/pkg/deployment/resources/pod_creator_gateway.go @@ -21,320 +21,35 @@ package resources import ( - "context" "fmt" - "math" core "k8s.io/api/core/v1" - meta "k8s.io/apimachinery/pkg/apis/meta/v1" - api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" - shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" - "github.com/arangodb/kube-arangodb/pkg/deployment/features" "github.com/arangodb/kube-arangodb/pkg/deployment/pod" - "github.com/arangodb/kube-arangodb/pkg/util/collection" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" - "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/interfaces" - kresources "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/resources" ) const ( - ArangoGatewayExecutor string = "/usr/local/bin/envoy" - GatewayVolumeMountDir = "/etc/gateway/" - GatewayVolumeName = "gateway" - GatewayConfigFileName = "gateway.yaml" - GatewayConfigChecksumName = "gateway.yaml-checksum" - GatewayConfigFilePath = GatewayVolumeMountDir + GatewayConfigFileName + ArangoGatewayExecutor string = "/usr/local/bin/envoy" + GatewayVolumeMountDir = "/etc/gateway/" + GatewayVolumeName = "gateway" + GatewayConfigFileName = "gateway.yaml" + GatewayConfigFilePath = GatewayVolumeMountDir + GatewayConfigFileName ) -type ArangoGatewayContainer struct { - groupSpec api.ServerGroupSpec - spec api.DeploymentSpec - group api.ServerGroup - resources *Resources - imageInfo api.ImageInfo - apiObject meta.Object - memberStatus api.MemberStatus - arangoMember api.ArangoMember -} - -var _ interfaces.PodCreator = &MemberGatewayPod{} -var _ interfaces.ContainerCreator = &ArangoGatewayContainer{} - -type MemberGatewayPod struct { - podName string - - groupSpec api.ServerGroupSpec - spec api.DeploymentSpec - group api.ServerGroup - arangoMember api.ArangoMember - resources *Resources - imageInfo api.ImageInfo - apiObject meta.Object - memberStatus api.MemberStatus - cachedStatus interfaces.Inspector -} - func GetGatewayConfigMapName(name string) string { return fmt.Sprintf("%s-gateway", name) } -func (a *ArangoGatewayContainer) GetCommand() ([]string, error) { - cmd := make([]string, 0, 128) - cmd = append(cmd, a.GetExecutor()) - cmd = append(cmd, createArangoGatewayArgs(a.groupSpec)...) - return cmd, nil -} - -func (a *ArangoGatewayContainer) GetName() string { - return shared.ServerContainerName -} - -func (a *ArangoGatewayContainer) GetPorts() []core.ContainerPort { - port := shared.ArangoPort - - return []core.ContainerPort{ - { - Name: shared.ServerContainerName, - ContainerPort: int32(port), - Protocol: core.ProtocolTCP, - }, - } -} - -func (a *ArangoGatewayContainer) GetExecutor() string { - return a.groupSpec.GetEntrypoint(ArangoGatewayExecutor) -} - -func (a *ArangoGatewayContainer) GetSecurityContext() *core.SecurityContext { - return k8sutil.CreateSecurityContext(a.groupSpec.SecurityContext) -} - -func (a *ArangoGatewayContainer) GetProbes() (*core.Probe, *core.Probe, *core.Probe, error) { - var liveness, readiness, startup *core.Probe - - probeLivenessConfig, err := a.resources.getLivenessProbe(a.spec, a.group, a.imageInfo) - if err != nil { - return nil, nil, nil, err - } - - probeReadinessConfig, err := a.resources.getReadinessProbe(a.spec, a.group, a.imageInfo) - if err != nil { - return nil, nil, nil, err - } - - probeStartupConfig, err := a.resources.getReadinessProbe(a.spec, a.group, a.imageInfo) - if err != nil { - return nil, nil, nil, err - } - - if probeLivenessConfig != nil { - liveness = probeLivenessConfig.Create() - } - - if probeReadinessConfig != nil { - readiness = probeReadinessConfig.Create() - } - - if probeStartupConfig != nil { - startup = probeStartupConfig.Create() - } - - return liveness, readiness, startup, nil -} - -func (a *ArangoGatewayContainer) GetResourceRequirements() core.ResourceRequirements { - return kresources.ExtractPodAcceptedResourceRequirement(a.arangoMember.Spec.Overrides.GetResources(&a.groupSpec)) -} - -func (a *ArangoGatewayContainer) GetLifecycle() (*core.Lifecycle, error) { - return k8sutil.NewLifecycleFinalizers() -} - -func (a *ArangoGatewayContainer) GetImagePullPolicy() core.PullPolicy { - return a.spec.GetImagePullPolicy() -} - -func (a *ArangoGatewayContainer) GetImage() string { - return a.imageInfo.Image -} - -func (a *ArangoGatewayContainer) GetEnvs() ([]core.EnvVar, []core.EnvFromSource) { - envs := NewEnvBuilder() - - envs.Add(true, k8sutil.GetLifecycleEnv()...) - - if len(a.groupSpec.Envs) > 0 { - for _, env := range a.groupSpec.Envs { - // Do not override preset envs - envs.Add(false, core.EnvVar{ - Name: env.Name, - Value: env.Value, - }) - } - } - - return envs.GetEnvList(), nil -} - -func (a *ArangoGatewayContainer) GetVolumeMounts() []core.VolumeMount { - return createGatewayVolumes(a.apiObject.GetName()).VolumeMounts() -} - -func (m *MemberGatewayPod) GetName() string { - return m.resources.context.GetAPIObject().GetName() -} - -func (m *MemberGatewayPod) GetRole() string { - return m.group.AsRole() -} - -func (m *MemberGatewayPod) GetImagePullSecrets() []string { - return m.spec.ImagePullSecrets -} - -func (m *MemberGatewayPod) GetPodAntiAffinity() *core.PodAntiAffinity { - a := &core.PodAntiAffinity{} - - pod.AppendPodAntiAffinityDefault(m, a) - - a = kresources.MergePodAntiAffinity(a, m.groupSpec.AntiAffinity) - - return kresources.OptionalPodAntiAffinity(a) -} - -func (m *MemberGatewayPod) GetPodAffinity() *core.PodAffinity { - a := &core.PodAffinity{} - - pod.AppendAffinityWithRole(m, a, api.ServerGroupDBServers.AsRole()) - - a = kresources.MergePodAffinity(a, m.groupSpec.Affinity) - - return kresources.OptionalPodAffinity(a) -} - -func (m *MemberGatewayPod) GetNodeAffinity() *core.NodeAffinity { - a := &core.NodeAffinity{} - - pod.AppendArchSelector(a, m.memberStatus.Architecture.Default(m.spec.Architecture.GetDefault()).AsNodeSelectorRequirement()) - - a = kresources.MergeNodeAffinity(a, m.groupSpec.NodeAffinity) - - return kresources.OptionalNodeAffinity(a) -} - -func (m *MemberGatewayPod) GetNodeSelector() map[string]string { - return m.groupSpec.GetNodeSelector() -} - -func (m *MemberGatewayPod) GetServiceAccountName() string { - return m.groupSpec.GetServiceAccountName() -} - -func (m *MemberGatewayPod) GetSidecars(pod *core.Pod) error { - // A sidecar provided by the user - sidecars := m.groupSpec.GetSidecars() - if len(sidecars) > 0 { - addLifecycleSidecar(m.groupSpec.SidecarCoreNames, sidecars) - pod.Spec.Containers = append(pod.Spec.Containers, sidecars...) - } - - return nil -} - -func (m *MemberGatewayPod) GetVolumes() []core.Volume { - return createGatewayVolumes(m.apiObject.GetName()).Volumes() -} - -func (m *MemberGatewayPod) IsDeploymentMode() bool { - return m.spec.IsDevelopment() -} - -func (m *MemberGatewayPod) GetInitContainers(cachedStatus interfaces.Inspector) ([]core.Container, error) { - var initContainers []core.Container - if c := m.groupSpec.InitContainers.GetContainers(); len(c) > 0 { - initContainers = append(initContainers, c...) - } - - res := kresources.ExtractPodInitContainerAcceptedResourceRequirement(m.GetContainerCreator().GetResourceRequirements()) - - initContainers = applyInitContainersResourceResources(initContainers, res) - initContainers = upscaleInitContainersResourceResources(initContainers, res) - - return initContainers, nil -} - -func (m *MemberGatewayPod) GetFinalizers() []string { - return nil -} - -func (m *MemberGatewayPod) GetTolerations() []core.Toleration { - return m.resources.CreatePodTolerations(m.group, m.groupSpec) -} - -func (m *MemberGatewayPod) GetContainerCreator() interfaces.ContainerCreator { - return &ArangoGatewayContainer{ - groupSpec: m.groupSpec, - spec: m.spec, - group: m.group, - resources: m.resources, - imageInfo: m.imageInfo, - apiObject: m.apiObject, - memberStatus: m.memberStatus, - arangoMember: m.arangoMember, - } -} - -func (m *MemberGatewayPod) GetRestartPolicy() core.RestartPolicy { - if features.RestartPolicyAlways().Enabled() { - return core.RestartPolicyAlways - } - return core.RestartPolicyNever -} - -func (m *MemberGatewayPod) Init(ctx context.Context, cachedStatus interfaces.Inspector, pod *core.Pod) error { - terminationGracePeriodSeconds := int64(math.Ceil(m.groupSpec.GetTerminationGracePeriod(m.group).Seconds())) - pod.Spec.TerminationGracePeriodSeconds = &terminationGracePeriodSeconds - pod.Spec.PriorityClassName = m.groupSpec.PriorityClassName - - return nil -} - -func (m *MemberGatewayPod) Validate(_ interfaces.Inspector) error { - if err := validateSidecars(m.groupSpec.SidecarCoreNames, m.groupSpec.GetSidecars()); err != nil { - return err - } - - return nil -} - -func (m *MemberGatewayPod) ApplyPodSpec(spec *core.PodSpec) error { - if s := m.groupSpec.SchedulerName; s != nil { - spec.SchedulerName = *s - } - - m.groupSpec.PodModes.Apply(spec) - - return nil -} - -func (m *MemberGatewayPod) Annotations() map[string]string { - return collection.MergeAnnotations(m.spec.Annotations, m.groupSpec.Annotations) -} - -func (m *MemberGatewayPod) Labels() map[string]string { - return collection.ReservedLabels().Filter(collection.MergeAnnotations(m.spec.Labels, m.groupSpec.Labels)) -} - -func createGatewayVolumes(memberName string) pod.Volumes { +func createGatewayVolumes(input pod.Input) pod.Volumes { volumes := pod.NewVolumes() - volumes.AddVolume(k8sutil.LifecycleVolume()) - volumes.AddVolumeMount(k8sutil.LifecycleVolumeMount()) - - volumes.AddVolume(k8sutil.CreateVolumeWithConfigMap(GatewayVolumeName, GetGatewayConfigMapName(memberName))) + volumes.AddVolume(k8sutil.CreateVolumeWithConfigMap(GatewayVolumeName, GetGatewayConfigMapName(input.ApiObject.GetName()))) volumes.AddVolumeMount(GatewayVolumeMount()) + // TLS + volumes.Append(pod.TLS(), input) + return volumes } diff --git a/pkg/deployment/resources/pod_creator_gateway_container.go b/pkg/deployment/resources/pod_creator_gateway_container.go new file mode 100644 index 000000000..6510b4e72 --- /dev/null +++ b/pkg/deployment/resources/pod_creator_gateway_container.go @@ -0,0 +1,149 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package resources + +import ( + core "k8s.io/api/core/v1" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" + "github.com/arangodb/kube-arangodb/pkg/deployment/pod" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/interfaces" + kresources "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/resources" +) + +var _ interfaces.ContainerCreator = &ArangoGatewayContainer{} + +type ArangoGatewayContainer struct { + member *MemberGatewayPod + resources *Resources + groupSpec api.ServerGroupSpec + spec api.DeploymentSpec + group api.ServerGroup + arangoMember api.ArangoMember + imageInfo api.ImageInfo + cachedStatus interfaces.Inspector + input pod.Input + status api.MemberStatus +} + +func (a *ArangoGatewayContainer) GetCommand() ([]string, error) { + cmd := make([]string, 0, 128) + cmd = append(cmd, a.GetExecutor()) + cmd = append(cmd, createArangoGatewayArgs(a.input)...) + return cmd, nil +} + +func (a *ArangoGatewayContainer) GetName() string { + return shared.ServerContainerName +} + +func (a *ArangoGatewayContainer) GetPorts() []core.ContainerPort { + port := shared.ArangoPort + + return []core.ContainerPort{ + { + Name: shared.ServerContainerName, + ContainerPort: int32(port), + Protocol: core.ProtocolTCP, + }, + } +} + +func (a *ArangoGatewayContainer) GetExecutor() string { + return a.groupSpec.GetEntrypoint(ArangoGatewayExecutor) +} + +func (a *ArangoGatewayContainer) GetSecurityContext() *core.SecurityContext { + return k8sutil.CreateSecurityContext(a.groupSpec.SecurityContext) +} + +func (a *ArangoGatewayContainer) GetProbes() (*core.Probe, *core.Probe, *core.Probe, error) { + var liveness, readiness, startup *core.Probe + + probeLivenessConfig, err := a.resources.getLivenessProbe(a.spec, a.group, a.imageInfo) + if err != nil { + return nil, nil, nil, err + } + + probeReadinessConfig, err := a.resources.getReadinessProbe(a.spec, a.group, a.imageInfo) + if err != nil { + return nil, nil, nil, err + } + + probeStartupConfig, err := a.resources.getReadinessProbe(a.spec, a.group, a.imageInfo) + if err != nil { + return nil, nil, nil, err + } + + if probeLivenessConfig != nil { + liveness = probeLivenessConfig.Create() + } + + if probeReadinessConfig != nil { + readiness = probeReadinessConfig.Create() + } + + if probeStartupConfig != nil { + startup = probeStartupConfig.Create() + } + + return liveness, readiness, startup, nil +} + +func (a *ArangoGatewayContainer) GetResourceRequirements() core.ResourceRequirements { + return kresources.ExtractPodAcceptedResourceRequirement(a.arangoMember.Spec.Overrides.GetResources(&a.groupSpec)) +} + +func (a *ArangoGatewayContainer) GetLifecycle() (*core.Lifecycle, error) { + return k8sutil.NewLifecycleFinalizers() +} + +func (a *ArangoGatewayContainer) GetImagePullPolicy() core.PullPolicy { + return a.spec.GetImagePullPolicy() +} + +func (a *ArangoGatewayContainer) GetImage() string { + return a.imageInfo.Image +} + +func (a *ArangoGatewayContainer) GetEnvs() ([]core.EnvVar, []core.EnvFromSource) { + envs := NewEnvBuilder() + + envs.Add(true, k8sutil.GetLifecycleEnv()...) + + if len(a.groupSpec.Envs) > 0 { + for _, env := range a.groupSpec.Envs { + // Do not override preset envs + envs.Add(false, core.EnvVar{ + Name: env.Name, + Value: env.Value, + }) + } + } + + return envs.GetEnvList(), nil +} + +func (a *ArangoGatewayContainer) GetVolumeMounts() []core.VolumeMount { + return createGatewayVolumes(a.input).VolumeMounts() +} diff --git a/pkg/deployment/resources/pod_creator_gateway_pod.go b/pkg/deployment/resources/pod_creator_gateway_pod.go new file mode 100644 index 000000000..75daaaf92 --- /dev/null +++ b/pkg/deployment/resources/pod_creator_gateway_pod.go @@ -0,0 +1,242 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package resources + +import ( + "context" + "fmt" + "math" + + core "k8s.io/api/core/v1" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + schedulerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1" + schedulerContainerResourcesApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1/container/resources" + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/arangodb/kube-arangodb/pkg/deployment/pod" + "github.com/arangodb/kube-arangodb/pkg/integrations/sidecar" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/collection" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/interfaces" + kresources "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/resources" +) + +var _ interfaces.PodCreator = &MemberGatewayPod{} + +type MemberGatewayPod struct { + podName string + status api.MemberStatus + groupSpec api.ServerGroupSpec + spec api.DeploymentSpec + deploymentStatus api.DeploymentStatus + group api.ServerGroup + arangoMember api.ArangoMember + context Context + resources *Resources + imageInfo api.ImageInfo + cachedStatus interfaces.Inspector +} + +func (m *MemberGatewayPod) GetName() string { + return m.resources.context.GetAPIObject().GetName() +} + +func (m *MemberGatewayPod) GetRole() string { + return m.group.AsRole() +} + +func (m *MemberGatewayPod) GetImagePullSecrets() []string { + return m.spec.ImagePullSecrets +} + +func (m *MemberGatewayPod) GetPodAntiAffinity() *core.PodAntiAffinity { + a := &core.PodAntiAffinity{} + + pod.AppendPodAntiAffinityDefault(m, a) + + a = kresources.MergePodAntiAffinity(a, m.groupSpec.AntiAffinity) + + return kresources.OptionalPodAntiAffinity(a) +} + +func (m *MemberGatewayPod) AsInput() pod.Input { + return pod.Input{ + ApiObject: m.context.GetAPIObject(), + Deployment: m.spec, + Status: m.deploymentStatus, + Group: m.group, + GroupSpec: m.groupSpec, + Version: m.imageInfo.ArangoDBVersion, + Enterprise: m.imageInfo.Enterprise, + Member: m.status, + ArangoMember: m.arangoMember, + } +} + +func (m *MemberGatewayPod) GetPodAffinity() *core.PodAffinity { + a := &core.PodAffinity{} + + pod.AppendAffinityWithRole(m, a, api.ServerGroupDBServers.AsRole()) + + a = kresources.MergePodAffinity(a, m.groupSpec.Affinity) + + return kresources.OptionalPodAffinity(a) +} + +func (m *MemberGatewayPod) GetNodeAffinity() *core.NodeAffinity { + a := &core.NodeAffinity{} + + pod.AppendArchSelector(a, m.status.Architecture.Default(m.spec.Architecture.GetDefault()).AsNodeSelectorRequirement()) + + a = kresources.MergeNodeAffinity(a, m.groupSpec.NodeAffinity) + + return kresources.OptionalNodeAffinity(a) +} + +func (m *MemberGatewayPod) GetNodeSelector() map[string]string { + return m.groupSpec.GetNodeSelector() +} + +func (m *MemberGatewayPod) GetServiceAccountName() string { + return m.groupSpec.GetServiceAccountName() +} + +func (m *MemberGatewayPod) GetSidecars(pod *core.PodTemplateSpec) error { + // A sidecar provided by the user + sidecars := m.groupSpec.GetSidecars() + if len(sidecars) > 0 { + addLifecycleSidecar(m.groupSpec.SidecarCoreNames, sidecars) + pod.Spec.Containers = append(pod.Spec.Containers, sidecars...) + } + + return nil +} + +func (m *MemberGatewayPod) GetVolumes() []core.Volume { + return createGatewayVolumes(m.AsInput()).Volumes() +} + +func (m *MemberGatewayPod) IsDeploymentMode() bool { + return m.spec.IsDevelopment() +} + +func (m *MemberGatewayPod) GetInitContainers(cachedStatus interfaces.Inspector) ([]core.Container, error) { + var initContainers []core.Container + if c := m.groupSpec.InitContainers.GetContainers(); len(c) > 0 { + initContainers = append(initContainers, c...) + } + + res := kresources.ExtractPodInitContainerAcceptedResourceRequirement(m.GetContainerCreator().GetResourceRequirements()) + + initContainers = applyInitContainersResourceResources(initContainers, res) + initContainers = upscaleInitContainersResourceResources(initContainers, res) + + return initContainers, nil +} + +func (m *MemberGatewayPod) GetFinalizers() []string { + return nil +} + +func (m *MemberGatewayPod) GetTolerations() []core.Toleration { + return m.resources.CreatePodTolerations(m.group, m.groupSpec) +} + +func (m *MemberGatewayPod) GetContainerCreator() interfaces.ContainerCreator { + return &ArangoGatewayContainer{ + member: m, + spec: m.spec, + group: m.group, + resources: m.resources, + imageInfo: m.imageInfo, + groupSpec: m.groupSpec, + arangoMember: m.arangoMember, + cachedStatus: m.cachedStatus, + input: m.AsInput(), + status: m.status, + } +} + +func (m *MemberGatewayPod) GetRestartPolicy() core.RestartPolicy { + if features.RestartPolicyAlways().Enabled() { + return core.RestartPolicyAlways + } + return core.RestartPolicyNever +} + +func (m *MemberGatewayPod) Init(ctx context.Context, cachedStatus interfaces.Inspector, pod *core.PodTemplateSpec) error { + terminationGracePeriodSeconds := int64(math.Ceil(m.groupSpec.GetTerminationGracePeriod(m.group).Seconds())) + pod.Spec.TerminationGracePeriodSeconds = &terminationGracePeriodSeconds + pod.Spec.PriorityClassName = m.groupSpec.PriorityClassName + + return nil +} + +func (m *MemberGatewayPod) Validate(_ interfaces.Inspector) error { + if err := validateSidecars(m.groupSpec.SidecarCoreNames, m.groupSpec.GetSidecars()); err != nil { + return err + } + + return nil +} + +func (m *MemberGatewayPod) ApplyPodSpec(spec *core.PodSpec) error { + if s := m.groupSpec.SchedulerName; s != nil { + spec.SchedulerName = *s + } + + m.groupSpec.PodModes.Apply(spec) + + return nil +} + +func (m *MemberGatewayPod) Annotations() map[string]string { + return collection.MergeAnnotations(m.spec.Annotations, m.groupSpec.Annotations) +} + +func (m *MemberGatewayPod) Labels() map[string]string { + l := collection.ReservedLabels().Filter(collection.MergeAnnotations(m.spec.Labels, m.groupSpec.Labels)) + + if m.status.Topology != nil && m.deploymentStatus.Topology.Enabled() && m.deploymentStatus.Topology.ID == m.status.Topology.ID { + if l == nil { + l = map[string]string{} + } + + l[k8sutil.LabelKeyArangoZone] = fmt.Sprintf("%d", m.status.Topology.Zone) + l[k8sutil.LabelKeyArangoTopology] = string(m.status.Topology.ID) + } + + return l +} + +func (m *MemberGatewayPod) Profiles() (schedulerApi.ProfileTemplates, error) { + integration, err := sidecar.NewIntegration(&schedulerContainerResourcesApi.Image{ + Image: util.NewType(m.resources.context.GetOperatorImage()), + }, m.spec.Gateway.GetSidecar(), []string{shared.ServerContainerName}) + + if err != nil { + return nil, err + } + + return []*schedulerApi.ProfileTemplate{integration}, nil +} diff --git a/pkg/deployment/resources/pod_creator_sync.go b/pkg/deployment/resources/pod_creator_sync.go index eeeecb137..fa7b5bb44 100644 --- a/pkg/deployment/resources/pod_creator_sync.go +++ b/pkg/deployment/resources/pod_creator_sync.go @@ -32,6 +32,7 @@ import ( meta "k8s.io/apimachinery/pkg/apis/meta/v1" api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + schedulerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1" shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" "github.com/arangodb/kube-arangodb/pkg/deployment/features" "github.com/arangodb/kube-arangodb/pkg/deployment/pod" @@ -266,7 +267,7 @@ func (m *MemberSyncPod) GetServiceAccountName() string { return m.groupSpec.GetServiceAccountName() } -func (m *MemberSyncPod) GetSidecars(pod *core.Pod) error { +func (m *MemberSyncPod) GetSidecars(pod *core.PodTemplateSpec) error { // A sidecar provided by the user sidecars := m.groupSpec.GetSidecars() if len(sidecars) > 0 { @@ -350,7 +351,7 @@ func (m *MemberSyncPod) GetRestartPolicy() core.RestartPolicy { } // Init initializes the arangosync pod. -func (m *MemberSyncPod) Init(ctx context.Context, cachedStatus interfaces.Inspector, pod *core.Pod) error { +func (m *MemberSyncPod) Init(ctx context.Context, cachedStatus interfaces.Inspector, pod *core.PodTemplateSpec) error { terminationGracePeriodSeconds := int64(math.Ceil(m.groupSpec.GetTerminationGracePeriod(m.group).Seconds())) pod.Spec.TerminationGracePeriodSeconds = &terminationGracePeriodSeconds pod.Spec.PriorityClassName = m.groupSpec.PriorityClassName @@ -517,3 +518,7 @@ func (m *MemberSyncPod) syncHostAlias() *core.HostAlias { return &alias } + +func (m *MemberSyncPod) Profiles() (schedulerApi.ProfileTemplates, error) { + return nil, nil +} diff --git a/pkg/deployment/resources/pod_creator_tolerations.go b/pkg/deployment/resources/pod_creator_tolerations.go new file mode 100644 index 000000000..422d1af78 --- /dev/null +++ b/pkg/deployment/resources/pod_creator_tolerations.go @@ -0,0 +1,68 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package resources + +import ( + "time" + + core "k8s.io/api/core/v1" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/tolerations" +) + +// CreatePodTolerations creates a list of tolerations for a pod created for the given group. +func CreatePodTolerations(mode api.DeploymentMode, group api.ServerGroup) []core.Toleration { + notReadyDur := tolerations.TolerationDuration{Forever: false, TimeSpan: time.Minute} + unreachableDur := tolerations.TolerationDuration{Forever: false, TimeSpan: time.Minute} + switch group { + case api.ServerGroupAgents: + notReadyDur.Forever = true + unreachableDur.Forever = true + case api.ServerGroupCoordinators: + notReadyDur.TimeSpan = 15 * time.Second + unreachableDur.TimeSpan = 15 * time.Second + case api.ServerGroupDBServers: + notReadyDur.TimeSpan = 5 * time.Minute + unreachableDur.TimeSpan = 5 * time.Minute + case api.ServerGroupSingle: + if mode == api.DeploymentModeSingle { + notReadyDur.Forever = true + unreachableDur.Forever = true + } else { + notReadyDur.TimeSpan = 5 * time.Minute + unreachableDur.TimeSpan = 5 * time.Minute + } + case api.ServerGroupSyncMasters: + notReadyDur.TimeSpan = 15 * time.Second + unreachableDur.TimeSpan = 15 * time.Second + case api.ServerGroupSyncWorkers: + notReadyDur.TimeSpan = 1 * time.Minute + unreachableDur.TimeSpan = 1 * time.Minute + case api.ServerGroupGateways: + notReadyDur.TimeSpan = 15 * time.Second + unreachableDur.TimeSpan = 15 * time.Second + } + return []core.Toleration{tolerations.NewNoExecuteToleration(tolerations.TolerationKeyNodeNotReady, notReadyDur), + tolerations.NewNoExecuteToleration(tolerations.TolerationKeyNodeUnreachable, unreachableDur), + tolerations.NewNoExecuteToleration(tolerations.TolerationKeyNodeAlphaUnreachable, unreachableDur), + } +} diff --git a/pkg/deployment/resources/pod_leader.go b/pkg/deployment/resources/pod_leader.go index 6be14bd8a..e72120d28 100644 --- a/pkg/deployment/resources/pod_leader.go +++ b/pkg/deployment/resources/pod_leader.go @@ -125,7 +125,7 @@ func (r *Resources) EnsureLeader(ctx context.Context, cachedStatus inspectorInte selector := k8sutil.LabelsForLeaderMember(deploymentName, group.AsRole(), leaderID) if s, ok := cachedStatus.Service().V1().GetSimple(leaderAgentSvcName); ok { - if c, err := patcher.ServicePatcher(ctx, cachedStatus.ServicesModInterface().V1(), s, meta.PatchOptions{}, patcher.PatchServiceSelector(selector), patcher.PatchServicePorts(ports)); err != nil { + if _, c, err := patcher.Patcher[*core.Service](ctx, cachedStatus.ServicesModInterface().V1(), s, meta.PatchOptions{}, patcher.PatchServiceSelector(selector), patcher.PatchServicePorts(ports)); err != nil { return err } else { if !c { diff --git a/pkg/deployment/resources/secrets.go b/pkg/deployment/resources/secrets.go index f79a7f136..5d6af257e 100644 --- a/pkg/deployment/resources/secrets.go +++ b/pkg/deployment/resources/secrets.go @@ -131,7 +131,7 @@ func (r *Resources) EnsureSecrets(ctx context.Context, cachedStatus inspectorInt if err := reconcileRequired.ParallelAll(len(members), func(id int) error { switch members[id].Group.Type() { - case api.ServerGroupTypeArangoD: + case api.ServerGroupTypeArangoD, api.ServerGroupTypeGateway: memberName := members[id].Member.ArangoMemberName(r.context.GetAPIObject().GetName(), members[id].Group) member, ok := cachedStatus.ArangoMember().V1().GetSimple(memberName) diff --git a/pkg/deployment/resources/services.go b/pkg/deployment/resources/services.go index 869e6b488..a040f6f9b 100644 --- a/pkg/deployment/resources/services.go +++ b/pkg/deployment/resources/services.go @@ -120,7 +120,7 @@ func (r *Resources) EnsureServices(ctx context.Context, cachedStatus inspectorIn continue } else { - if changed, err := patcher.ServicePatcher(ctx, svcs, s, meta.PatchOptions{}, + if _, changed, err := patcher.Patcher[*core.Service](ctx, svcs, s, meta.PatchOptions{}, patcher.PatchServicePorts(ports), patcher.PatchServiceSelector(selector), patcher.PatchServicePublishNotReadyAddresses(true), @@ -176,7 +176,7 @@ func (r *Resources) EnsureServices(ctx context.Context, cachedStatus inspectorIn reconcileRequired.Required() continue } else { - if changed, err := patcher.ServicePatcher(ctx, svcs, s, meta.PatchOptions{}, + if _, changed, err := patcher.Patcher[*core.Service](ctx, svcs, s, meta.PatchOptions{}, patcher.PatchServicePorts(ports), patcher.PatchServiceSelector(selector), patcher.PatchServicePublishNotReadyAddresses(false), @@ -205,7 +205,7 @@ func (r *Resources) EnsureServices(ctx context.Context, cachedStatus inspectorIn log.Str("service", svcName).Debug("Created headless service") } } else { - if changed, err := patcher.ServicePatcher(ctx, svcs, s, meta.PatchOptions{}, patcher.PatchServicePorts(headlessPorts), patcher.PatchServiceSelector(headlessSelector)); err != nil { + if _, changed, err := patcher.Patcher[*core.Service](ctx, svcs, s, meta.PatchOptions{}, patcher.PatchServicePorts(headlessPorts), patcher.PatchServiceSelector(headlessSelector)); err != nil { log.Err(err).Debug("Failed to patch headless service") return errors.WithStack(err) } else if changed { @@ -245,7 +245,7 @@ func (r *Resources) EnsureServices(ctx context.Context, cachedStatus inspectorIn } } } else { - if changed, err := patcher.ServicePatcher(ctx, svcs, s, meta.PatchOptions{}, patcher.PatchServiceOnlyPorts(clientServicePorts...), patcher.PatchServiceSelector(clientServiceSelectors)); err != nil { + if _, changed, err := patcher.Patcher[*core.Service](ctx, svcs, s, meta.PatchOptions{}, patcher.PatchServiceOnlyPorts(clientServicePorts...), patcher.PatchServiceSelector(clientServiceSelectors)); err != nil { log.Err(err).Debug("Failed to patch database client service") return errors.WithStack(err) } else if changed { @@ -380,7 +380,7 @@ func (r *Resources) ensureExternalAccessServices(ctx context.Context, cachedStat } } if !createExternalAccessService && !deleteExternalAccessService { - if changed, err := patcher.ServicePatcher(ctx, svcs, existing, meta.PatchOptions{}, + if _, changed, err := patcher.Patcher[*core.Service](ctx, svcs, existing, meta.PatchOptions{}, patcher.PatchServiceSelector(eaSelector), patcher.Optional(patcher.PatchServiceOnlyPorts(eaPorts...), owned)); err != nil { log.Err(err).Debug("Failed to patch database client service") @@ -434,8 +434,8 @@ func (r *Resources) ensureExternalAccessManagedServices(ctx context.Context, cac log := r.log.Str("section", "service-ea").Str("service", eaServiceName) managedServiceNames := spec.GetManagedServiceNames() - apply := func(svc *core.Service) (bool, error) { - return patcher.ServicePatcher(ctx, cachedStatus.ServicesModInterface().V1(), svc, meta.PatchOptions{}, + apply := func(svc *core.Service) (*core.Service, bool, error) { + return patcher.Patcher[*core.Service](ctx, cachedStatus.ServicesModInterface().V1(), svc, meta.PatchOptions{}, patcher.PatchServiceSelector(selectors)) } @@ -446,7 +446,7 @@ func (r *Resources) ensureExternalAccessManagedServices(ctx context.Context, cac log.Warn("the field \"spec.externalAccess.managedServiceNames\" should be provided for \"managed\" service type") return nil } - } else if changed, err := apply(svc); err != nil { + } else if _, changed, err := apply(svc); err != nil { return errors.WithMessage(err, "failed to ensure service selector") } else if changed { log.Info("selector applied to the managed service \"%s\"", svc.GetName()) @@ -464,7 +464,7 @@ func (r *Resources) ensureExternalAccessManagedServices(ctx context.Context, cac continue } - if changed, err := apply(svc); err != nil { + if _, changed, err := apply(svc); err != nil { return errors.WithMessage(err, "failed to ensure service selector") } else if changed { log.Info("selector applied to the managed service \"%s\"", svcName) diff --git a/pkg/handlers/networking/route/handler_deployment.go b/pkg/handlers/networking/route/handler_deployment.go index 23e16b7fa..86f7faa06 100644 --- a/pkg/handlers/networking/route/handler_deployment.go +++ b/pkg/handlers/networking/route/handler_deployment.go @@ -35,7 +35,7 @@ import ( ) func (h *handler) HandleArangoDeployment(ctx context.Context, item operation.Item, extension *networkingApi.ArangoRoute, status *networkingApi.ArangoRouteStatus) (bool, error) { - var name = util.WithDefault(extension.Spec.DeploymentName) + var name = util.WithDefault(extension.Spec.Deployment) if status.Deployment != nil { name = status.Deployment.GetName() diff --git a/pkg/handlers/networking/route/handler_deployment_test.go b/pkg/handlers/networking/route/handler_deployment_test.go index 53ae7eff5..b79d9f5a2 100644 --- a/pkg/handlers/networking/route/handler_deployment_test.go +++ b/pkg/handlers/networking/route/handler_deployment_test.go @@ -41,7 +41,7 @@ func Test_Handler_Deployment(t *testing.T) { // Arrange extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.DeploymentName = util.NewType("deployment") + obj.Spec.Deployment = util.NewType("deployment") }, func(t *testing.T, obj *networkingApi.ArangoRoute) { obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ @@ -73,7 +73,7 @@ func Test_Handler_MissingDeployment(t *testing.T) { // Arrange extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.DeploymentName = util.NewType("deployment-missing") + obj.Spec.Deployment = util.NewType("deployment-missing") }, func(t *testing.T, obj *networkingApi.ArangoRoute) { obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ @@ -105,7 +105,7 @@ func Test_Handler_Deployment_Changed(t *testing.T) { // Arrange extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.DeploymentName = util.NewType("deployment") + obj.Spec.Deployment = util.NewType("deployment") }, func(t *testing.T, obj *networkingApi.ArangoRoute) { obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ diff --git a/pkg/handlers/networking/route/handler_destination.go b/pkg/handlers/networking/route/handler_destination.go index 0d646a859..80806d135 100644 --- a/pkg/handlers/networking/route/handler_destination.go +++ b/pkg/handlers/networking/route/handler_destination.go @@ -35,7 +35,7 @@ import ( "github.com/arangodb/kube-arangodb/pkg/util" ) -func (h *handler) HandleArangoDestination(ctx context.Context, item operation.Item, extension *networkingApi.ArangoRoute, status *networkingApi.ArangoRouteStatus, _ *api.ArangoDeployment) (*operator.Condition, bool, error) { +func (h *handler) HandleArangoDestination(ctx context.Context, item operation.Item, extension *networkingApi.ArangoRoute, status *networkingApi.ArangoRouteStatus, deployment *api.ArangoDeployment) (*operator.Condition, bool, error) { if dest := extension.Spec.GetDestination(); dest != nil { if svc := dest.GetService(); svc != nil { port := svc.Port @@ -117,30 +117,56 @@ func (h *handler) HandleArangoDestination(ctx context.Context, item operation.It }, false, nil } - var targets = networkingApi.ArangoRouteStatusTargets{ - networkingApi.ArangoRouteStatusTarget{ - Url: fmt.Sprintf("%s://%s.%s.svc:%d%s", dest.GetSchema().String(), s.GetName(), s.GetNamespace(), destPort, extension.Spec.GetRoute().GetPath()), - TLS: networkingApi.ArangoRouteStatusTargetTLS{ - Insecure: extension.Spec.Destination.GetTLS().GetInsecure(), - }, - }, + var target networkingApi.ArangoRouteStatusTarget + + target.Path = dest.GetPath() + + if dest.Schema.Get() == networkingApi.ArangoRouteSpecDestinationSchemaHTTPS { + target.TLS = &networkingApi.ArangoRouteStatusTargetTLS{ + Insecure: util.NewType(extension.Spec.Destination.GetTLS().GetInsecure()), + } } - if status.Targets.Hash() == targets.Hash() { + if ip := s.Spec.ClusterIP; ip != "" { + target.Destinations = networkingApi.ArangoRouteStatusTargetDestinations{ + networkingApi.ArangoRouteStatusTargetDestination{ + Host: ip, + Port: destPort, + }, + } + } else { + if domain := deployment.Spec.ClusterDomain; domain != nil { + target.Destinations = networkingApi.ArangoRouteStatusTargetDestinations{ + networkingApi.ArangoRouteStatusTargetDestination{ + Host: fmt.Sprintf("%s.%s.svc.%s", s.GetName(), s.GetNamespace(), *domain), + Port: destPort, + }, + } + } else { + target.Destinations = networkingApi.ArangoRouteStatusTargetDestinations{ + networkingApi.ArangoRouteStatusTargetDestination{ + Host: fmt.Sprintf("%s.%s.svc", s.GetName(), s.GetNamespace()), + Port: destPort, + }, + } + } + } + + if status.Target.Hash() == target.Hash() { return &operator.Condition{ Status: true, Reason: "Destination Found", Message: "Destination Found", - Hash: targets.Hash(), + Hash: target.Hash(), }, false, nil } - status.Targets = targets + status.Target = &target return &operator.Condition{ Status: true, Reason: "Destination Found", Message: "Destination Found", - Hash: targets.Hash(), + Hash: target.Hash(), }, true, nil } } @@ -154,8 +180,8 @@ func (h *handler) HandleArangoDestination(ctx context.Context, item operation.It func (h *handler) HandleArangoDestinationWithTargets(ctx context.Context, item operation.Item, extension *networkingApi.ArangoRoute, status *networkingApi.ArangoRouteStatus, depl *api.ArangoDeployment) (*operator.Condition, bool, error) { c, changed, err := h.HandleArangoDestination(ctx, item, extension, status, depl) - if c == nil && !c.Status && status.Targets != nil { - status.Targets = nil + if c == nil && !c.Status && status.Target != nil { + status.Target = nil changed = true } diff --git a/pkg/handlers/networking/route/handler_destination_test.go b/pkg/handlers/networking/route/handler_destination_test.go index 81ff36452..e53cd0f7f 100644 --- a/pkg/handlers/networking/route/handler_destination_test.go +++ b/pkg/handlers/networking/route/handler_destination_test.go @@ -42,7 +42,7 @@ func Test_Handler_Destination_Service_Missing(t *testing.T) { // Arrange extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.DeploymentName = util.NewType("deployment") + obj.Spec.Deployment = util.NewType("deployment") }, func(t *testing.T, obj *networkingApi.ArangoRoute) { obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ @@ -81,7 +81,7 @@ func Test_Handler_Destination_Service_Valid(t *testing.T) { // Arrange extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.DeploymentName = util.NewType("deployment") + obj.Spec.Deployment = util.NewType("deployment") }, func(t *testing.T, obj *networkingApi.ArangoRoute) { obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ @@ -114,11 +114,117 @@ func Test_Handler_Destination_Service_Valid(t *testing.T) { require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + require.Len(t, extension.Status.Target.RenderURLs(), 1) + require.EqualValues(t, "http://deployment.fake.svc:10244/", extension.Status.Target.RenderURLs()[0]) + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) require.True(t, ok) require.EqualValues(t, c.Reason, "Destination Found") require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Hash, extension.Status.Targets.Hash()) + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) +} + +func Test_Handler_Destination_Service_Valid_WithIP(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Service: &networkingApi.ArangoRouteSpecDestinationService{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromInt32(10244)), + }, + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10244, + }, + } + obj.Spec.ClusterIP = "127.0.0.2" + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + + require.Len(t, extension.Status.Target.RenderURLs(), 1) + require.EqualValues(t, "http://127.0.0.2:10244/", extension.Status.Target.RenderURLs()[0]) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) +} + +func Test_Handler_Destination_Service_Valid_WithPath(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Service: &networkingApi.ArangoRouteSpecDestinationService{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromInt32(10244)), + }, + Path: util.NewType("/test/path/"), + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10244, + }, + } + obj.Spec.ClusterIP = "127.0.0.2" + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + + require.Len(t, extension.Status.Target.RenderURLs(), 1) + require.EqualValues(t, "http://127.0.0.2:10244/test/path/", extension.Status.Target.RenderURLs()[0]) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) } func Test_Handler_Destination_Service_ValidName(t *testing.T) { @@ -128,7 +234,7 @@ func Test_Handler_Destination_Service_ValidName(t *testing.T) { // Arrange extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.DeploymentName = util.NewType("deployment") + obj.Spec.Deployment = util.NewType("deployment") }, func(t *testing.T, obj *networkingApi.ArangoRoute) { obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ @@ -170,7 +276,7 @@ func Test_Handler_Destination_Service_ValidName(t *testing.T) { require.True(t, ok) require.EqualValues(t, c.Reason, "Destination Found") require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Hash, extension.Status.Targets.Hash()) + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) } func Test_Handler_Destination_Service_WrongPort(t *testing.T) { @@ -180,7 +286,7 @@ func Test_Handler_Destination_Service_WrongPort(t *testing.T) { // Arrange extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.DeploymentName = util.NewType("deployment") + obj.Spec.Deployment = util.NewType("deployment") }, func(t *testing.T, obj *networkingApi.ArangoRoute) { obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ @@ -226,7 +332,7 @@ func Test_Handler_Destination_Service_WrongPortName(t *testing.T) { // Arrange extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.DeploymentName = util.NewType("deployment") + obj.Spec.Deployment = util.NewType("deployment") }, func(t *testing.T, obj *networkingApi.ArangoRoute) { obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ @@ -272,7 +378,7 @@ func Test_Handler_Destination_Service_Insecure_Default(t *testing.T) { // Arrange extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.DeploymentName = util.NewType("deployment") + obj.Spec.Deployment = util.NewType("deployment") }, func(t *testing.T, obj *networkingApi.ArangoRoute) { obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ @@ -309,10 +415,9 @@ func Test_Handler_Destination_Service_Insecure_Default(t *testing.T) { require.True(t, ok) require.EqualValues(t, c.Reason, "Destination Found") require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Hash, extension.Status.Targets.Hash()) + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) - require.Len(t, extension.Status.Targets, 1) - require.False(t, extension.Status.Targets[0].TLS.Insecure) + require.False(t, extension.Status.Target.TLS.IsInsecure()) } func Test_Handler_Destination_Service_Insecure_Nil(t *testing.T) { @@ -322,7 +427,7 @@ func Test_Handler_Destination_Service_Insecure_Nil(t *testing.T) { // Arrange extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.DeploymentName = util.NewType("deployment") + obj.Spec.Deployment = util.NewType("deployment") }, func(t *testing.T, obj *networkingApi.ArangoRoute) { obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ @@ -362,20 +467,19 @@ func Test_Handler_Destination_Service_Insecure_Nil(t *testing.T) { require.True(t, ok) require.EqualValues(t, c.Reason, "Destination Found") require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Hash, extension.Status.Targets.Hash()) + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) - require.Len(t, extension.Status.Targets, 1) - require.False(t, extension.Status.Targets[0].TLS.Insecure) + require.False(t, extension.Status.Target.TLS.IsInsecure()) } -func Test_Handler_Destination_Service_Insecure_Override(t *testing.T) { +func Test_Handler_Destination_Service_Insecure_HTTPS_Override(t *testing.T) { // Setup handler := newFakeHandler() // Arrange extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.DeploymentName = util.NewType("deployment") + obj.Spec.Deployment = util.NewType("deployment") }, func(t *testing.T, obj *networkingApi.ArangoRoute) { obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ @@ -388,6 +492,7 @@ func Test_Handler_Destination_Service_Insecure_Override(t *testing.T) { TLS: &networkingApi.ArangoRouteSpecDestinationTLS{ Insecure: util.NewType(true), }, + Schema: util.NewType(networkingApi.ArangoRouteSpecDestinationSchemaHTTPS), } }) deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") @@ -415,8 +520,60 @@ func Test_Handler_Destination_Service_Insecure_Override(t *testing.T) { require.True(t, ok) require.EqualValues(t, c.Reason, "Destination Found") require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Hash, extension.Status.Targets.Hash()) + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) - require.Len(t, extension.Status.Targets, 1) - require.True(t, extension.Status.Targets[0].TLS.Insecure) + require.True(t, extension.Status.Target.TLS.IsInsecure()) +} + +func Test_Handler_Destination_Service_Insecure_HTTP_Override(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Service: &networkingApi.ArangoRouteSpecDestinationService{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromInt32(10244)), + }, + TLS: &networkingApi.ArangoRouteSpecDestinationTLS{ + Insecure: util.NewType(true), + }, + Schema: util.NewType(networkingApi.ArangoRouteSpecDestinationSchemaHTTP), + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10244, + }, + } + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) + + require.False(t, extension.Status.Target.TLS.IsInsecure()) } diff --git a/pkg/integrations/sidecar/core.go b/pkg/integrations/sidecar/core.go new file mode 100644 index 000000000..fc61f232b --- /dev/null +++ b/pkg/integrations/sidecar/core.go @@ -0,0 +1,59 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package sidecar + +import ( + "fmt" + "strings" + + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +type Core struct { + Internal *bool + External *bool +} + +func (c *Core) GetInternal() bool { + if c == nil || c.Internal == nil { + return true + } + + return *c.Internal +} + +func (c *Core) GetExternal() bool { + if c == nil || c.External == nil { + return false + } + + return *c.External +} + +func (c *Core) Args(int Integration) k8sutil.OptionPairs { + var options k8sutil.OptionPairs + name, ver := int.Name() + + options.Add(fmt.Sprintf("--integration.%s.%s.internal", strings.ToLower(name), strings.ToLower(ver)), c.GetInternal()) + options.Add(fmt.Sprintf("--integration.%s.%s.external", strings.ToLower(name), strings.ToLower(ver)), c.GetExternal()) + + return options +} diff --git a/pkg/integrations/sidecar/integration.authentication.v1.go b/pkg/integrations/sidecar/integration.authentication.v1.go new file mode 100644 index 000000000..7fc742fa6 --- /dev/null +++ b/pkg/integrations/sidecar/integration.authentication.v1.go @@ -0,0 +1,85 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package sidecar + +import ( + core "k8s.io/api/core/v1" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" + "github.com/arangodb/kube-arangodb/pkg/deployment/pod" + "github.com/arangodb/kube-arangodb/pkg/util/errors" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +var _ IntegrationVolumes = IntegrationAuthenticationV1{} + +type IntegrationAuthenticationV1 struct { + Core *Core + Deployment *api.ArangoDeployment +} + +func (i IntegrationAuthenticationV1) Name() (string, string) { + return "AUTHENTICATION", "V1" +} + +func (i IntegrationAuthenticationV1) Validate() error { + if i.Deployment == nil { + return errors.Errorf("Deployment is nil") + } + + return nil +} + +func (i IntegrationAuthenticationV1) Args() (k8sutil.OptionPairs, error) { + options := k8sutil.CreateOptionPairs() + + options.Add("--integration.authentication.v1", true) + options.Add("--integration.authentication.v1.enabled", i.Deployment.GetAcceptedSpec().IsAuthenticated()) + options.Add("--integration.authentication.v1.path", shared.ClusterJWTSecretVolumeMountDir) + + options.Merge(i.Core.Args(i)) + + return options, nil +} + +func (i IntegrationAuthenticationV1) Volumes() ([]core.Volume, []core.VolumeMount, error) { + if i.Deployment.GetAcceptedSpec().IsAuthenticated() { + return []core.Volume{ + { + Name: shared.ClusterJWTSecretVolumeName, + VolumeSource: core.VolumeSource{ + Secret: &core.SecretVolumeSource{ + SecretName: pod.JWTSecretFolder(i.Deployment.GetName()), + }, + }, + }, + }, []core.VolumeMount{ + { + Name: shared.ClusterJWTSecretVolumeName, + ReadOnly: true, + MountPath: shared.ClusterJWTSecretVolumeMountDir, + }, + }, nil + } + + return nil, nil, nil +} diff --git a/pkg/integrations/sidecar/integration.authorization.v1.go b/pkg/integrations/sidecar/integration.authorization.v1.go new file mode 100644 index 000000000..76fbe8b67 --- /dev/null +++ b/pkg/integrations/sidecar/integration.authorization.v1.go @@ -0,0 +1,47 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package sidecar + +import ( + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +type IntegrationAuthorizationV0 struct { + Core *Core +} + +func (i IntegrationAuthorizationV0) Name() (string, string) { + return "AUTHORIZATION", "V0" +} + +func (i IntegrationAuthorizationV0) Validate() error { + return nil +} + +func (i IntegrationAuthorizationV0) Args() (k8sutil.OptionPairs, error) { + options := k8sutil.CreateOptionPairs() + + options.Add("--integration.authorization.v0", true) + + options.Merge(i.Core.Args(i)) + + return options, nil +} diff --git a/pkg/integrations/sidecar/integration.envoy.v3.go b/pkg/integrations/sidecar/integration.envoy.v3.go new file mode 100644 index 000000000..08f626871 --- /dev/null +++ b/pkg/integrations/sidecar/integration.envoy.v3.go @@ -0,0 +1,57 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package sidecar + +import ( + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" + "github.com/arangodb/kube-arangodb/pkg/util/errors" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +type IntegrationEnvoyV3 struct { + Core *Core + Deployment *api.ArangoDeployment +} + +func (i IntegrationEnvoyV3) Name() (string, string) { + return "ENVOY", "V3" +} + +func (i IntegrationEnvoyV3) Validate() error { + if i.Deployment == nil { + return errors.Errorf("Deployment is nil") + } + + return nil +} + +func (i IntegrationEnvoyV3) Args() (k8sutil.OptionPairs, error) { + options := k8sutil.CreateOptionPairs() + + options.Add("--integration.authentication.v1", true) + options.Add("--integration.authentication.v1.enabled", i.Deployment.GetAcceptedSpec().IsAuthenticated()) + options.Add("--integration.authentication.v1.path", shared.ClusterJWTSecretVolumeMountDir) + + options.Merge(i.Core.Args(i)) + + return options, nil +} diff --git a/pkg/integrations/sidecar/integration.go b/pkg/integrations/sidecar/integration.go new file mode 100644 index 000000000..53146ff0e --- /dev/null +++ b/pkg/integrations/sidecar/integration.go @@ -0,0 +1,211 @@ +// +// DISCLAIMER +// +// Copyright 2023-2024 ArangoDB GmbH, Cologne, Germany + +package sidecar + +import ( + "fmt" + "strings" + + core "k8s.io/api/core/v1" + + schedulerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1" + schedulerContainerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1/container" + schedulerContainerResourcesApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1/container/resources" + schedulerPodApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1/pod" + schedulerPodResourcesApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1/pod/resources" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/constants" + "github.com/arangodb/kube-arangodb/pkg/util/errors" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +const ( + ContainerName = "integration" + ListenPortName = "integration" + ListenPortHealthName = "health" +) + +func WithIntegrationEnvs(in Integration) ([]core.EnvVar, error) { + if v, ok := in.(IntegrationEnvs); ok { + return v.Envs() + } + + return nil, nil +} + +type IntegrationEnvs interface { + Integration + Envs() ([]core.EnvVar, error) +} + +func WithIntegrationVolumes(in Integration) ([]core.Volume, []core.VolumeMount, error) { + if v, ok := in.(IntegrationVolumes); ok { + return v.Volumes() + } + + return nil, nil, nil +} + +type IntegrationVolumes interface { + Integration + Volumes() ([]core.Volume, []core.VolumeMount, error) +} + +type Integration interface { + Name() (string, string) + Args() (k8sutil.OptionPairs, error) + Validate() error +} + +func NewIntegration(image *schedulerContainerResourcesApi.Image, integration *schedulerApi.IntegrationSidecar, coreContainers []string, integrations ...Integration) (*schedulerApi.ProfileTemplate, error) { + for _, integration := range integrations { + if err := integration.Validate(); err != nil { + name, version := integration.Name() + + return nil, errors.Wrapf(err, "Failure in %s/%s", name, version) + } + } + + // Arguments + + exePath := k8sutil.BinaryPath() + lifecycle, err := k8sutil.NewLifecycleFinalizersWithBinary(exePath) + if err != nil { + return nil, errors.Wrapf(err, "NewLifecycleFinalizers failed") + } + + options := k8sutil.CreateOptionPairs(64) + + options.Addf("--services.address", "127.0.0.1:%d", integration.GetListenPort()) + options.Addf("--health.address", "0.0.0.0:%d", integration.GetControllerListenPort()) + + // Volumes + var volumes []core.Volume + var volumeMounts []core.VolumeMount + + // Envs + + var envs = []core.EnvVar{ + { + Name: "INTEGRATION_API_ADDRESS", + Value: fmt.Sprintf("127.0.0.1:%d", integration.GetListenPort()), + }, + { + Name: "INTEGRATION_SERVICE_ADDRESS", + Value: fmt.Sprintf("127.0.0.1:%d", integration.GetListenPort()), + }, + } + + for _, i := range integrations { + name, version := i.Name() + + if err := i.Validate(); err != nil { + return nil, errors.Wrapf(err, "Failure in %s/%s", name, version) + } + + if args, err := i.Args(); err != nil { + return nil, errors.Wrapf(err, "Failure in arguments %s/%s", name, version) + } else if len(args) > 0 { + options.Merge(args) + } + + if lvolumes, lvolumeMounts, err := WithIntegrationVolumes(i); err != nil { + return nil, errors.Wrapf(err, "Failure in volumes %s/%s", name, version) + } else if len(lvolumes) > 0 || len(lvolumeMounts) > 0 { + volumes = append(volumes, lvolumes...) + volumeMounts = append(volumeMounts, lvolumeMounts...) + } + + if lenvs, err := WithIntegrationEnvs(i); err != nil { + return nil, errors.Wrapf(err, "Failure in envs %s/%s", name, version) + } else if len(lenvs) > 0 { + envs = append(envs, lenvs...) + } + + envs = append(envs, core.EnvVar{ + Name: fmt.Sprintf("INTEGRATION_SERVICE_%s_%s", strings.ToUpper(name), strings.ToUpper(version)), + Value: fmt.Sprintf("127.0.0.1:%d", integration.GetListenPort()), + }) + } + + c := schedulerContainerApi.Container{ + Core: &schedulerContainerResourcesApi.Core{ + Command: append([]string{exePath, "integration"}, options.Sort().AsArgs()...), + }, + Environments: &schedulerContainerResourcesApi.Environments{ + Env: k8sutil.GetLifecycleEnv(), + }, + Networking: &schedulerContainerResourcesApi.Networking{ + Ports: []core.ContainerPort{ + { + Name: ListenPortName, + ContainerPort: int32(integration.GetListenPort()), + Protocol: core.ProtocolTCP, + }, + { + Name: ListenPortHealthName, + ContainerPort: int32(integration.GetControllerListenPort()), + Protocol: core.ProtocolTCP, + }, + }, + }, + Image: image, + + Lifecycle: &schedulerContainerResourcesApi.Lifecycle{ + Lifecycle: lifecycle, + }, + + Probes: &schedulerContainerResourcesApi.Probes{ + ReadinessProbe: &core.Probe{ + ProbeHandler: core.ProbeHandler{ + GRPC: &core.GRPCAction{ + Port: int32(integration.GetControllerListenPort()), + }, + }, + InitialDelaySeconds: 1, // Wait 1s before first probe + TimeoutSeconds: 2, // Timeout of each probe is 2s + PeriodSeconds: 30, // Interval between probes is 30s + SuccessThreshold: 1, // Single probe is enough to indicate success + FailureThreshold: 2, // Need 2 failed probes to consider a failed state + }, + }, + + VolumeMounts: &schedulerContainerResourcesApi.VolumeMounts{ + VolumeMounts: volumeMounts, + }, + } + + pt := schedulerApi.ProfileTemplate{ + Container: &schedulerApi.ProfileContainerTemplate{ + Containers: map[string]schedulerContainerApi.Container{ + ContainerName: util.TypeOrDefault(k8sutil.CreateDefaultContainerTemplate(image).With(&c).With(integration.GetContainer())), + }, + }, + Pod: &schedulerPodApi.Pod{ + Metadata: &schedulerPodResourcesApi.Metadata{ + Annotations: map[string]string{}, + }, + Volumes: &schedulerPodResourcesApi.Volumes{ + Volumes: volumes, + }, + }, + } + + for _, container := range coreContainers { + pt.Pod.Metadata.Annotations[fmt.Sprintf("%s/%s", constants.AnnotationShutdownCoreContainer, container)] = constants.AnnotationShutdownCoreContainerModeWait + } + + pt.Pod.Metadata.Annotations[fmt.Sprintf("%s/%s", constants.AnnotationShutdownContainer, ContainerName)] = ListenPortHealthName + pt.Pod.Metadata.Annotations[constants.AnnotationShutdownManagedContainer] = "true" + + pt.Container.Containers.ExtendContainers(&schedulerContainerApi.Container{ + Environments: &schedulerContainerResourcesApi.Environments{ + Env: envs, + }, + }, coreContainers...) + + return &pt, nil +} diff --git a/pkg/integrations/sidecar/integration.shutdown.v1.go b/pkg/integrations/sidecar/integration.shutdown.v1.go new file mode 100644 index 000000000..f8752c6d2 --- /dev/null +++ b/pkg/integrations/sidecar/integration.shutdown.v1.go @@ -0,0 +1,47 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package sidecar + +import ( + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +type IntegrationShutdownV1 struct { + Core *Core +} + +func (i IntegrationShutdownV1) Name() (string, string) { + return "SHUTDOWN", "V1" +} + +func (i IntegrationShutdownV1) Validate() error { + return nil +} + +func (i IntegrationShutdownV1) Args() (k8sutil.OptionPairs, error) { + options := k8sutil.CreateOptionPairs() + + options.Add("--integration.shutdown.v1", true) + + options.Merge(i.Core.Args(i)) + + return options, nil +} diff --git a/pkg/util/dict_test.go b/pkg/util/dict_test.go new file mode 100644 index 000000000..b0f1a3014 --- /dev/null +++ b/pkg/util/dict_test.go @@ -0,0 +1,50 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package util + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/require" +) + +func testDictDefaultValue[T any](t *testing.T, expected T) { + t.Run(reflect.TypeOf(expected).String(), func(t *testing.T) { + m := map[string]T{} + + ev, ok := m["missing"] + require.False(t, ok) + + require.Equal(t, expected, ev) + + evs := m["missing"] + + require.Equal(t, expected, evs) + }) +} + +func Test_Dict_Types(t *testing.T) { + testDictDefaultValue[string](t, "") + testDictDefaultValue[int](t, 0) + testDictDefaultValue[*string](t, nil) + testDictDefaultValue[*int](t, nil) +} diff --git a/pkg/util/k8sutil/interfaces/pod_creator.go b/pkg/util/k8sutil/interfaces/pod_creator.go index aa28aed45..2bfe75934 100644 --- a/pkg/util/k8sutil/interfaces/pod_creator.go +++ b/pkg/util/k8sutil/interfaces/pod_creator.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2023 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2024 ArangoDB GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import ( core "k8s.io/api/core/v1" + schedulerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/secret" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/service" ) @@ -39,11 +40,11 @@ type PodModifier interface { } type PodCreator interface { - Init(context.Context, Inspector, *core.Pod) error + Init(context.Context, Inspector, *core.PodTemplateSpec) error GetName() string GetRole() string GetVolumes() []core.Volume - GetSidecars(*core.Pod) error + GetSidecars(*core.PodTemplateSpec) error GetInitContainers(cachedStatus Inspector) ([]core.Container, error) GetFinalizers() []string GetTolerations() []core.Toleration @@ -61,6 +62,8 @@ type PodCreator interface { Annotations() map[string]string Labels() map[string]string + Profiles() (schedulerApi.ProfileTemplates, error) + PodModifier } diff --git a/pkg/util/k8sutil/pair.go b/pkg/util/k8sutil/pair.go index 576bd17fd..082decd49 100644 --- a/pkg/util/k8sutil/pair.go +++ b/pkg/util/k8sutil/pair.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2023 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2024 ArangoDB GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -148,6 +148,17 @@ func (o OptionPairs) AsArgs() []string { return s } +func (o OptionPairs) AsSplittedArgs() []string { + s := make([]string, len(o)*2) + + for id, pair := range o { + s[id*2] = pair.Key + s[id*2+1] = pair.Value + } + + return s +} + // OptionPair key value pair builder type OptionPair struct { Key string diff --git a/pkg/util/k8sutil/patcher/config_map.go b/pkg/util/k8sutil/patcher/config_map.go new file mode 100644 index 000000000..367929fc3 --- /dev/null +++ b/pkg/util/k8sutil/patcher/config_map.go @@ -0,0 +1,40 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package patcher + +import ( + core "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + + "github.com/arangodb/kube-arangodb/pkg/deployment/patch" +) + +func PatchConfigMapData(data map[string]string) Patch[*core.ConfigMap] { + return func(in *core.ConfigMap) []patch.Item { + if len(data) == len(in.Data) && equality.Semantic.DeepDerivative(data, in.Data) { + return nil + } + + return []patch.Item{ + patch.ItemReplace(patch.NewPath("data"), data), + } + } +} diff --git a/pkg/util/k8sutil/patcher/config_map_test.go b/pkg/util/k8sutil/patcher/config_map_test.go new file mode 100644 index 000000000..1f471cb58 --- /dev/null +++ b/pkg/util/k8sutil/patcher/config_map_test.go @@ -0,0 +1,105 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package patcher + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/arangodb/kube-arangodb/pkg/util/tests" +) + +func Test_ConfigMap(t *testing.T) { + c := tests.NewEmptyInspector(t) + + t.Run("Create", func(t *testing.T) { + require.NoError(t, c.Refresh(context.Background())) + + _, err := c.ConfigMapsModInterface().V1().Create(context.Background(), &core.ConfigMap{ + ObjectMeta: meta.ObjectMeta{ + Name: "test", + Namespace: c.Namespace(), + }, + }, meta.CreateOptions{}) + require.NoError(t, err) + }) + + require.NoError(t, c.Refresh(context.Background())) + + t.Run("Check", func(t *testing.T) { + cm, ok := c.ConfigMap().V1().GetSimple("test") + require.True(t, ok) + require.Len(t, cm.Data, 0) + }) + + require.NoError(t, c.Refresh(context.Background())) + + t.Run("Update", func(t *testing.T) { + cm, ok := c.ConfigMap().V1().GetSimple("test") + require.True(t, ok) + uCm, ok, err := Patcher[*core.ConfigMap](context.Background(), c.ConfigMapsModInterface().V1(), cm, meta.PatchOptions{}, PatchConfigMapData(map[string]string{ + "A": "B", + })) + require.NoError(t, err) + require.True(t, ok) + + require.NoError(t, c.Refresh(context.Background())) + + cm, ok = c.ConfigMap().V1().GetSimple("test") + require.True(t, ok) + + require.Equal(t, map[string]string{ + "A": "B", + }, uCm.Data) + + require.Equal(t, map[string]string{ + "A": "B", + }, cm.Data) + }) + + t.Run("Reupdate", func(t *testing.T) { + cm, ok := c.ConfigMap().V1().GetSimple("test") + require.True(t, ok) + + uCm, ok, err := Patcher[*core.ConfigMap](context.Background(), c.ConfigMapsModInterface().V1(), cm, meta.PatchOptions{}, PatchConfigMapData(map[string]string{ + "A": "B", + })) + require.NoError(t, err) + require.False(t, ok) + + require.NoError(t, c.Refresh(context.Background())) + + cm, ok = c.ConfigMap().V1().GetSimple("test") + require.True(t, ok) + + require.Equal(t, map[string]string{ + "A": "B", + }, uCm.Data) + + require.Equal(t, map[string]string{ + "A": "B", + }, cm.Data) + }) +} diff --git a/pkg/util/k8sutil/patcher/patcher.go b/pkg/util/k8sutil/patcher/patcher.go new file mode 100644 index 000000000..41bbd53b7 --- /dev/null +++ b/pkg/util/k8sutil/patcher/patcher.go @@ -0,0 +1,89 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package patcher + +import ( + "context" + "reflect" + + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/arangodb/kube-arangodb/pkg/deployment/patch" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/globals" +) + +type Patch[T meta.Object] func(in T) []patch.Item + +type Client[T meta.Object] interface { + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts meta.PatchOptions, subresources ...string) (result T, err error) +} + +func Patcher[T meta.Object](ctx context.Context, client Client[T], in T, opts meta.PatchOptions, functions ...Patch[T]) (T, bool, error) { + if v := reflect.ValueOf(in); !v.IsValid() || v.IsZero() { + return util.Default[T](), false, nil + } + + if in.GetName() == "" { + return util.Default[T](), false, nil + } + + var items []patch.Item + + for id := range functions { + if f := functions[id]; f != nil { + items = append(items, f(in)...) + } + } + + if len(items) == 0 { + return in, false, nil + } + + data, err := patch.NewPatch(items...).Marshal() + if err != nil { + return util.Default[T](), false, err + } + + nctx, c := globals.GetGlobals().Timeouts().Kubernetes().WithTimeout(ctx) + defer c() + + if obj, err := client.Patch(nctx, in.GetName(), types.JSONPatchType, data, opts); err != nil { + return util.Default[T](), false, err + } else { + return obj, true, nil + } +} + +func Optional[T meta.Object](p Patch[T], enabled bool) Patch[T] { + return func(in T) []patch.Item { + if !enabled { + return nil + } + + if p != nil { + return p(in) + } + + return nil + } +} diff --git a/pkg/util/k8sutil/patcher/service.go b/pkg/util/k8sutil/patcher/service.go index 274560224..4ac7cddb3 100644 --- a/pkg/util/k8sutil/patcher/service.go +++ b/pkg/util/k8sutil/patcher/service.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2024 ArangoDB GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,57 +21,13 @@ package patcher import ( - "context" - core "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" - meta "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" "github.com/arangodb/kube-arangodb/pkg/deployment/patch" - "github.com/arangodb/kube-arangodb/pkg/util/globals" - v1 "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/service/v1" ) -type ServicePatch func(in *core.Service) []patch.Item - -func ServicePatcher(ctx context.Context, client v1.ModInterface, in *core.Service, opts meta.PatchOptions, functions ...ServicePatch) (bool, error) { - if in == nil { - return false, nil - } - - if in.GetName() == "" { - return false, nil - } - - var items []patch.Item - - for id := range functions { - if f := functions[id]; f != nil { - items = append(items, f(in)...) - } - } - - if len(items) == 0 { - return false, nil - } - - data, err := patch.NewPatch(items...).Marshal() - if err != nil { - return false, err - } - - nctx, c := globals.GetGlobals().Timeouts().Kubernetes().WithTimeout(ctx) - defer c() - - if _, err := client.Patch(nctx, in.GetName(), types.JSONPatchType, data, opts); err != nil { - return false, err - } - - return true, nil -} - -func PatchServicePorts(ports []core.ServicePort) ServicePatch { +func PatchServicePorts(ports []core.ServicePort) Patch[*core.Service] { return func(in *core.Service) []patch.Item { if len(ports) == len(in.Spec.Ports) && equality.Semantic.DeepDerivative(ports, in.Spec.Ports) { return nil @@ -83,21 +39,7 @@ func PatchServicePorts(ports []core.ServicePort) ServicePatch { } } -func Optional(p ServicePatch, enabled bool) ServicePatch { - return func(in *core.Service) []patch.Item { - if !enabled { - return nil - } - - if p != nil { - return p(in) - } - - return nil - } -} - -func PatchServiceOnlyPorts(ports ...core.ServicePort) ServicePatch { +func PatchServiceOnlyPorts(ports ...core.ServicePort) Patch[*core.Service] { return func(in *core.Service) []patch.Item { psvc := in.Spec.DeepCopy() cp := psvc.Ports @@ -149,7 +91,7 @@ func PatchServiceOnlyPorts(ports ...core.ServicePort) ServicePatch { } } -func PatchServiceSelector(selector map[string]string) ServicePatch { +func PatchServiceSelector(selector map[string]string) Patch[*core.Service] { return func(in *core.Service) []patch.Item { if in.Spec.Selector != nil && equality.Semantic.DeepEqual(in.Spec.Selector, selector) { return nil @@ -161,7 +103,7 @@ func PatchServiceSelector(selector map[string]string) ServicePatch { } } -func PatchServiceType(t core.ServiceType) ServicePatch { +func PatchServiceType(t core.ServiceType) Patch[*core.Service] { return func(in *core.Service) []patch.Item { if in.Spec.Type == t { return nil @@ -173,7 +115,7 @@ func PatchServiceType(t core.ServiceType) ServicePatch { } } -func PatchServicePublishNotReadyAddresses(publishNotReadyAddresses bool) ServicePatch { +func PatchServicePublishNotReadyAddresses(publishNotReadyAddresses bool) Patch[*core.Service] { return func(in *core.Service) []patch.Item { if in.Spec.PublishNotReadyAddresses == publishNotReadyAddresses { return nil diff --git a/pkg/util/k8sutil/patcher/service_test.go b/pkg/util/k8sutil/patcher/service_test.go index 30c8b22b3..9ca3c1736 100644 --- a/pkg/util/k8sutil/patcher/service_test.go +++ b/pkg/util/k8sutil/patcher/service_test.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2024 ArangoDB GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -53,7 +53,7 @@ func Test_Service(t *testing.T) { require.True(t, ok) require.False(t, svc.Spec.PublishNotReadyAddresses) - changed, err := ServicePatcher(context.Background(), c.ServicesModInterface().V1(), svc, meta.PatchOptions{}, PatchServicePublishNotReadyAddresses(true)) + _, changed, err := Patcher[*core.Service](context.Background(), c.ServicesModInterface().V1(), svc, meta.PatchOptions{}, PatchServicePublishNotReadyAddresses(true)) require.NoError(t, err) require.True(t, changed) @@ -69,7 +69,7 @@ func Test_Service(t *testing.T) { require.True(t, ok) require.True(t, svc.Spec.PublishNotReadyAddresses) - changed, err := ServicePatcher(context.Background(), c.ServicesModInterface().V1(), svc, meta.PatchOptions{}, PatchServicePublishNotReadyAddresses(true)) + _, changed, err := Patcher[*core.Service](context.Background(), c.ServicesModInterface().V1(), svc, meta.PatchOptions{}, PatchServicePublishNotReadyAddresses(true)) require.NoError(t, err) require.False(t, changed) @@ -85,7 +85,7 @@ func Test_Service(t *testing.T) { require.True(t, ok) require.True(t, svc.Spec.PublishNotReadyAddresses) - changed, err := ServicePatcher(context.Background(), c.ServicesModInterface().V1(), svc, meta.PatchOptions{}, PatchServicePublishNotReadyAddresses(false)) + _, changed, err := Patcher[*core.Service](context.Background(), c.ServicesModInterface().V1(), svc, meta.PatchOptions{}, PatchServicePublishNotReadyAddresses(false)) require.NoError(t, err) require.True(t, changed) diff --git a/pkg/util/k8sutil/pods.go b/pkg/util/k8sutil/pods.go index 91359295b..5d27339e4 100644 --- a/pkg/util/k8sutil/pods.go +++ b/pkg/util/k8sutil/pods.go @@ -560,10 +560,10 @@ func NewContainer(containerCreator interfaces.ContainerCreator) (core.Container, } // NewPod creates a basic Pod for given settings. -func NewPod(deploymentName, role, id, podName string, podCreator interfaces.PodCreator) core.Pod { +func NewPod(deploymentName, role, id, podName string, podCreator interfaces.PodCreator) core.PodTemplateSpec { hostname := shared.CreatePodHostName(deploymentName, role, id) - p := core.Pod{ + p := core.PodTemplateSpec{ ObjectMeta: meta.ObjectMeta{ Name: podName, Labels: LabelsForMember(deploymentName, role, id), diff --git a/pkg/util/k8sutil/services.go b/pkg/util/k8sutil/services.go index 2d9b84afa..f4cbc065b 100644 --- a/pkg/util/k8sutil/services.go +++ b/pkg/util/k8sutil/services.go @@ -82,7 +82,7 @@ func CreateExporterService(ctx context.Context, cachedStatus inspector.Inspector svcName := CreateExporterClientServiceName(deploymentName) if svc, exists := cachedStatus.Service().V1().GetSimple(svcName); exists { - if changed, err := patcher.ServicePatcher(ctx, cachedStatus.ServicesModInterface().V1(), svc, meta.PatchOptions{}, patcher.PatchServiceSelector(selectors), patcher.PatchServicePorts(ports)); err != nil { + if _, changed, err := patcher.Patcher[*core.Service](ctx, cachedStatus.ServicesModInterface().V1(), svc, meta.PatchOptions{}, patcher.PatchServiceSelector(selectors), patcher.PatchServicePorts(ports)); err != nil { return "", false, err } else { return svcName, changed, nil diff --git a/pkg/util/k8sutil/tolerations/tolerations.go b/pkg/util/k8sutil/tolerations/tolerations.go index e0bfa0656..0cc4cf2f0 100644 --- a/pkg/util/k8sutil/tolerations/tolerations.go +++ b/pkg/util/k8sutil/tolerations/tolerations.go @@ -24,8 +24,6 @@ import ( "time" core "k8s.io/api/core/v1" - - api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" ) const ( @@ -105,41 +103,3 @@ func AddTolerationIfNotFound(source []core.Toleration, toAdd core.Toleration) [] return append(source, toAdd) } - -// CreatePodTolerations creates a list of tolerations for a pod created for the given group. -func CreatePodTolerations(mode api.DeploymentMode, group api.ServerGroup) []core.Toleration { - notReadyDur := TolerationDuration{Forever: false, TimeSpan: time.Minute} - unreachableDur := TolerationDuration{Forever: false, TimeSpan: time.Minute} - switch group { - case api.ServerGroupAgents: - notReadyDur.Forever = true - unreachableDur.Forever = true - case api.ServerGroupCoordinators: - notReadyDur.TimeSpan = 15 * time.Second - unreachableDur.TimeSpan = 15 * time.Second - case api.ServerGroupDBServers: - notReadyDur.TimeSpan = 5 * time.Minute - unreachableDur.TimeSpan = 5 * time.Minute - case api.ServerGroupSingle: - if mode == api.DeploymentModeSingle { - notReadyDur.Forever = true - unreachableDur.Forever = true - } else { - notReadyDur.TimeSpan = 5 * time.Minute - unreachableDur.TimeSpan = 5 * time.Minute - } - case api.ServerGroupSyncMasters: - notReadyDur.TimeSpan = 15 * time.Second - unreachableDur.TimeSpan = 15 * time.Second - case api.ServerGroupSyncWorkers: - notReadyDur.TimeSpan = 1 * time.Minute - unreachableDur.TimeSpan = 1 * time.Minute - case api.ServerGroupGateways: - notReadyDur.TimeSpan = 15 * time.Second - unreachableDur.TimeSpan = 15 * time.Second - } - return []core.Toleration{NewNoExecuteToleration(TolerationKeyNodeNotReady, notReadyDur), - NewNoExecuteToleration(TolerationKeyNodeUnreachable, unreachableDur), - NewNoExecuteToleration(TolerationKeyNodeAlphaUnreachable, unreachableDur), - } -}