diff --git a/api/v1alpha1/dragonfly_types.go b/api/v1alpha1/dragonfly_types.go index 276d06b..b3efba1 100644 --- a/api/v1alpha1/dragonfly_types.go +++ b/api/v1alpha1/dragonfly_types.go @@ -144,6 +144,11 @@ type DragonflySpec struct { // +optional // +kubebuilder:validation:Optional ServiceSpec *ServiceSpec `json:"serviceSpec,omitempty"` + + // (Optional) Dragonfly pod init containers + // +optional + // +kubebuilder:validation:Optional + InitContainers []corev1.Container `json:"initContainers,omitempty"` } type ServiceSpec struct { @@ -221,6 +226,9 @@ type DragonflyStatus struct { //+kubebuilder:object:root=true //+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="The current phase of the Dragonfly cluster" +//+kubebuilder:printcolumn:name="Rolling Update",type="boolean",JSONPath=".status.isRollingUpdate",description="Indicates if a rolling update is in progress" +//+kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".spec.replicas",description="Number of replicas" // Dragonfly is the Schema for the dragonflies API type Dragonfly struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 2e00e07..b242d6f 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -204,6 +204,13 @@ func (in *DragonflySpec) DeepCopyInto(out *DragonflySpec) { *out = new(ServiceSpec) (*in).DeepCopyInto(*out) } + if in.InitContainers != nil { + in, out := &in.InitContainers, &out.InitContainers + *out = make([]v1.Container, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DragonflySpec. diff --git a/charts/dragonfly-operator/Chart.yaml b/charts/dragonfly-operator/Chart.yaml index 336e539..6ba195e 100644 --- a/charts/dragonfly-operator/Chart.yaml +++ b/charts/dragonfly-operator/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: v1.1.7 +version: v1.1.8 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.1.7" +appVersion: "v1.1.8" diff --git a/config/crd/bases/dragonflydb.io_dragonflies.yaml b/config/crd/bases/dragonflydb.io_dragonflies.yaml index ef9274e..3f05dd8 100644 --- a/config/crd/bases/dragonflydb.io_dragonflies.yaml +++ b/config/crd/bases/dragonflydb.io_dragonflies.yaml @@ -14,7 +14,20 @@ spec: singular: dragonfly scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - description: The current phase of the Dragonfly cluster + jsonPath: .status.phase + name: Phase + type: string + - description: Indicates if a rolling update is in progress + jsonPath: .status.isRollingUpdate + name: Rolling Update + type: boolean + - description: Number of replicas + jsonPath: .spec.replicas + name: Replicas + type: integer + name: v1alpha1 schema: openAPIV3Schema: description: Dragonfly is the Schema for the dragonflies API @@ -1376,6 +1389,1434 @@ spec: description: (Optional) imagePullPolicy to set to Dragonfly, default is Always type: string + initContainers: + description: (Optional) Dragonfly pod init containers + items: + description: A single application container that you want to run + within a pod. + properties: + args: + description: |- + 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. + More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + description: |- + 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. + More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + description: |- + List of environment variables to set in the container. + Cannot be updated. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be + a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + description: |- + List of sources to populate environment variables in the container. + The keys defined within a source must be a C_IDENTIFIER. All invalid keys + will be reported as an event when the container is starting. When a key exists in multiple + sources, the value associated with the last source will take precedence. + Values defined by an Env with a duplicate key will take precedence. + Cannot be updated. + items: + description: EnvFromSource represents the source of a set + of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + optional: + description: Specify whether the ConfigMap must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend to each + key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + optional: + description: Specify whether the Secret must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + description: |- + Container image name. + More info: https://kubernetes.io/docs/concepts/containers/images + This field is optional to allow higher level config management to default or override + container images in workload controllers like Deployments and StatefulSets. + type: string + imagePullPolicy: + description: |- + Image pull policy. + One of Always, Never, IfNotPresent. + Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images + type: string + lifecycle: + description: |- + Actions that the management system should take in response to container lifecycle events. + Cannot be updated. + properties: + postStart: + description: |- + PostStart is called immediately after a container is created. If the handler fails, + the container is terminated and restarted according to its restart policy. + Other management of the container blocks until the hook completes. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + sleep: + description: Sleep represents the duration that the + container should sleep before being terminated. + properties: + seconds: + description: Seconds is the number of seconds to + sleep. + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + description: |- + Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept + for the backward compatibility. There are no validation of this field and + lifecycle hooks will fail in runtime when tcp handler is specified. + properties: + host: + description: 'Optional: Host name to connect to, + defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: |- + PreStop is called immediately before a container is terminated due to an + API request or management event such as liveness/startup probe failure, + preemption, resource contention, etc. The handler is not called if the + container crashes or exits. The Pod's termination grace period countdown begins before the + PreStop hook is executed. Regardless of the outcome of the handler, the + container will eventually terminate within the Pod's termination grace + period (unless delayed by finalizers). Other management of the container blocks until the hook completes + or until the termination grace period is reached. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + sleep: + description: Sleep represents the duration that the + container should sleep before being terminated. + properties: + seconds: + description: Seconds is the number of seconds to + sleep. + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + description: |- + Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept + for the backward compatibility. There are no validation of this field and + lifecycle hooks will fail in runtime when tcp handler is specified. + properties: + host: + description: 'Optional: Host name to connect to, + defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + description: |- + Periodic probe of container liveness. + Container will be restarted if the probe fails. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP + port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + name: + description: |- + Name of the container specified as a DNS_LABEL. + Each container in a pod must have a unique name (DNS_LABEL). + Cannot be updated. + type: string + ports: + description: |- + 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. + Modifying this array with strategic merge patch may corrupt the data. + For more information See https://github.com/kubernetes/kubernetes/issues/108255. + Cannot be updated. + items: + description: ContainerPort represents a network port in a + single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + description: |- + Periodic probe of container service readiness. + Container will be removed from service endpoints if the probe fails. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP + port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + resizePolicy: + description: Resources resize policy for the container. + items: + description: ContainerResizePolicy represents resource resize + policy for the container. + properties: + resourceName: + description: |- + Name of the resource to which this resource resize policy applies. + Supported values: cpu, memory. + type: string + restartPolicy: + description: |- + Restart policy to apply when specified resource is resized. + If not specified, it defaults to NotRequired. + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + description: |- + Compute Resources required by this container. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + restartPolicy: + description: |- + RestartPolicy defines the restart behavior of individual containers in a pod. + This field may only be set for init containers, and the only allowed value is "Always". + For non-init containers or when this field is not specified, + the restart behavior is defined by the Pod's restart policy and the container type. + Setting the RestartPolicy as "Always" for the init container will have the following effect: + this init container will be continually restarted on + exit until all regular containers have terminated. Once all regular + containers have completed, all init containers with restartPolicy "Always" + will be shut down. This lifecycle differs from normal init containers and + is often referred to as a "sidecar" container. Although this init + container still starts in the init container sequence, it does not wait + for the container to complete before proceeding to the next init + container. Instead, the next init container starts immediately after this + init container is started, or after any startupProbe has successfully + completed. + type: string + securityContext: + description: |- + SecurityContext defines the security options the container should be run with. + If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. + More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + appArmorProfile: + description: |- + appArmorProfile is the AppArmor options to use by this container. If set, this profile + overrides the pod's appArmorProfile. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile loaded on the node that should be used. + The profile must be preconfigured on the node to work. + Must match the loaded name of the profile. + Must be set if and only if type is "Localhost". + type: string + type: + description: |- + type indicates which kind of AppArmor profile will be applied. + Valid options are: + Localhost - a profile pre-loaded on the node. + RuntimeDefault - the container runtime's default profile. + Unconfined - no AppArmor enforcement. + type: string + required: + - type + type: object + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + startupProbe: + description: |- + 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. + This cannot be updated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number + must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header + to be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP + port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + stdin: + description: |- + Whether this container should allocate a buffer for stdin in the container runtime. If this + is not set, reads from stdin in the container will always result in EOF. + Default is false. + type: boolean + stdinOnce: + description: |- + Whether the container runtime should close the stdin channel after it has been opened by + a single attach. When stdin is true the stdin stream will remain open across multiple attach + sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the + first client attaches to stdin, and then remains open and accepts data until the client disconnects, + at which time stdin is closed and remains closed until the container is restarted. If this + flag is false, a container processes that reads from stdin will never receive an EOF. + Default is false + type: boolean + terminationMessagePath: + description: |- + Optional: Path at which the file to which the container's termination message + will be written is mounted into the container's filesystem. + Message written is intended to be brief final status, such as an assertion failure message. + Will be truncated by the node if greater than 4096 bytes. The total message length across + all containers will be limited to 12kb. + Defaults to /dev/termination-log. + Cannot be updated. + type: string + terminationMessagePolicy: + description: |- + Indicate how the termination message should be populated. File will use the contents of + terminationMessagePath to populate the container status message on both success and failure. + FallbackToLogsOnError will use the last chunk of container log output if the termination + message file is empty and the container exited with an error. + The log output is limited to 2048 bytes or 80 lines, whichever is smaller. + Defaults to File. + Cannot be updated. + type: string + tty: + description: |- + Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. + Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of block devices to be + used by the container. + items: + description: volumeDevice describes a mapping of a raw block + device within a container. + properties: + devicePath: + description: devicePath is the path inside of the container + that the device will be mapped to. + type: string + name: + description: name must match the name of a persistentVolumeClaim + in the pod + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + description: |- + Pod volumes to mount into the container's filesystem. + Cannot be updated. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + + If ReadOnly is false, this field has no meaning and must be unspecified. + + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + description: |- + Container's working directory. + If not specified, the container runtime's default will be used, which + might be configured in the container image. + Cannot be updated. + type: string + required: + - name + type: object + type: array labels: additionalProperties: type: string diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 6a23343..3c969b7 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ kind: Kustomization images: - name: controller newName: docker.dragonflydb.io/dragonflydb/operator - newTag: v1.1.7 + newTag: v1.1.8 diff --git a/e2e/dragonfly_controller_test.go b/e2e/dragonfly_controller_test.go index 4b8d22f..84eda5e 100644 --- a/e2e/dragonfly_controller_test.go +++ b/e2e/dragonfly_controller_test.go @@ -305,7 +305,7 @@ var _ = Describe("Dragonfly Lifecycle tests", Ordered, FlakeAttempts(3), func() }, &df) Expect(err).To(BeNil()) - df.Spec.Image = fmt.Sprintf("%s:%s", resources.DragonflyImage, "v1.21.2") + df.Spec.Image = fmt.Sprintf("%s:%s", resources.DragonflyImage, "v1.24.0") err = k8sClient.Update(ctx, &df) Expect(err).To(BeNil()) }) @@ -418,11 +418,13 @@ var _ = Describe("Dragonfly Lifecycle tests", Ordered, FlakeAttempts(3), func() err = k8sClient.Update(ctx, &df) Expect(err).To(BeNil()) + GinkgoLogr.Info("start timestamp", "timestamp", time.Now().UTC()) // Wait until Dragonfly object is marked ready err = waitForDragonflyPhase(ctx, k8sClient, name, namespace, controller.PhaseReady, 3*time.Minute) Expect(err).To(BeNil()) err = waitForStatefulSetReady(ctx, k8sClient, name, namespace, 3*time.Minute) Expect(err).To(BeNil()) + GinkgoLogr.Info("end timestamp", "timestamp", time.Now().UTC()) // Check for service and statefulset var ss appsv1.StatefulSet @@ -478,10 +480,19 @@ var _ = Describe("Dragonfly Lifecycle tests", Ordered, FlakeAttempts(3), func() // check for affinity Expect(pod.Spec.Affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution).To(Equal(newAffinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution)) } + // Update df to the latest + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: name, + Namespace: namespace, + }, &df) + Expect(err).To(BeNil()) + GinkgoLogr.Info("df arg propagate phase", "phase", df.Status.Phase, "rolling-update", df.Status.IsRollingUpdate) + }) It("Check for data", func() { stopChan := make(chan struct{}, 1) + defer close(stopChan) rc, err := checkAndK8sPortForwardRedis(ctx, clientset, cfg, stopChan, name, namespace, password, 6395) Expect(err).To(BeNil()) @@ -489,7 +500,6 @@ var _ = Describe("Dragonfly Lifecycle tests", Ordered, FlakeAttempts(3), func() data, err := rc.Get(ctx, "foo").Result() Expect(err).To(BeNil()) Expect(data).To(Equal("bar")) - defer close(stopChan) }) It("Change Service specification to LoadBalancer", func() { @@ -512,11 +522,12 @@ var _ = Describe("Dragonfly Lifecycle tests", Ordered, FlakeAttempts(3), func() Labels: newLabels, } + GinkgoLogr.Info("df phase", "phase", df.Status.Phase, "rolling-update", df.Status.IsRollingUpdate) err = k8sClient.Update(ctx, &df) Expect(err).To(BeNil()) // Wait until Dragonfly object is marked ready - err = waitForDragonflyPhase(ctx, k8sClient, name, namespace, controller.PhaseReady, 3*time.Minute) + err = waitForDragonflyPhase(ctx, k8sClient, name, namespace, controller.PhaseReady, 1*time.Minute) Expect(err).To(BeNil()) err = waitForStatefulSetReady(ctx, k8sClient, name, namespace, 3*time.Minute) Expect(err).To(BeNil()) @@ -533,6 +544,19 @@ var _ = Describe("Dragonfly Lifecycle tests", Ordered, FlakeAttempts(3), func() Expect(svc.Labels).To(Equal(newLabels)) }) + It("Should recreate missing statefulset", func() { + var ss appsv1.StatefulSet + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: name, + Namespace: namespace, + }, &ss) + Expect(err).To(BeNil()) + + Expect(k8sClient.Delete(ctx, &ss)).To(BeNil()) + err = waitForStatefulSetReady(ctx, k8sClient, name, namespace, 2*time.Minute) + Expect(err).To(BeNil()) + }) + It("Cleanup", func() { var df resourcesv1.Dragonfly err := k8sClient.Get(ctx, types.NamespacedName{ @@ -613,8 +637,8 @@ user john on #0c8e2b662f1c0f1 -@all +@string +hset Expect(err).To(BeNil()) Expect(result).To(HaveLen(2)) Expect(result).To(ContainElements( - "user default on nopass ~* +@all", - "user john on #0c8e2b662f1c0f -@all +@string +hset", + "user default on nopass ~* resetchannels +@all", + "user john on #0c8e2b662f1c0f resetchannels -@all +@string +hset", )) }) It("Cleanup", func() { @@ -919,6 +943,8 @@ func isDragonflyInphase(ctx context.Context, c client.Client, name, namespace, p return false, nil } + GinkgoLogr.Info("dragonfly phase", "phase", df.Status.Phase, "update", df.Status.IsRollingUpdate) + // Ready means we also want rolling update to be false if phase == controller.PhaseReady { // check for replicas diff --git a/go.mod b/go.mod index 71793d7..e2b04ab 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/onsi/gomega v1.33.1 github.com/pkg/errors v0.9.1 github.com/redis/go-redis/v9 v9.5.3 + github.com/samber/lo v1.47.0 k8s.io/api v0.30.2 k8s.io/apimachinery v0.30.2 k8s.io/client-go v0.30.2 diff --git a/go.sum b/go.sum index 77c6f00..e602756 100644 --- a/go.sum +++ b/go.sum @@ -98,6 +98,8 @@ github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRci github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/internal/controller/dragonfly_controller.go b/internal/controller/dragonfly_controller.go index 496af1a..9ec7ad4 100644 --- a/internal/controller/dragonfly_controller.go +++ b/internal/controller/dragonfly_controller.go @@ -25,6 +25,7 @@ import ( "github.com/dragonflydb/dragonfly-operator/internal/resources" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" @@ -91,7 +92,49 @@ func (r *DragonflyReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( r.EventRecorder.Event(&df, corev1.EventTypeNormal, "Resources", "Created resources") return ctrl.Result{}, nil - } else if df.Status.IsRollingUpdate { + } + + // Ensure all resources exist before moving forward. + missingResources, err := r.getMissingResources(ctx, &df) + if err != nil { + log.Error(err, "could not get resources") + return ctrl.Result{}, err + } + for _, resource := range missingResources { + // recreate missing resources + if err := r.Create(ctx, resource); err != nil { + log.Error(err, fmt.Sprintf("could not create resource %s/%s/%s", resource.GetObjectKind(), resource.GetNamespace(), resource.GetName())) + return ctrl.Result{}, err + } + } + + var statefulSet appsv1.StatefulSet + if err := r.Get(ctx, client.ObjectKey{Namespace: df.Namespace, Name: df.Name}, &statefulSet); err != nil { + log.Error(err, "could not get statefulset") + return ctrl.Result{}, err + } + + // Update all resources even if the df is in rollout state to ensure + // that newer updates don't get blocked by failed update attempts. + log.Info("updating existing resources") + newResources, err := resources.GetDragonflyResources(ctx, &df) + if err != nil { + log.Error(err, "could not get resources") + return ctrl.Result{}, err + } + + // update all resources + for _, resource := range newResources { + if err := r.Update(ctx, resource); err != nil { + log.Error(err, fmt.Sprintf("could not update resource %s/%s/%s", resource.GetObjectKind(), resource.GetNamespace(), resource.GetName())) + return ctrl.Result{}, err + } + } + + log.Info("Updated resources for object") + r.EventRecorder.Event(&df, corev1.EventTypeNormal, "Resources", "Updated resources") + + if df.Status.IsRollingUpdate { // This is a Rollout log.Info("Rolling out new version") var updatedStatefulset appsv1.StatefulSet @@ -127,6 +170,14 @@ func (r *DragonflyReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( } } else { log.Info("found pod without label", "pod", pod.Name) + if isFailedToStart(&pod) { + // This is a new pod which is trying to be ready, but couldn't start due to misconfig. + // Delete the pod and create a new one. + if err := r.Delete(ctx, &pod); err != nil { + log.Error(err, "could not delete pod") + return ctrl.Result{RequeueAfter: 5 * time.Second}, err + } + } // retry after they are ready return ctrl.Result{RequeueAfter: 5 * time.Second}, nil } @@ -234,54 +285,65 @@ func (r *DragonflyReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( } return ctrl.Result{}, nil - } else { + } else if statefulSet.Status.UpdatedReplicas != statefulSet.Status.Replicas { // perform a rollout only if the pod spec has changed - var statefulSet appsv1.StatefulSet - if err := r.Get(ctx, client.ObjectKey{Namespace: df.Namespace, Name: df.Name}, &statefulSet); err != nil { - log.Error(err, "could not get statefulset") - return ctrl.Result{}, err - } - // Check if the pod spec has changed log.Info("Checking if pod spec has changed", "updatedReplicas", statefulSet.Status.UpdatedReplicas, "currentReplicas", statefulSet.Status.Replicas) - if statefulSet.Status.UpdatedReplicas != statefulSet.Status.Replicas { - log.Info("Pod spec has changed, performing a rollout") - r.EventRecorder.Event(&df, corev1.EventTypeNormal, "Rollout", "Starting a rollout") - - // Start rollout and update status - // update status so that we can track progress - df.Status.IsRollingUpdate = true - if err := r.Status().Update(ctx, &df); err != nil { - log.Error(err, "could not update the Dragonfly object") - return ctrl.Result{Requeue: true}, err - } - - r.EventRecorder.Event(&df, corev1.EventTypeNormal, "Resources", "Performing a rollout") + log.Info("Pod spec has changed, performing a rollout") + r.EventRecorder.Event(&df, corev1.EventTypeNormal, "Rollout", "Starting a rollout") - // requeue so that the rollout is processed - return ctrl.Result{RequeueAfter: 5 * time.Second}, nil + // Start rollout and update status + // update status so that we can track progress + df.Status.IsRollingUpdate = true + if err := r.Status().Update(ctx, &df); err != nil { + log.Error(err, "could not update the Dragonfly object") + return ctrl.Result{Requeue: true}, err } - // Is this a Dragonfly object update? - log.Info("updating existing resources") - newResources, err := resources.GetDragonflyResources(ctx, &df) - if err != nil { - log.Error(err, "could not get resources") - return ctrl.Result{}, err - } + r.EventRecorder.Event(&df, corev1.EventTypeNormal, "Resources", "Performing a rollout") + } + return ctrl.Result{Requeue: true}, nil +} - // update all resources - for _, resource := range newResources { - if err := r.Update(ctx, resource); err != nil { - log.Error(err, fmt.Sprintf("could not update resource %s/%s/%s", resource.GetObjectKind(), resource.GetNamespace(), resource.GetName())) - return ctrl.Result{}, err - } +func isFailedToStart(pod *corev1.Pod) bool { + for _, containerStatus := range pod.Status.ContainerStatuses { + if (containerStatus.State.Waiting != nil && isFailureReason(containerStatus.State.Waiting.Reason)) || + (containerStatus.State.Terminated != nil && isFailureReason(containerStatus.State.Terminated.Reason)) { + return true } + } + return false +} + +// isFailureReason checks if the given reason indicates a failure. +func isFailureReason(reason string) bool { + return reason == "ErrImagePull" || + reason == "ImagePullBackOff" || + reason == "CrashLoopBackOff" || + reason == "RunContainerError" +} - log.Info("Updated resources for object") - r.EventRecorder.Event(&df, corev1.EventTypeNormal, "Resources", "Updated resources") - return ctrl.Result{Requeue: true}, nil +func (r *DragonflyReconciler) getMissingResources(ctx context.Context, df *dfv1alpha1.Dragonfly) ([]client.Object, error) { + resources, err := resources.GetDragonflyResources(ctx, df) + if err != nil { + return nil, err + } + missingResources := make([]client.Object, 0) + for _, resource := range resources { + obj := resource.DeepCopyObject().(client.Object) + + err := r.Get(ctx, client.ObjectKey{ + Namespace: df.Namespace, + Name: resource.GetName(), + }, obj) + + if errors.IsNotFound(err) { + missingResources = append(missingResources, resource) + } else if err != nil { + return nil, fmt.Errorf("failed to get resource %s/%s: %w", df.Namespace, resource.GetName(), err) + } } + return missingResources, nil } // SetupWithManager sets up the controller with the Manager. @@ -289,7 +351,8 @@ func (r *DragonflyReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). // Listen only to spec changes For(&dfv1alpha1.Dragonfly{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). - Owns(&appsv1.StatefulSet{}). - Owns(&corev1.Service{}). + Owns(&appsv1.StatefulSet{}, builder.MatchEveryOwner). + Owns(&corev1.Service{}, builder.MatchEveryOwner). + Named("Dragonfly"). Complete(r) } diff --git a/internal/controller/dragonfly_instance.go b/internal/controller/dragonfly_instance.go index c094d4d..0e2b143 100644 --- a/internal/controller/dragonfly_instance.go +++ b/internal/controller/dragonfly_instance.go @@ -20,6 +20,8 @@ import ( "context" "errors" "fmt" + "net" + "strconv" "strings" dfv1alpha1 "github.com/dragonflydb/dragonfly-operator/api/v1alpha1" @@ -329,7 +331,7 @@ func (dfi *DragonflyInstance) getPods(ctx context.Context) (*corev1.PodList, err // to the given master instance func (dfi *DragonflyInstance) replicaOf(ctx context.Context, pod *corev1.Pod, masterIp string) error { redisClient := redis.NewClient(&redis.Options{ - Addr: fmt.Sprintf("%s:%d", pod.Status.PodIP, resources.DragonflyAdminPort), + Addr: net.JoinHostPort(pod.Status.PodIP, strconv.Itoa(resources.DragonflyAdminPort)), }) defer redisClient.Close() @@ -357,7 +359,7 @@ func (dfi *DragonflyInstance) replicaOf(ctx context.Context, pod *corev1.Pod, ma // along while updating other pods to be replicas func (dfi *DragonflyInstance) replicaOfNoOne(ctx context.Context, pod *corev1.Pod) error { redisClient := redis.NewClient(&redis.Options{ - Addr: fmt.Sprintf("%s:%d", pod.Status.PodIP, resources.DragonflyAdminPort), + Addr: net.JoinHostPort(pod.Status.PodIP, strconv.Itoa(resources.DragonflyAdminPort)), }) defer redisClient.Close() diff --git a/internal/controller/dragonfly_pod_lifecycle_controller.go b/internal/controller/dragonfly_pod_lifecycle_controller.go index 4c9cb11..6c622a7 100644 --- a/internal/controller/dragonfly_pod_lifecycle_controller.go +++ b/internal/controller/dragonfly_pod_lifecycle_controller.go @@ -204,6 +204,7 @@ func (r *DfPodLifeCycleReconciler) SetupWithManager(mgr ctrl.Manager) error { return e.Object.GetLabels()[resources.KubernetesAppNameLabelKey] == "dragonfly" }, }). + Named("DragonflyPodLifecycle"). For(&corev1.Pod{}). Complete(r) } diff --git a/internal/resources/resources.go b/internal/resources/resources.go index 7fbc4fd..8f1fadb 100644 --- a/internal/resources/resources.go +++ b/internal/resources/resources.go @@ -156,6 +156,10 @@ func GetDragonflyResources(ctx context.Context, df *resourcesv1.Dragonfly) ([]cl }, } + if len(df.Spec.InitContainers) > 0 { + statefulset.Spec.Template.Spec.InitContainers = df.Spec.InitContainers + } + // Skip Assigning FileSystem Group. Required for platforms such as Openshift that require IDs to not be set, as it injects a fixed randomized ID per namespace into all pods. // Skip Assigning FileSystem Group if podSecurityContext is set as well. if !df.Spec.SkipFSGroup && df.Spec.PodSecurityContext == nil { @@ -184,6 +188,10 @@ func GetDragonflyResources(ctx context.Context, df *resourcesv1.Dragonfly) ([]cl } if df.Spec.MemcachedPort != 0 { statefulset.Spec.Template.Spec.Containers[0].Args = append(statefulset.Spec.Template.Spec.Containers[0].Args, fmt.Sprintf("--memcached_port=%d", df.Spec.MemcachedPort)) + statefulset.Spec.Template.Spec.Containers[0].Ports = append(statefulset.Spec.Template.Spec.Containers[0].Ports, corev1.ContainerPort{ + Name: "memcached", + ContainerPort: df.Spec.MemcachedPort, + }) } if df.Spec.AclFromSecret != nil { diff --git a/internal/resources/version.go b/internal/resources/version.go index 623d183..91d65fc 100644 --- a/internal/resources/version.go +++ b/internal/resources/version.go @@ -17,5 +17,5 @@ limitations under the License. package resources const ( - Version = "v1.21.2" + Version = "v1.25.5" ) diff --git a/manifests/dragonfly-operator.yaml b/manifests/dragonfly-operator.yaml index 29a7286..6cf779a 100644 --- a/manifests/dragonfly-operator.yaml +++ b/manifests/dragonfly-operator.yaml @@ -2100,7 +2100,7 @@ spec: - --leader-elect command: - /manager - image: docker.dragonflydb.io/dragonflydb/operator:v1.1.7 + image: docker.dragonflydb.io/dragonflydb/operator:v1.1.8 livenessProbe: httpGet: path: /healthz diff --git a/monitoring/grafana-dashboard.json b/monitoring/grafana-dashboard.json index 6c33de4..1ec51c2 100644 --- a/monitoring/grafana-dashboard.json +++ b/monitoring/grafana-dashboard.json @@ -162,7 +162,7 @@ "uid": "${DS_PROMETHEUS}" }, "exemplar": true, - "expr": "max(max_over_time(dragonfly_uptime_in_seconds{pod=~\"$instance\"}[$__interval]))", + "expr": "max(max_over_time(dragonfly_uptime_in_seconds{namespace=\"$namespace\",pod=~\"$instance\"}[$__interval]))", "format": "time_series", "interval": "", "intervalFactor": 2, @@ -248,7 +248,7 @@ "uid": "${DS_PROMETHEUS}" }, "exemplar": true, - "expr": "dragonfly_connected_clients{pod=~\"$instance\"}", + "expr": "dragonfly_connected_clients{namespace=\"$namespace\",pod=~\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 2, @@ -339,7 +339,7 @@ "uid": "${DS_PROMETHEUS}" }, "exemplar": true, - "expr": "100 * (dragonfly_memory_used_bytes{pod=~\"$instance\"} / dragonfly_memory_max_bytes{pod=~\"$instance\"} )", + "expr": "100 * (dragonfly_memory_used_bytes{namespace=\"$namespace\",pod=~\"$instance\"} / dragonfly_memory_max_bytes{namespace=\"$namespace\",pod=~\"$instance\"} )", "format": "time_series", "interval": "", "intervalFactor": 2, @@ -411,7 +411,7 @@ "disableTextWrap": false, "editorMode": "code", "exemplar": false, - "expr": "dragonfly_master{pod=\"$instance\"}", + "expr": "dragonfly_master{namespace=\"$namespace\",pod=\"$instance\"}", "fullMetaSearch": false, "includeNullMetadata": true, "instant": true, @@ -486,7 +486,7 @@ }, "editorMode": "code", "exemplar": true, - "expr": "rate(dragonfly_commands_processed_total{pod=~\"$instance\"}[5m])", + "expr": "rate(dragonfly_commands_processed_total{namespace=\"$namespace\",pod=~\"$instance\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 2, @@ -595,7 +595,7 @@ "uid": "${DS_PROMETHEUS}" }, "exemplar": true, - "expr": "dragonfly_memory_used_bytes{pod=~\"$instance\"} ", + "expr": "dragonfly_memory_used_bytes{namespace=\"$namespace\",pod=~\"$instance\"} ", "format": "time_series", "interval": "", "intervalFactor": 2, @@ -611,7 +611,7 @@ "uid": "${DS_PROMETHEUS}" }, "exemplar": true, - "expr": "dragonfly_memory_max_bytes{pod=~\"$instance\"} ", + "expr": "dragonfly_memory_max_bytes{namespace=\"$namespace\",pod=~\"$instance\"} ", "format": "time_series", "hide": false, "interval": "", @@ -713,7 +713,7 @@ "uid": "${DS_PROMETHEUS}" }, "exemplar": true, - "expr": "rate(dragonfly_net_input_bytes_total{pod=~\"$instance\"}[5m])", + "expr": "rate(dragonfly_net_input_bytes_total{namespace=\"$namespace\",pod=~\"$instance\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 2, @@ -727,7 +727,7 @@ "uid": "${DS_PROMETHEUS}" }, "exemplar": true, - "expr": "rate(dragonfly_net_output_bytes_total{pod=~\"$instance\"}[5m])", + "expr": "rate(dragonfly_net_output_bytes_total{namespace=\"$namespace\",pod=~\"$instance\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 2, @@ -829,7 +829,7 @@ "uid": "${DS_PROMETHEUS}" }, "exemplar": true, - "expr": "sum (dragonfly_db_keys{pod=~\"$instance\"}) by (db)", + "expr": "sum (dragonfly_db_keys{namespace=\"$namespace\",pod=~\"$instance\"}) by (db)", "format": "time_series", "interval": "", "intervalFactor": 2, @@ -930,7 +930,7 @@ "uid": "${DS_PROMETHEUS}" }, "exemplar": true, - "expr": "sum (dragonfly_db_keys{pod=~\"$instance\"}) - sum (dragonfly_db_keys_expiring{pod=~\"$instance\"}) ", + "expr": "sum (dragonfly_db_keys{namespace=\"$namespace\",pod=~\"$instance\"}) - sum (dragonfly_db_keys_expiring{namespace=\"$namespace\",pod=~\"$instance\"}) ", "format": "time_series", "interval": "", "intervalFactor": 2, @@ -945,7 +945,7 @@ "uid": "${DS_PROMETHEUS}" }, "exemplar": true, - "expr": "sum (dragonfly_db_keys_expiring{pod=~\"$instance\"})", + "expr": "sum (dragonfly_db_keys_expiring{namespace=\"$namespace\",pod=~\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 2, @@ -1071,7 +1071,7 @@ }, "disableTextWrap": false, "editorMode": "code", - "expr": "irate(dragonfly_connections_received_total{pod=\"$instance\"}[$__rate_interval])", + "expr": "irate(dragonfly_connections_received_total{namespace=\"$namespace\",pod=\"$instance\"}[$__rate_interval])", "fullMetaSearch": false, "includeNullMetadata": false, "instant": false, @@ -1142,7 +1142,7 @@ "uid": "${DS_PROMETHEUS}" }, "exemplar": true, - "expr": "dragonfly_connected_clients{pod=\"$instance\"}", + "expr": "dragonfly_connected_clients{namespace=\"$namespace\",pod=\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 1, @@ -1243,7 +1243,7 @@ }, "editorMode": "code", "exemplar": true, - "expr": "rate(dragonfly_keyspace_hits_total{pod=~\"$instance\"}[5m])", + "expr": "rate(dragonfly_keyspace_hits_total{namespace=\"$namespace\",pod=~\"$instance\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 2, @@ -1260,7 +1260,7 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "rate(dragonfly_keyspace_misses_total{pod=~\"$instance\"}[5m])", + "expr": "rate(dragonfly_keyspace_misses_total{namespace=\"$namespace\",pod=~\"$instance\"}[5m])", "hide": false, "instant": false, "legendFormat": "misses", @@ -1376,7 +1376,7 @@ "uid": "${DS_PROMETHEUS}" }, "exemplar": true, - "expr": "sum(rate(dragonfly_expired_keys_total{pod=~\"$instance\"}[5m])) by (instance)", + "expr": "sum(rate(dragonfly_expired_keys_total{namespace=\"$namespace\",pod=~\"$instance\"}[5m])) by (instance)", "format": "time_series", "hide": false, "interval": "", @@ -1393,7 +1393,7 @@ "uid": "${DS_PROMETHEUS}" }, "exemplar": true, - "expr": "sum(rate(dragonfly_evicted_keys_total{pod=~\"$instance\"}[5m])) by (instance)", + "expr": "sum(rate(dragonfly_evicted_keys_total{namespace=\"$namespace\",pod=~\"$instance\"}[5m])) by (instance)", "format": "time_series", "interval": "", "intervalFactor": 2, @@ -1442,49 +1442,78 @@ ], "templating": { "list": [ - { - "current": {}, - "definition": "label_values(dragonfly_uptime_in_seconds,app)", - "hide": 0, - "includeAll": false, - "label": "", - "multi": false, - "name": "Dragonfly", - "options": [], - "query": { - "query": "label_values(dragonfly_uptime_in_seconds,app)", - "refId": "PrometheusVariableQueryEditor-VariableQuery" - }, - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "sort": 0, - "type": "query" + "hide": 2, + "name": "DS_PROMETHEUS", + "query": "prometheus", + "skipUrlSync": true, + "type": "constant" }, { - "current": {}, - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "definition": "label_values(dragonfly_uptime_in_seconds{app=\"$Dragonfly\"},pod)", - "hide": 0, - "includeAll": false, - "label": "instance", - "multi": false, - "name": "instance", - "options": [], - "query": { - "query": "label_values(dragonfly_uptime_in_seconds{app=\"$Dragonfly\"},pod)", - "refId": "PrometheusVariableQueryEditor-VariableQuery" - }, - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "sort": 0, - "type": "query" - } + "current": {}, + "definition": "label_values(dragonfly_uptime_in_seconds,app)", + "hide": 0, + "includeAll": false, + "label": "", + "multi": false, + "name": "Dragonfly", + "options": [], + "query": { + "query": "label_values(dragonfly_uptime_in_seconds,app)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values(dragonfly_uptime_in_seconds{app=\"$Dragonfly\"},namespace)", + "hide": 0, + "includeAll": false, + "label": "namespace", + "multi": false, + "name": "namespace", + "options": [], + "query": { + "query": "label_values(dragonfly_uptime_in_seconds{app=\"$Dragonfly\"},namespace)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values(dragonfly_uptime_in_seconds{app=\"$Dragonfly\",namespace=\"$namespace\"},pod)", + "hide": 0, + "includeAll": false, + "label": "instance", + "multi": false, + "name": "instance", + "options": [], + "query": { + "query": "label_values(dragonfly_uptime_in_seconds{app=\"$Dragonfly\",namespace=\"$namespace\"},pod)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } ] }, "time": {