diff --git a/apis/workloads/v1/instanceset_types.go b/apis/workloads/v1/instanceset_types.go index 6aa18360416..269bbb3e3af 100644 --- a/apis/workloads/v1/instanceset_types.go +++ b/apis/workloads/v1/instanceset_types.go @@ -524,6 +524,23 @@ type LifecycleActions struct { // +optional Switchover *Action `json:"switchover,omitempty"` + // Defines the procedure to add a new replica into membership. + // + // +optional + MemberJoin *Action `json:"memberJoin,omitempty"` + + // Defines the procedure to remove a replica from membership. + // + // +optional + MemberLeave *Action `json:"memberLeave,omitempty"` + + // Defines the procedure for importing data into a replica. + // InstanceSet only orchestrates the target replica side of this action. + // Any source selection, dump, or streaming protocol remains the responsibility of the action implementation itself. + // + // +optional + DataLoad *Action `json:"dataLoad,omitempty"` + // Defines the procedure that update replicas with new configuration. // // +optional @@ -579,6 +596,21 @@ type InstanceStatus struct { // +optional Configs []InstanceConfigStatus `json:"configs,omitempty"` + // Represents whether the instance is provisioned. + // + // +optional + Provisioned bool `json:"provisioned,omitempty"` + + // Represents whether the instance data is loaded. + // + // +optional + DataLoaded *bool `json:"dataLoaded,omitempty"` + + // Represents whether the instance has joined the cluster membership. + // + // +optional + MemberJoined *bool `json:"memberJoined,omitempty"` + // Represents whether the instance is in volume expansion. // // +optional @@ -703,7 +735,7 @@ func (r *InstanceSet) IsInstanceSetReady() bool { if !instancesReady { return false } - return r.IsRoleProbeDone() + return r.IsRoleProbeDone() && r.IsLifecycleReady() } func (r *InstanceSet) IsRoleProbeDone() bool { @@ -719,3 +751,15 @@ func (r *InstanceSet) IsRoleProbeDone() bool { } return cnt == replicas } + +func (r *InstanceSet) IsLifecycleReady() bool { + for _, inst := range r.Status.InstanceStatus { + if inst.DataLoaded != nil && !*inst.DataLoaded { + return false + } + if inst.MemberJoined != nil && !*inst.MemberJoined { + return false + } + } + return true +} diff --git a/apis/workloads/v1/zz_generated.deepcopy.go b/apis/workloads/v1/zz_generated.deepcopy.go index 844137509df..ed1eafeefd7 100644 --- a/apis/workloads/v1/zz_generated.deepcopy.go +++ b/apis/workloads/v1/zz_generated.deepcopy.go @@ -482,6 +482,16 @@ func (in *InstanceStatus) DeepCopyInto(out *InstanceStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.DataLoaded != nil { + in, out := &in.DataLoaded, &out.DataLoaded + *out = new(bool) + **out = **in + } + if in.MemberJoined != nil { + in, out := &in.MemberJoined, &out.MemberJoined + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceStatus. @@ -612,6 +622,21 @@ func (in *LifecycleActions) DeepCopyInto(out *LifecycleActions) { *out = new(appsv1.Action) (*in).DeepCopyInto(*out) } + if in.MemberJoin != nil { + in, out := &in.MemberJoin, &out.MemberJoin + *out = new(appsv1.Action) + (*in).DeepCopyInto(*out) + } + if in.MemberLeave != nil { + in, out := &in.MemberLeave, &out.MemberLeave + *out = new(appsv1.Action) + (*in).DeepCopyInto(*out) + } + if in.DataLoad != nil { + in, out := &in.DataLoad, &out.DataLoad + *out = new(appsv1.Action) + (*in).DeepCopyInto(*out) + } if in.Reconfigure != nil { in, out := &in.Reconfigure, &out.Reconfigure *out = new(appsv1.Action) diff --git a/config/crd/bases/workloads.kubeblocks.io_instances.yaml b/config/crd/bases/workloads.kubeblocks.io_instances.yaml index e9699057a78..fbb98118e2e 100644 --- a/config/crd/bases/workloads.kubeblocks.io_instances.yaml +++ b/config/crd/bases/workloads.kubeblocks.io_instances.yaml @@ -1095,6 +1095,1305 @@ spec: description: Defines a set of hooks that customize the behavior of an Instance throughout its lifecycle. properties: + dataLoad: + description: |- + Defines the procedure for importing data into a replica. + InstanceSet only orchestrates the target replica side of this action. + Any source selection, dump, or streaming protocol remains the responsibility of the action implementation itself. + properties: + exec: + description: |- + Defines the command to run. + + + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. + + + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Specifies the name of the container within the same pod whose resources will be shared with the action. + This allows the action to utilize the specified container's resources without executing within it. + + + The name must match one of the containers defined in `componentDefinition.spec.runtime`. + + + The resources that can be shared are included: + + + - volume mounts + + + This field cannot be updated. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. + + + This field 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: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. + + + When specified, a dedicated container will be created using this image to execute the Action. + All actions with same image will share the same container. + + + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + grpc: + description: |- + Defines the gRPC call to issue. + + + This field cannot be updated. + properties: + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + description: Name of the method to invoke on the gRPC + service. + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "50051") or a named port defined in the container spec. + type: string + request: + additionalProperties: + type: string + description: |- + Request payload for the gRPC method. + + + Keys are proto field names (lowerCamelCase); values are strings that can include Go templates. + Templates are rendered with predefined action variables before the request is sent. + type: object + response: + description: Required response schema for the gRPC method. + properties: + message: + description: |- + Name of the field in the response whose value should be output. + Printed to stdout on success, or stderr on failure. + type: string + status: + description: |- + Name of the string field in the response that carries status information. + If non-empty, the action fails. + type: string + type: object + service: + description: Fully-qualified name of the gRPC service + to call. + type: string + required: + - method + - port + - service + type: object + http: + description: |- + Defines the HTTP request to perform. + + + This field cannot be updated. + properties: + body: + description: |- + Optional HTTP request body. + + + Supports Go text/template syntax; rendered with predefined variables before sending. + type: string + headers: + description: |- + Custom headers to set in the request. + Header values may use Go text/template syntax, rendered with predefined variables. + items: + description: HTTPHeader represents a single HTTP header + key/value pair. + properties: + name: + description: Name of the header field. + type: string + value: + description: Value of the header field. + type: string + required: + - name + - value + type: object + type: array + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + default: GET + description: |- + The HTTP method to use. + Defaults to "GET". + enum: + - GET + - POST + - PUT + - DELETE + - HEAD + - PATCH + type: string + path: + default: / + description: |- + The path to request on the HTTP server. + Defaults to "/" if not specified. + pattern: ^/.* + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "8080") or a named port defined in the container spec. + type: string + scheme: + default: HTTP + description: |- + The scheme to use for connecting to the host. + Defaults to "HTTP". + enum: + - HTTP + - HTTPS + type: string + required: + - port + type: object + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. + + + The conditions are as follows: + + + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. + + + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. + + + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. + + + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + Behavior based on the value: + - Positive (> 0): The action will be terminated after this many seconds. The maximum allowed value is 60. + - Zero (= 0): The timeout is managed by the system, defaulting to 30 seconds typically. + - Negative (< 0): No timeout is applied; the action runs until the command completes. + + + This field cannot be updated. + format: int32 + type: integer + type: object + memberJoin: + description: Defines the procedure to add a new replica into membership. + properties: + exec: + description: |- + Defines the command to run. + + + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. + + + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Specifies the name of the container within the same pod whose resources will be shared with the action. + This allows the action to utilize the specified container's resources without executing within it. + + + The name must match one of the containers defined in `componentDefinition.spec.runtime`. + + + The resources that can be shared are included: + + + - volume mounts + + + This field cannot be updated. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. + + + This field 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: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. + + + When specified, a dedicated container will be created using this image to execute the Action. + All actions with same image will share the same container. + + + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + grpc: + description: |- + Defines the gRPC call to issue. + + + This field cannot be updated. + properties: + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + description: Name of the method to invoke on the gRPC + service. + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "50051") or a named port defined in the container spec. + type: string + request: + additionalProperties: + type: string + description: |- + Request payload for the gRPC method. + + + Keys are proto field names (lowerCamelCase); values are strings that can include Go templates. + Templates are rendered with predefined action variables before the request is sent. + type: object + response: + description: Required response schema for the gRPC method. + properties: + message: + description: |- + Name of the field in the response whose value should be output. + Printed to stdout on success, or stderr on failure. + type: string + status: + description: |- + Name of the string field in the response that carries status information. + If non-empty, the action fails. + type: string + type: object + service: + description: Fully-qualified name of the gRPC service + to call. + type: string + required: + - method + - port + - service + type: object + http: + description: |- + Defines the HTTP request to perform. + + + This field cannot be updated. + properties: + body: + description: |- + Optional HTTP request body. + + + Supports Go text/template syntax; rendered with predefined variables before sending. + type: string + headers: + description: |- + Custom headers to set in the request. + Header values may use Go text/template syntax, rendered with predefined variables. + items: + description: HTTPHeader represents a single HTTP header + key/value pair. + properties: + name: + description: Name of the header field. + type: string + value: + description: Value of the header field. + type: string + required: + - name + - value + type: object + type: array + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + default: GET + description: |- + The HTTP method to use. + Defaults to "GET". + enum: + - GET + - POST + - PUT + - DELETE + - HEAD + - PATCH + type: string + path: + default: / + description: |- + The path to request on the HTTP server. + Defaults to "/" if not specified. + pattern: ^/.* + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "8080") or a named port defined in the container spec. + type: string + scheme: + default: HTTP + description: |- + The scheme to use for connecting to the host. + Defaults to "HTTP". + enum: + - HTTP + - HTTPS + type: string + required: + - port + type: object + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. + + + The conditions are as follows: + + + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. + + + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. + + + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. + + + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + Behavior based on the value: + - Positive (> 0): The action will be terminated after this many seconds. The maximum allowed value is 60. + - Zero (= 0): The timeout is managed by the system, defaulting to 30 seconds typically. + - Negative (< 0): No timeout is applied; the action runs until the command completes. + + + This field cannot be updated. + format: int32 + type: integer + type: object + memberLeave: + description: Defines the procedure to remove a replica from membership. + properties: + exec: + description: |- + Defines the command to run. + + + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. + + + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Specifies the name of the container within the same pod whose resources will be shared with the action. + This allows the action to utilize the specified container's resources without executing within it. + + + The name must match one of the containers defined in `componentDefinition.spec.runtime`. + + + The resources that can be shared are included: + + + - volume mounts + + + This field cannot be updated. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. + + + This field 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: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. + + + When specified, a dedicated container will be created using this image to execute the Action. + All actions with same image will share the same container. + + + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + grpc: + description: |- + Defines the gRPC call to issue. + + + This field cannot be updated. + properties: + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + description: Name of the method to invoke on the gRPC + service. + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "50051") or a named port defined in the container spec. + type: string + request: + additionalProperties: + type: string + description: |- + Request payload for the gRPC method. + + + Keys are proto field names (lowerCamelCase); values are strings that can include Go templates. + Templates are rendered with predefined action variables before the request is sent. + type: object + response: + description: Required response schema for the gRPC method. + properties: + message: + description: |- + Name of the field in the response whose value should be output. + Printed to stdout on success, or stderr on failure. + type: string + status: + description: |- + Name of the string field in the response that carries status information. + If non-empty, the action fails. + type: string + type: object + service: + description: Fully-qualified name of the gRPC service + to call. + type: string + required: + - method + - port + - service + type: object + http: + description: |- + Defines the HTTP request to perform. + + + This field cannot be updated. + properties: + body: + description: |- + Optional HTTP request body. + + + Supports Go text/template syntax; rendered with predefined variables before sending. + type: string + headers: + description: |- + Custom headers to set in the request. + Header values may use Go text/template syntax, rendered with predefined variables. + items: + description: HTTPHeader represents a single HTTP header + key/value pair. + properties: + name: + description: Name of the header field. + type: string + value: + description: Value of the header field. + type: string + required: + - name + - value + type: object + type: array + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + default: GET + description: |- + The HTTP method to use. + Defaults to "GET". + enum: + - GET + - POST + - PUT + - DELETE + - HEAD + - PATCH + type: string + path: + default: / + description: |- + The path to request on the HTTP server. + Defaults to "/" if not specified. + pattern: ^/.* + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "8080") or a named port defined in the container spec. + type: string + scheme: + default: HTTP + description: |- + The scheme to use for connecting to the host. + Defaults to "HTTP". + enum: + - HTTP + - HTTPS + type: string + required: + - port + type: object + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. + + + The conditions are as follows: + + + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. + + + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. + + + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. + + + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + Behavior based on the value: + - Positive (> 0): The action will be terminated after this many seconds. The maximum allowed value is 60. + - Zero (= 0): The timeout is managed by the system, defaulting to 30 seconds typically. + - Negative (< 0): No timeout is applied; the action runs until the command completes. + + + This field cannot be updated. + format: int32 + type: integer + type: object reconfigure: description: Defines the procedure that update replicas with new configuration. diff --git a/config/crd/bases/workloads.kubeblocks.io_instancesets.yaml b/config/crd/bases/workloads.kubeblocks.io_instancesets.yaml index e646b43d848..c73826228b4 100644 --- a/config/crd/bases/workloads.kubeblocks.io_instancesets.yaml +++ b/config/crd/bases/workloads.kubeblocks.io_instancesets.yaml @@ -2534,6 +2534,1305 @@ spec: description: Defines a set of hooks that customize the behavior of an Instance throughout its lifecycle. properties: + dataLoad: + description: |- + Defines the procedure for importing data into a replica. + InstanceSet only orchestrates the target replica side of this action. + Any source selection, dump, or streaming protocol remains the responsibility of the action implementation itself. + properties: + exec: + description: |- + Defines the command to run. + + + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. + + + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Specifies the name of the container within the same pod whose resources will be shared with the action. + This allows the action to utilize the specified container's resources without executing within it. + + + The name must match one of the containers defined in `componentDefinition.spec.runtime`. + + + The resources that can be shared are included: + + + - volume mounts + + + This field cannot be updated. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. + + + This field 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: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. + + + When specified, a dedicated container will be created using this image to execute the Action. + All actions with same image will share the same container. + + + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + grpc: + description: |- + Defines the gRPC call to issue. + + + This field cannot be updated. + properties: + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + description: Name of the method to invoke on the gRPC + service. + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "50051") or a named port defined in the container spec. + type: string + request: + additionalProperties: + type: string + description: |- + Request payload for the gRPC method. + + + Keys are proto field names (lowerCamelCase); values are strings that can include Go templates. + Templates are rendered with predefined action variables before the request is sent. + type: object + response: + description: Required response schema for the gRPC method. + properties: + message: + description: |- + Name of the field in the response whose value should be output. + Printed to stdout on success, or stderr on failure. + type: string + status: + description: |- + Name of the string field in the response that carries status information. + If non-empty, the action fails. + type: string + type: object + service: + description: Fully-qualified name of the gRPC service + to call. + type: string + required: + - method + - port + - service + type: object + http: + description: |- + Defines the HTTP request to perform. + + + This field cannot be updated. + properties: + body: + description: |- + Optional HTTP request body. + + + Supports Go text/template syntax; rendered with predefined variables before sending. + type: string + headers: + description: |- + Custom headers to set in the request. + Header values may use Go text/template syntax, rendered with predefined variables. + items: + description: HTTPHeader represents a single HTTP header + key/value pair. + properties: + name: + description: Name of the header field. + type: string + value: + description: Value of the header field. + type: string + required: + - name + - value + type: object + type: array + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + default: GET + description: |- + The HTTP method to use. + Defaults to "GET". + enum: + - GET + - POST + - PUT + - DELETE + - HEAD + - PATCH + type: string + path: + default: / + description: |- + The path to request on the HTTP server. + Defaults to "/" if not specified. + pattern: ^/.* + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "8080") or a named port defined in the container spec. + type: string + scheme: + default: HTTP + description: |- + The scheme to use for connecting to the host. + Defaults to "HTTP". + enum: + - HTTP + - HTTPS + type: string + required: + - port + type: object + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. + + + The conditions are as follows: + + + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. + + + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. + + + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. + + + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + Behavior based on the value: + - Positive (> 0): The action will be terminated after this many seconds. The maximum allowed value is 60. + - Zero (= 0): The timeout is managed by the system, defaulting to 30 seconds typically. + - Negative (< 0): No timeout is applied; the action runs until the command completes. + + + This field cannot be updated. + format: int32 + type: integer + type: object + memberJoin: + description: Defines the procedure to add a new replica into membership. + properties: + exec: + description: |- + Defines the command to run. + + + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. + + + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Specifies the name of the container within the same pod whose resources will be shared with the action. + This allows the action to utilize the specified container's resources without executing within it. + + + The name must match one of the containers defined in `componentDefinition.spec.runtime`. + + + The resources that can be shared are included: + + + - volume mounts + + + This field cannot be updated. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. + + + This field 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: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. + + + When specified, a dedicated container will be created using this image to execute the Action. + All actions with same image will share the same container. + + + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + grpc: + description: |- + Defines the gRPC call to issue. + + + This field cannot be updated. + properties: + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + description: Name of the method to invoke on the gRPC + service. + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "50051") or a named port defined in the container spec. + type: string + request: + additionalProperties: + type: string + description: |- + Request payload for the gRPC method. + + + Keys are proto field names (lowerCamelCase); values are strings that can include Go templates. + Templates are rendered with predefined action variables before the request is sent. + type: object + response: + description: Required response schema for the gRPC method. + properties: + message: + description: |- + Name of the field in the response whose value should be output. + Printed to stdout on success, or stderr on failure. + type: string + status: + description: |- + Name of the string field in the response that carries status information. + If non-empty, the action fails. + type: string + type: object + service: + description: Fully-qualified name of the gRPC service + to call. + type: string + required: + - method + - port + - service + type: object + http: + description: |- + Defines the HTTP request to perform. + + + This field cannot be updated. + properties: + body: + description: |- + Optional HTTP request body. + + + Supports Go text/template syntax; rendered with predefined variables before sending. + type: string + headers: + description: |- + Custom headers to set in the request. + Header values may use Go text/template syntax, rendered with predefined variables. + items: + description: HTTPHeader represents a single HTTP header + key/value pair. + properties: + name: + description: Name of the header field. + type: string + value: + description: Value of the header field. + type: string + required: + - name + - value + type: object + type: array + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + default: GET + description: |- + The HTTP method to use. + Defaults to "GET". + enum: + - GET + - POST + - PUT + - DELETE + - HEAD + - PATCH + type: string + path: + default: / + description: |- + The path to request on the HTTP server. + Defaults to "/" if not specified. + pattern: ^/.* + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "8080") or a named port defined in the container spec. + type: string + scheme: + default: HTTP + description: |- + The scheme to use for connecting to the host. + Defaults to "HTTP". + enum: + - HTTP + - HTTPS + type: string + required: + - port + type: object + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. + + + The conditions are as follows: + + + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. + + + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. + + + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. + + + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + Behavior based on the value: + - Positive (> 0): The action will be terminated after this many seconds. The maximum allowed value is 60. + - Zero (= 0): The timeout is managed by the system, defaulting to 30 seconds typically. + - Negative (< 0): No timeout is applied; the action runs until the command completes. + + + This field cannot be updated. + format: int32 + type: integer + type: object + memberLeave: + description: Defines the procedure to remove a replica from membership. + properties: + exec: + description: |- + Defines the command to run. + + + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. + + + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Specifies the name of the container within the same pod whose resources will be shared with the action. + This allows the action to utilize the specified container's resources without executing within it. + + + The name must match one of the containers defined in `componentDefinition.spec.runtime`. + + + The resources that can be shared are included: + + + - volume mounts + + + This field cannot be updated. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. + + + This field 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: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. + + + When specified, a dedicated container will be created using this image to execute the Action. + All actions with same image will share the same container. + + + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + grpc: + description: |- + Defines the gRPC call to issue. + + + This field cannot be updated. + properties: + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + description: Name of the method to invoke on the gRPC + service. + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "50051") or a named port defined in the container spec. + type: string + request: + additionalProperties: + type: string + description: |- + Request payload for the gRPC method. + + + Keys are proto field names (lowerCamelCase); values are strings that can include Go templates. + Templates are rendered with predefined action variables before the request is sent. + type: object + response: + description: Required response schema for the gRPC method. + properties: + message: + description: |- + Name of the field in the response whose value should be output. + Printed to stdout on success, or stderr on failure. + type: string + status: + description: |- + Name of the string field in the response that carries status information. + If non-empty, the action fails. + type: string + type: object + service: + description: Fully-qualified name of the gRPC service + to call. + type: string + required: + - method + - port + - service + type: object + http: + description: |- + Defines the HTTP request to perform. + + + This field cannot be updated. + properties: + body: + description: |- + Optional HTTP request body. + + + Supports Go text/template syntax; rendered with predefined variables before sending. + type: string + headers: + description: |- + Custom headers to set in the request. + Header values may use Go text/template syntax, rendered with predefined variables. + items: + description: HTTPHeader represents a single HTTP header + key/value pair. + properties: + name: + description: Name of the header field. + type: string + value: + description: Value of the header field. + type: string + required: + - name + - value + type: object + type: array + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + default: GET + description: |- + The HTTP method to use. + Defaults to "GET". + enum: + - GET + - POST + - PUT + - DELETE + - HEAD + - PATCH + type: string + path: + default: / + description: |- + The path to request on the HTTP server. + Defaults to "/" if not specified. + pattern: ^/.* + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "8080") or a named port defined in the container spec. + type: string + scheme: + default: HTTP + description: |- + The scheme to use for connecting to the host. + Defaults to "HTTP". + enum: + - HTTP + - HTTPS + type: string + required: + - port + type: object + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. + + + The conditions are as follows: + + + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. + + + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. + + + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. + + + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + Behavior based on the value: + - Positive (> 0): The action will be terminated after this many seconds. The maximum allowed value is 60. + - Zero (= 0): The timeout is managed by the system, defaulting to 30 seconds typically. + - Negative (< 0): No timeout is applied; the action runs until the command completes. + + + This field cannot be updated. + format: int32 + type: integer + type: object reconfigure: description: Defines the procedure that update replicas with new configuration. @@ -11789,10 +13088,20 @@ spec: - name type: object type: array + dataLoaded: + description: Represents whether the instance data is loaded. + type: boolean + memberJoined: + description: Represents whether the instance has joined the + cluster membership. + type: boolean podName: default: Unknown description: Represents the name of the pod. type: string + provisioned: + description: Represents whether the instance is provisioned. + type: boolean role: description: Represents the role of the instance observed. type: string diff --git a/controllers/apps/component/component_controller_test.go b/controllers/apps/component/component_controller_test.go index e249fb3ce5f..6e9bb111150 100644 --- a/controllers/apps/component/component_controller_test.go +++ b/controllers/apps/component/component_controller_test.go @@ -22,7 +22,6 @@ package component import ( "fmt" "strconv" - "strings" "time" . "github.com/onsi/ginkgo/v2" @@ -478,24 +477,6 @@ var _ = Describe("Component Controller", func() { scaleInCheck := func() { checkUpdatedItsReplicas() - - By("Checking pod's annotation should be updated consistently") - Eventually(func(g Gomega) { - podList := corev1.PodList{} - g.Expect(k8sClient.List(testCtx.Ctx, &podList, client.MatchingLabels{ - constant.AppInstanceLabelKey: clusterKey.Name, - constant.KBAppComponentLabelKey: compName, - })).Should(Succeed()) - for _, pod := range podList.Items { - ss := strings.Split(pod.Name, "-") - ordinal, _ := strconv.Atoi(ss[len(ss)-1]) - if ordinal >= updatedReplicas { - continue - } - // The annotation was updated by the mocked member leave action. - g.Expect(pod.Annotations[podAnnotationKey4Test]).Should(Equal(fmt.Sprintf("%d", updatedReplicas))) - } - }).Should(Succeed()) } if int(comp.Spec.Replicas) < updatedReplicas { diff --git a/controllers/apps/component/transformer_component_pre_terminate.go b/controllers/apps/component/transformer_component_pre_terminate.go index 1c31dfcdc53..2d8ad66aba6 100644 --- a/controllers/apps/component/transformer_component_pre_terminate.go +++ b/controllers/apps/component/transformer_component_pre_terminate.go @@ -107,13 +107,12 @@ func (t *componentPreTerminateTransformer) provisioned(transCtx *componentTransf return false, client.IgnoreNotFound(err) } - provisioned, err := component.GetReplicasStatusFunc(its, func(s component.ReplicaStatus) bool { - return s.Provisioned - }) - if err != nil { - return false, err + for _, status := range its.Status.InstanceStatus { + if status.Provisioned { + return true, nil + } } - return len(provisioned) > 0, nil + return false, nil } func (t *componentPreTerminateTransformer) checkPreTerminateDone(transCtx *componentTransformContext, dag *graph.DAG) bool { diff --git a/controllers/apps/component/transformer_component_pre_terminate_test.go b/controllers/apps/component/transformer_component_pre_terminate_test.go index dad9a3c3e8d..e8305f58b1a 100644 --- a/controllers/apps/component/transformer_component_pre_terminate_test.go +++ b/controllers/apps/component/transformer_component_pre_terminate_test.go @@ -37,7 +37,6 @@ import ( workloads "github.com/apecloud/kubeblocks/apis/workloads/v1" appsutil "github.com/apecloud/kubeblocks/controllers/apps/util" "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/controller/component" "github.com/apecloud/kubeblocks/pkg/controller/graph" "github.com/apecloud/kubeblocks/pkg/controller/model" kbacli "github.com/apecloud/kubeblocks/pkg/kbagent/client" @@ -65,10 +64,12 @@ var _ = Describe("pre-terminate transformer test", func() { } provisioned := func(its *workloads.InstanceSet) { - replicas := []string{ - fmt.Sprintf("%s-0", its.Name), + its.Status.InstanceStatus = []workloads.InstanceStatus{ + { + PodName: fmt.Sprintf("%s-0", its.Name), + Provisioned: true, + }, } - Expect(component.StatusReplicasStatus(its, replicas, false, false)).Should(Succeed()) } BeforeEach(func() { @@ -199,12 +200,9 @@ var _ = Describe("pre-terminate transformer test", func() { It("not provisioned", func() { its := reader.Objects[1].(*workloads.InstanceSet) - Expect(component.UpdateReplicasStatusFunc(its, func(r *component.ReplicasStatus) error { - for i := range r.Status { - r.Status[i].Provisioned = false - } - return nil - })).Should(Succeed()) + for i := range its.Status.InstanceStatus { + its.Status.InstanceStatus[i].Provisioned = false + } transformer := &componentPreTerminateTransformer{} err := transformer.Transform(transCtx, dag) diff --git a/controllers/apps/component/transformer_component_status.go b/controllers/apps/component/transformer_component_status.go index 970e59bfbbd..953cc76d3d0 100644 --- a/controllers/apps/component/transformer_component_status.go +++ b/controllers/apps/component/transformer_component_status.go @@ -225,20 +225,18 @@ func (t *componentStatusTransformer) hasScaleOutRunning(transCtx *componentTrans return false, false, nil } - replicas, err := component.GetReplicasStatusFunc(t.protoITS, func(status component.ReplicaStatus) bool { - return status.DataLoaded != nil && !*status.DataLoaded || - status.MemberJoined != nil && !*status.MemberJoined - }) - if err != nil { - return false, false, err - } - if len(replicas) == 0 { - return false, false, nil + for _, status := range t.runningITS.Status.InstanceStatus { + if status.DataLoaded != nil && !*status.DataLoaded { + return true, false, nil + } + if status.MemberJoined != nil && !*status.MemberJoined { + return true, false, nil + } } // TODO: scale-out failed - return true, false, nil + return false, false, nil } func (t *componentStatusTransformer) hasVolumeExpansionRunning() bool { diff --git a/controllers/apps/component/transformer_component_workload.go b/controllers/apps/component/transformer_component_workload.go index 6d070c64532..1fbbaaec84b 100644 --- a/controllers/apps/component/transformer_component_workload.go +++ b/controllers/apps/component/transformer_component_workload.go @@ -27,7 +27,6 @@ import ( "golang.org/x/exp/maps" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/sets" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -102,10 +101,6 @@ func (t *componentWorkloadTransformer) reconcileWorkload(ctx context.Context, cl t.buildInstanceSetPlacementAnnotation(comp, protoITS) - if err := t.reconcileReplicasStatus(ctx, cli, synthesizedComp, runningITS, protoITS); err != nil { - return err - } - return nil } @@ -121,43 +116,6 @@ func (t *componentWorkloadTransformer) buildInstanceSetPlacementAnnotation(comp } } -func (t *componentWorkloadTransformer) reconcileReplicasStatus(ctx context.Context, cli client.Reader, - synthesizedComp *component.SynthesizedComponent, runningITS, protoITS *workloads.InstanceSet) error { - var ( - namespace = synthesizedComp.Namespace - clusterName = synthesizedComp.ClusterName - compName = synthesizedComp.Name - ) - - // HACK: sync replicas status from runningITS to protoITS - component.BuildReplicasStatus(runningITS, protoITS) - - replicas, err := func() ([]string, error) { - pods, err := component.ListOwnedPods(ctx, cli, namespace, clusterName, compName) - if err != nil { - return nil, err - } - podNameSet := sets.New[string]() - for _, pod := range pods { - podNameSet.Insert(pod.Name) - } - - desiredPodNames, err := component.GetDesiredPodNamesByITS(runningITS, protoITS) - if err != nil { - return nil, err - } - desiredPodNameSet := sets.New(desiredPodNames...) - - return desiredPodNameSet.Intersection(podNameSet).UnsortedList(), nil - }() - if err != nil { - return err - } - - hasMemberJoinDefined, hasDataActionDefined := hasMemberJoinNDataActionDefined(synthesizedComp.LifecycleActions.ComponentLifecycleActions) - return component.StatusReplicasStatus(protoITS, replicas, hasMemberJoinDefined, hasDataActionDefined) -} - func (t *componentWorkloadTransformer) handleUpdate(transCtx *componentTransformContext, cli model.GraphClient, dag *graph.DAG, synthesizedComp *component.SynthesizedComponent, comp *appsv1.Component, runningITS, protoITS *workloads.InstanceSet) error { start, stop := t.handleWorkloadStartNStop(transCtx, synthesizedComp, runningITS, &protoITS) @@ -206,13 +164,13 @@ func isCompStopped(synthesizedComp *component.SynthesizedComponent) bool { return ptr.Deref(synthesizedComp.Stop, false) } -func (t *componentWorkloadTransformer) handleWorkloadUpdate(transCtx *componentTransformContext, dag *graph.DAG, - synthesizeComp *component.SynthesizedComponent, comp *appsv1.Component, obj, its *workloads.InstanceSet) error { - cwo, err := newComponentWorkloadOps(transCtx, t.Client, synthesizeComp, comp, obj, its, dag) +func (t *componentWorkloadTransformer) handleWorkloadUpdate(_ *componentTransformContext, _ *graph.DAG, + synthesizeComp *component.SynthesizedComponent, _ *appsv1.Component, obj, its *workloads.InstanceSet) error { + cwo, err := newComponentWorkloadOps(synthesizeComp, obj, its) if err != nil { return err } - if err := cwo.horizontalScale(); err != nil { + if err := cwo.validateHorizontalScale(); err != nil { return err } return nil @@ -686,19 +644,3 @@ func checkNRollbackProtoImages(itsObj, itsProto *workloads.InstanceSet) { rollback(1, &itsProto.Spec.Template.Spec.Containers[i]) } } - -func hasMemberJoinNDataActionDefined(lifecycleActions *appsv1.ComponentLifecycleActions) (bool, bool) { - if lifecycleActions == nil { - return false, false - } - hasActionDefined := func(actions []*appsv1.Action) bool { - for _, action := range actions { - if !action.Defined() { - return false - } - } - return true - } - return hasActionDefined([]*appsv1.Action{lifecycleActions.MemberJoin}), - hasActionDefined([]*appsv1.Action{lifecycleActions.DataDump, lifecycleActions.DataLoad}) -} diff --git a/controllers/apps/component/transformer_component_workload_ops.go b/controllers/apps/component/transformer_component_workload_ops.go index 8e494b8f421..1250e2eb454 100644 --- a/controllers/apps/component/transformer_component_workload_ops.go +++ b/controllers/apps/component/transformer_component_workload_ops.go @@ -20,47 +20,25 @@ along with this program. If not, see . package component import ( - "errors" "fmt" - "slices" - "time" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/client" appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" workloads "github.com/apecloud/kubeblocks/apis/workloads/v1" "github.com/apecloud/kubeblocks/pkg/controller/component" - "github.com/apecloud/kubeblocks/pkg/controller/graph" - "github.com/apecloud/kubeblocks/pkg/controller/lifecycle" - "github.com/apecloud/kubeblocks/pkg/controller/model" - intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" ) type componentWorkloadOps struct { - transCtx *componentTransformContext - cli client.Client - component *appsv1.Component synthesizeComp *component.SynthesizedComponent - dag *graph.DAG - // runningITS is a snapshot of the InstanceSet that is already running - runningITS *workloads.InstanceSet - // protoITS is the InstanceSet object that is rebuilt from scratch during each reconcile process - protoITS *workloads.InstanceSet desiredCompPodNameSet sets.Set[string] runningItsPodNameSet sets.Set[string] } -func newComponentWorkloadOps(transCtx *componentTransformContext, - cli client.Client, - synthesizedComp *component.SynthesizedComponent, - comp *appsv1.Component, +func newComponentWorkloadOps(synthesizedComp *component.SynthesizedComponent, runningITS *workloads.InstanceSet, - protoITS *workloads.InstanceSet, - dag *graph.DAG) (*componentWorkloadOps, error) { + protoITS *workloads.InstanceSet) (*componentWorkloadOps, error) { runningITSPodNames, err := component.GetCurrentPodNamesByITS(runningITS) if err != nil { return nil, err @@ -70,325 +48,20 @@ func newComponentWorkloadOps(transCtx *componentTransformContext, return nil, err } return &componentWorkloadOps{ - transCtx: transCtx, - cli: cli, - component: comp, synthesizeComp: synthesizedComp, - runningITS: runningITS, - protoITS: protoITS, - dag: dag, desiredCompPodNameSet: sets.New(protoITSPodNames...), runningItsPodNameSet: sets.New(runningITSPodNames...), }, nil } -func (r *componentWorkloadOps) horizontalScale() error { - var ( - in = r.runningItsPodNameSet.Difference(r.desiredCompPodNameSet) - out = r.desiredCompPodNameSet.Difference(r.runningItsPodNameSet) - ) - if in.Len() == 0 && out.Len() == 0 { - return r.postHorizontalScale() // TODO: how about consecutive horizontal scales? - } - - if in.Len() > 0 { - if err := r.scaleIn(); err != nil { - return err - } - } - - if out.Len() > 0 { - if err := r.scaleOut(); err != nil { - return err - } - } - - r.transCtx.EventRecorder.Eventf(r.component, - corev1.EventTypeNormal, - "HorizontalScale", - "start horizontal scale component %s of cluster %s from %d to %d", - r.synthesizeComp.Name, r.synthesizeComp.ClusterName, int(*r.runningITS.Spec.Replicas), r.synthesizeComp.Replicas) - - return nil -} - -func (r *componentWorkloadOps) scaleIn() error { - if r.synthesizeComp.Replicas == 0 && len(r.synthesizeComp.VolumeClaimTemplates) > 0 { - if r.synthesizeComp.PVCRetentionPolicy.WhenScaled != appsv1.RetainPersistentVolumeClaimRetentionPolicyType { - return fmt.Errorf("when intending to scale-in to 0, only the \"Retain\" option is supported for the PVC retention policy") - } - } - - deleteReplicas := r.runningItsPodNameSet.Difference(r.desiredCompPodNameSet).UnsortedList() - joinedReplicas := make([]string, 0) - err := component.DeleteReplicasStatus(r.protoITS, deleteReplicas, func(s component.ReplicaStatus) { - // has no member join defined or has joined successfully - if s.Provisioned && (s.MemberJoined == nil || *s.MemberJoined) { - joinedReplicas = append(joinedReplicas, s.Name) - } - }) - if err != nil { - return err - } - - // TODO: check the component definition to determine whether we need to call leave member before deleting replicas. - if err := r.leaveMember4ScaleIn(deleteReplicas, joinedReplicas); err != nil { - r.transCtx.Logger.Error(err, "leave member at scale-in error") - return err - } - return nil -} - -func (r *componentWorkloadOps) leaveMember4ScaleIn(deleteReplicas, joinedReplicas []string) error { - pods, err := component.ListOwnedPods(r.transCtx.Context, r.cli, - r.synthesizeComp.Namespace, r.synthesizeComp.ClusterName, r.synthesizeComp.Name) - if err != nil { - return err - } - - deleteReplicasSet := sets.New(deleteReplicas...) - joinedReplicasSet := sets.New(joinedReplicas...) - hasMemberLeaveDefined := r.synthesizeComp.LifecycleActions.ComponentLifecycleActions != nil && r.synthesizeComp.LifecycleActions.MemberLeave != nil - r.transCtx.Logger.Info("leave member at scaling-in", "delete replicas", deleteReplicas, - "joined replicas", joinedReplicas, "has member-leave action defined", hasMemberLeaveDefined) - - leaveErrors := make([]error, 0) - for _, pod := range pods { - if deleteReplicasSet.Has(pod.Name) { - if joinedReplicasSet.Has(pod.Name) { // else: hasn't joined yet, no need to leave - if err = r.leaveMemberForPod(pod, pods); err != nil { - leaveErrors = append(leaveErrors, err) - } - joinedReplicasSet.Delete(pod.Name) - } - deleteReplicasSet.Delete(pod.Name) - } - } - - if hasMemberLeaveDefined && len(joinedReplicasSet) > 0 { - leaveErrors = append(leaveErrors, - fmt.Errorf("some replicas have joined but not leaved since the Pod object is not exist: %v", sets.List(joinedReplicasSet))) - } - if len(leaveErrors) > 0 { - return intctrlutil.NewRequeueError(time.Second, fmt.Sprintf("%v", leaveErrors)) - } - return nil -} - -func (r *componentWorkloadOps) leaveMemberForPod(pod *corev1.Pod, pods []*corev1.Pod) error { - var ( - synthesizedComp = r.synthesizeComp - lifecycleActions = synthesizedComp.LifecycleActions - ) - - switchover := func(lfa lifecycle.Lifecycle, pod *corev1.Pod) error { - if lifecycleActions.Switchover == nil { - return nil - } - err := lfa.Switchover(r.transCtx.Context, r.cli, nil, "") - if err == nil { - r.transCtx.Logger.Info("succeed to call switchover action", "pod", pod.Name) - } else if !errors.Is(err, lifecycle.ErrActionNotDefined) { - r.transCtx.Logger.Info("failed to call switchover action, ignore it", "pod", pod.Name, "error", err) - } +func (r *componentWorkloadOps) validateHorizontalScale() error { + in := r.runningItsPodNameSet.Difference(r.desiredCompPodNameSet) + if in.Len() == 0 { return nil } - - leaveMember := func(lfa lifecycle.Lifecycle, pod *corev1.Pod) error { - if lifecycleActions.MemberLeave == nil { - return nil - } - err := lfa.MemberLeave(r.transCtx.Context, r.cli, nil) - if err != nil { - if errors.Is(err, lifecycle.ErrActionNotDefined) { - return nil - } - return err - } - r.transCtx.Logger.Info("succeed to call leave member action", "pod", pod.Name) - return nil - } - - if lifecycleActions.ComponentLifecycleActions == nil || - (lifecycleActions.Switchover == nil && lifecycleActions.MemberLeave == nil) { - return nil - } - - lfa, err := lifecycle.New(synthesizedComp.Namespace, synthesizedComp.ClusterName, synthesizedComp.Name, - lifecycleActions.ComponentLifecycleActions, synthesizedComp.TemplateVars, pod, pods) - if err != nil { - return err - } - - if err = switchover(lfa, pod); err != nil { - return err - } - if err = leaveMember(lfa, pod); err != nil { - return err - } - return nil -} - -func (r *componentWorkloadOps) scaleOut() error { - if err := r.buildDataReplicationTask(); err != nil { - return err - } - - // replicas to be created - newReplicas := r.desiredCompPodNameSet.Difference(r.runningItsPodNameSet).UnsortedList() - hasMemberJoinDefined, hasDataActionDefined := hasMemberJoinNDataActionDefined(r.synthesizeComp.LifecycleActions.ComponentLifecycleActions) - return component.NewReplicasStatus(r.protoITS, newReplicas, hasMemberJoinDefined, hasDataActionDefined) -} - -func (r *componentWorkloadOps) buildDataReplicationTask() error { - _, hasDataActionDefined := hasMemberJoinNDataActionDefined(r.synthesizeComp.LifecycleActions.ComponentLifecycleActions) - if !hasDataActionDefined { - return nil - } - - // replicas to be provisioned - newReplicas := r.desiredCompPodNameSet.Difference(r.runningItsPodNameSet).UnsortedList() - // replicas in provisioning that the data has not been loaded - provisioningReplicas, err := component.GetReplicasStatusFunc(r.protoITS, func(s component.ReplicaStatus) bool { - return s.DataLoaded != nil && !*s.DataLoaded - }) - if err != nil { - return err - } - - if len(newReplicas) == 0 && len(provisioningReplicas) == 0 { - return nil - } - - // the source replica - source, err := r.sourceReplica(r.synthesizeComp.LifecycleActions.DataDump, provisioningReplicas) - if err != nil { - return err - } - - replicas := append(slices.Clone(newReplicas), provisioningReplicas...) - parameters, err := component.NewReplicaTask(r.synthesizeComp.FullCompName, r.synthesizeComp.Generation, source, replicas) - if err != nil { - return err - } - // apply the updated env to the env CM - transCtx := &componentTransformContext{ - Context: r.transCtx.Context, - Client: model.NewGraphClient(r.cli), - SynthesizeComponent: r.synthesizeComp, - Component: r.component, - } - return createOrUpdateEnvConfigMap(transCtx, r.dag, nil, parameters) -} - -func (r *componentWorkloadOps) sourceReplica(dataDump *appsv1.Action, provisioningReplicas []string) (*corev1.Pod, error) { - pods, err := component.ListOwnedPods(r.transCtx.Context, r.cli, - r.synthesizeComp.Namespace, r.synthesizeComp.ClusterName, r.synthesizeComp.Name) - if err != nil { - return nil, err - } - if len(provisioningReplicas) > 0 { - // exclude provisioning replicas - pods = slices.DeleteFunc(pods, func(pod *corev1.Pod) bool { - return slices.Contains(provisioningReplicas, pod.Name) - }) - } - if len(pods) > 0 { - if len(dataDump.TargetPodSelector) == 0 && (dataDump.Exec == nil || len(dataDump.Exec.TargetPodSelector) == 0) { - dataDump.TargetPodSelector = appsv1.AnyReplica - } - // TODO: idempotence for provisioning replicas - pods, err = lifecycle.SelectTargetPods(pods, nil, dataDump) - if err != nil { - return nil, err - } - if len(pods) > 0 { - return pods[0], nil - } - } - return nil, fmt.Errorf("no available pod to dump data") -} - -func (r *componentWorkloadOps) postHorizontalScale() error { - if err := r.postScaleOut(); err != nil { - return err - } - return nil -} - -func (r *componentWorkloadOps) postScaleOut() error { - if err := r.buildDataReplicationTask(); err != nil { - return err - } - if err := r.joinMember4ScaleOut(); err != nil { - return err - } - return nil -} - -func (r *componentWorkloadOps) joinMember4ScaleOut() error { - pods, err := component.ListOwnedPods(r.transCtx.Context, r.cli, - r.synthesizeComp.Namespace, r.synthesizeComp.ClusterName, r.synthesizeComp.Name) - if err != nil { - return err - } - - joinErrors := make([]error, 0) - if err = component.UpdateReplicasStatusFunc(r.protoITS, func(replicas *component.ReplicasStatus) error { - for _, pod := range pods { - i := slices.IndexFunc(replicas.Status, func(r component.ReplicaStatus) bool { - return r.Name == pod.Name - }) - if i < 0 { - continue // the pod is not in the replicas status? - } - - status := replicas.Status[i] - if status.MemberJoined == nil || *status.MemberJoined { - continue // no need to join or already joined - } - - // TODO: should wait for the data to be loaded before joining the member? - - if err := r.joinMemberForPod(pod, pods); err != nil { - joinErrors = append(joinErrors, fmt.Errorf("pod %s: %w", pod.Name, err)) - } else { - replicas.Status[i].MemberJoined = ptr.To(true) - } - } - - notJoinedReplicas := make([]string, 0) - for _, r := range replicas.Status { - if r.MemberJoined != nil && !*r.MemberJoined { - notJoinedReplicas = append(notJoinedReplicas, r.Name) - } - } - if len(notJoinedReplicas) > 0 { - joinErrors = append(joinErrors, fmt.Errorf("some replicas have not joined: %v", notJoinedReplicas)) - } - return nil - }); err != nil { - return err - } - - if len(joinErrors) > 0 { - return intctrlutil.NewRequeueError(time.Second, fmt.Sprintf("%v", joinErrors)) - } - return nil -} - -func (r *componentWorkloadOps) joinMemberForPod(pod *corev1.Pod, pods []*corev1.Pod) error { - synthesizedComp := r.synthesizeComp - lfa, err := lifecycle.New(synthesizedComp.Namespace, synthesizedComp.ClusterName, synthesizedComp.Name, - synthesizedComp.LifecycleActions.ComponentLifecycleActions, synthesizedComp.TemplateVars, pod, pods) - if err != nil { - return err - } - if err = lfa.MemberJoin(r.transCtx.Context, r.cli, nil); err != nil { - if !errors.Is(err, lifecycle.ErrActionNotDefined) { - return err - } + if r.synthesizeComp.Replicas == 0 && len(r.synthesizeComp.VolumeClaimTemplates) > 0 && + r.synthesizeComp.PVCRetentionPolicy.WhenScaled != appsv1.RetainPersistentVolumeClaimRetentionPolicyType { + return fmt.Errorf("when intending to scale-in to 0, only the \"Retain\" option is supported for the PVC retention policy") } - r.transCtx.Logger.Info("succeed to join member for pod", "pod", pod.Name) return nil } diff --git a/controllers/apps/component/transformer_component_workload_test.go b/controllers/apps/component/transformer_component_workload_test.go index e5b792d5bae..185d7c890ba 100644 --- a/controllers/apps/component/transformer_component_workload_test.go +++ b/controllers/apps/component/transformer_component_workload_test.go @@ -16,188 +16,26 @@ along with this program. If not, see . package component import ( - "context" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/golang/mock/gomock" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" workloads "github.com/apecloud/kubeblocks/apis/workloads/v1" - appsutil "github.com/apecloud/kubeblocks/controllers/apps/util" "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/controller/component" - "github.com/apecloud/kubeblocks/pkg/controller/graph" - "github.com/apecloud/kubeblocks/pkg/controller/model" intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" - kbacli "github.com/apecloud/kubeblocks/pkg/kbagent/client" - kbagentproto "github.com/apecloud/kubeblocks/pkg/kbagent/proto" testapps "github.com/apecloud/kubeblocks/pkg/testutil/apps" ) var _ = Describe("Component Workload Operations Test", func() { const ( - clusterName = "test-cluster" - compName = "test-comp" - kubeblocksName = "kubeblocks" - ) - - var ( - reader *appsutil.MockReader - dag *graph.DAG - comp *appsv1.Component - synthesizeComp *component.SynthesizedComponent + clusterName = "test-cluster" + compName = "test-comp" ) - roles := []appsv1.ReplicaRole{ - {Name: "leader", UpdatePriority: 3}, - {Name: "follower", UpdatePriority: 2}, - } - - newDAG := func(graphCli model.GraphClient, comp *appsv1.Component) *graph.DAG { - d := graph.NewDAG() - graphCli.Root(d, comp, comp, model.ActionStatusPtr()) - return d - } - - BeforeEach(func() { - reader = &appsutil.MockReader{} - comp = &appsv1.Component{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testCtx.DefaultNamespace, - Name: constant.GenerateClusterComponentName(clusterName, compName), - Labels: map[string]string{ - constant.AppManagedByLabelKey: constant.AppName, - constant.AppInstanceLabelKey: clusterName, - constant.KBAppComponentLabelKey: compName, - }, - }, - Spec: appsv1.ComponentSpec{}, - } - - synthesizeComp = &component.SynthesizedComponent{ - Namespace: testCtx.DefaultNamespace, - ClusterName: clusterName, - Name: compName, - Roles: roles, - LifecycleActions: component.SynthesizedLifecycleActions{ - ComponentLifecycleActions: &appsv1.ComponentLifecycleActions{ - MemberJoin: &appsv1.Action{ - Exec: &appsv1.ExecAction{ - Image: "test-image", - }, - }, - MemberLeave: &appsv1.Action{ - Exec: &appsv1.ExecAction{ - Image: "test-image", - }, - }, - Switchover: &appsv1.Action{ - Exec: &appsv1.ExecAction{ - Image: "test-image", - }, - }, - }, - }, - } - - graphCli := model.NewGraphClient(reader) - dag = newDAG(graphCli, comp) - }) - Context("Member Leave Operations", func() { - var ( - ops *componentWorkloadOps - pod0 *corev1.Pod - pod1 *corev1.Pod - pods []*corev1.Pod - ) - - BeforeEach(func() { - pod0 = testapps.NewPodFactory(testCtx.DefaultNamespace, "test-pod-0"). - AddContainer(corev1.Container{ - Image: "test-image", - Name: "test-container", - }). - AddLabels( - constant.AppManagedByLabelKey, kubeblocksName, - constant.AppInstanceLabelKey, clusterName, - constant.KBAppComponentLabelKey, compName, - ). - GetObject() - - pod1 = testapps.NewPodFactory(testCtx.DefaultNamespace, "test-pod-1"). - AddContainer(corev1.Container{ - Image: "test-image", - Name: "test-container", - }). - AddLabels( - constant.AppManagedByLabelKey, kubeblocksName, - constant.AppInstanceLabelKey, clusterName, - constant.KBAppComponentLabelKey, compName, - ). - GetObject() - - pods = []*corev1.Pod{pod0, pod1} - - container := corev1.Container{ - Name: "mock-container-name", - Image: testapps.ApeCloudMySQLImage, - ImagePullPolicy: corev1.PullIfNotPresent, - } - - mockITS := testapps.NewInstanceSetFactory(testCtx.DefaultNamespace, - "test-its", clusterName, compName). - AddFinalizers([]string{constant.DBClusterFinalizerName}). - AddContainer(container). - AddAppInstanceLabel(clusterName). - AddAppComponentLabel(compName). - AddAppManagedByLabel(). - SetReplicas(2). - SetRoles(roles). - GetObject() - - ops = &componentWorkloadOps{ - transCtx: &componentTransformContext{ - Context: ctx, - Logger: logger, - EventRecorder: clusterRecorder, - }, - cli: k8sClient, - component: comp, - synthesizeComp: synthesizeComp, - runningITS: mockITS, - protoITS: mockITS.DeepCopy(), - dag: dag, - } - }) - - It("should handle switchover for when scale in", func() { - testapps.MockKBAgentClient(func(recorder *kbacli.MockClientMockRecorder) { - recorder.Action(gomock.Any(), gomock.Any()).Times(2).DoAndReturn(func(ctx context.Context, req kbagentproto.ActionRequest) (kbagentproto.ActionResponse, error) { - GinkgoWriter.Printf("ActionRequest: %#v\n", req) - switch req.Action { - case "switchover": - Expect(req.Parameters["KB_SWITCHOVER_CURRENT_NAME"]).Should(Equal(pod1.Name)) - case "memberLeave": - Expect(req.Parameters["KB_LEAVE_MEMBER_POD_NAME"]).Should(Equal(pod1.Name)) - } - rsp := kbagentproto.ActionResponse{Message: "mock success"} - return rsp, nil - }) - }) - - By("setting up leader pod") - pod1.Labels[constant.RoleLabelKey] = "follower" - pod1.Labels[constant.RoleLabelKey] = "leader" - - By("executing leave member for leader") - Expect(ops.leaveMemberForPod(pod1, pods)).Should(Succeed()) - }) - It("should eliminate upgrade-only diff by preserving legacy config-manager", func() { oldITS := testapps.NewInstanceSetFactory(testCtx.DefaultNamespace, "old-its", clusterName, compName). diff --git a/controllers/k8score/event_controller.go b/controllers/k8score/event_controller.go index be42515001d..cecd3565400 100644 --- a/controllers/k8score/event_controller.go +++ b/controllers/k8score/event_controller.go @@ -83,7 +83,6 @@ func (r *EventReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl handlers := []eventHandler{ &instanceset.PodRoleEventHandler{}, &component.AvailableEventHandler{}, - &component.KBAgentTaskEventHandler{}, } for _, handler := range handlers { if err := handler.Handle(r.Client, reqCtx, r.Recorder, event); err != nil && !apierrors.IsNotFound(err) { diff --git a/controllers/workloads/instanceset_controller_test.go b/controllers/workloads/instanceset_controller_test.go index 6be7bb67142..6a2411b3e95 100644 --- a/controllers/workloads/instanceset_controller_test.go +++ b/controllers/workloads/instanceset_controller_test.go @@ -141,6 +141,21 @@ var _ = Describe("InstanceSet Controller", func() { return pods } + mockPodRoleReady := func(podName, role string) { + By("mock pod role and ready") + podKey := types.NamespacedName{ + Namespace: itsObj.Namespace, + Name: podName, + } + Expect(testapps.GetAndChangeObj(&testCtx, podKey, func(pod *corev1.Pod) { + if pod.Labels == nil { + pod.Labels = map[string]string{} + } + pod.Labels[constant.RoleLabelKey] = role + })()).Should(Succeed()) + mockPodReady(podName) + } + Context("reconciliation", func() { It("should reconcile well", func() { name := "test-instance-set" @@ -471,7 +486,8 @@ var _ = Describe("InstanceSet Controller", func() { Eventually(testapps.CheckObj(&testCtx, itsKey, func(g Gomega, its *workloads.InstanceSet) { g.Expect(its.Status.InstanceStatus).Should(HaveLen(1)) g.Expect(its.Status.InstanceStatus[0]).Should(Equal(workloads.InstanceStatus{ - PodName: fmt.Sprintf("%s-0", itsObj.Name), + PodName: fmt.Sprintf("%s-0", itsObj.Name), + Provisioned: true, Configs: []workloads.InstanceConfigStatus{ { Name: "log", @@ -486,6 +502,286 @@ var _ = Describe("InstanceSet Controller", func() { })).Should(Succeed()) }) + It("instance status - lifecycle", func() { + createITSObj(itsName, func(f *testapps.MockInstanceSetFactory) { + f.Get().Spec.LifecycleActions = &workloads.LifecycleActions{ + MemberJoin: testapps.NewLifecycleAction("member-join"), + DataLoad: testapps.NewLifecycleAction("data-load"), + } + }) + + By("check lifecycle status defaults for an available pod") + podName := fmt.Sprintf("%s-0", itsObj.Name) + mockPodReady(podName) + Eventually(testapps.CheckObj(&testCtx, itsKey, func(g Gomega, its *workloads.InstanceSet) { + g.Expect(its.Status.InstanceStatus).Should(HaveLen(1)) + g.Expect(its.Status.InstanceStatus[0].PodName).Should(Equal(podName)) + g.Expect(its.Status.InstanceStatus[0].Provisioned).Should(BeTrue()) + g.Expect(its.Status.InstanceStatus[0].DataLoaded).Should(BeNil()) + g.Expect(its.Status.InstanceStatus[0].MemberJoined).ShouldNot(BeNil()) + g.Expect(*its.Status.InstanceStatus[0].MemberJoined).Should(BeTrue()) + })).Should(Succeed()) + }) + + It("scale-out lifecycle actions", func() { + type actionCall struct { + name string + targetPod string + memberPod string + sourcePod string + } + var actions []actionCall + testapps.MockKBAgentClient(func(recorder *kbacli.MockClientMockRecorder) { + recorder.Action(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req kbaproto.ActionRequest) (kbaproto.ActionResponse, error) { + actions = append(actions, actionCall{ + name: req.Action, + targetPod: req.Parameters["KB_TARGET_POD_NAME"], + memberPod: req.Parameters["KB_JOIN_MEMBER_POD_NAME"], + sourcePod: req.Parameters["KB_SOURCE_POD_NAME"], + }) + return kbaproto.ActionResponse{}, nil + }).AnyTimes() + }) + + createITSObj(itsName, func(f *testapps.MockInstanceSetFactory) { + f.Get().Spec.LifecycleActions = &workloads.LifecycleActions{ + MemberJoin: testapps.NewLifecycleAction("member-join"), + DataLoad: testapps.NewLifecycleAction("data-load"), + } + }) + + mockPodReady(fmt.Sprintf("%s-0", itsObj.Name)) + Expect(testapps.GetAndChangeObj(&testCtx, itsKey, func(its *workloads.InstanceSet) { + its.Spec.Replicas = ptr.To[int32](2) + })()).Should(Succeed()) + + scaleOutPodName := fmt.Sprintf("%s-1", itsObj.Name) + scaleOutPodKey := types.NamespacedName{Namespace: itsObj.Namespace, Name: scaleOutPodName} + Eventually(testapps.CheckObjExists(&testCtx, scaleOutPodKey, &corev1.Pod{}, true)).Should(Succeed()) + mockPodReady(scaleOutPodName) + + Eventually(func(g Gomega) { + g.Expect(actions).ShouldNot(BeEmpty()) + g.Expect(actions[0].name).Should(Equal("dataLoad")) + g.Expect(actions[0].targetPod).Should(Equal(scaleOutPodName)) + g.Expect(actions[0].sourcePod).Should(BeEmpty()) + memberJoinSeen := false + for _, action := range actions { + if action.name == "memberJoin" { + g.Expect(action.memberPod).Should(Equal(scaleOutPodName)) + memberJoinSeen = true + } + } + g.Expect(memberJoinSeen).Should(BeTrue()) + }).Should(Succeed()) + + Eventually(testapps.CheckObj(&testCtx, itsKey, func(g Gomega, its *workloads.InstanceSet) { + g.Expect(its.Status.InstanceStatus).Should(HaveLen(2)) + for _, status := range its.Status.InstanceStatus { + switch status.PodName { + case fmt.Sprintf("%s-0", itsObj.Name): + g.Expect(status.DataLoaded).Should(BeNil()) + g.Expect(status.MemberJoined).ShouldNot(BeNil()) + g.Expect(*status.MemberJoined).Should(BeTrue()) + case scaleOutPodName: + g.Expect(status.Provisioned).Should(BeTrue()) + g.Expect(status.DataLoaded).ShouldNot(BeNil()) + g.Expect(*status.DataLoaded).Should(BeTrue()) + g.Expect(status.MemberJoined).ShouldNot(BeNil()) + g.Expect(*status.MemberJoined).Should(BeTrue()) + } + } + })).Should(Succeed()) + }) + + It("best-effort parallel scale-out lifecycle actions", func() { + type actionCall struct { + name string + targetPod string + memberPod string + sourcePod string + } + var actions []actionCall + testapps.MockKBAgentClient(func(recorder *kbacli.MockClientMockRecorder) { + recorder.Action(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req kbaproto.ActionRequest) (kbaproto.ActionResponse, error) { + actions = append(actions, actionCall{ + name: req.Action, + targetPod: req.Parameters["KB_TARGET_POD_NAME"], + memberPod: req.Parameters["KB_JOIN_MEMBER_POD_NAME"], + sourcePod: req.Parameters["KB_SOURCE_POD_NAME"], + }) + return kbaproto.ActionResponse{}, nil + }).AnyTimes() + }) + + createITSObj(itsName, func(f *testapps.MockInstanceSetFactory) { + f.Get().Spec.LifecycleActions = &workloads.LifecycleActions{ + MemberJoin: testapps.NewLifecycleAction("member-join"), + DataLoad: testapps.NewLifecycleAction("data-load"), + } + f.Get().Spec.MemberUpdateStrategy = ptr.To(workloads.BestEffortParallelUpdateStrategy) + f.Get().Spec.PodManagementPolicy = appsv1.ParallelPodManagement + }) + + mockPodReady(fmt.Sprintf("%s-0", itsObj.Name)) + Expect(testapps.GetAndChangeObj(&testCtx, itsKey, func(its *workloads.InstanceSet) { + its.Spec.Replicas = ptr.To[int32](3) + })()).Should(Succeed()) + + scaleOutPod1 := fmt.Sprintf("%s-1", itsObj.Name) + scaleOutPod2 := fmt.Sprintf("%s-2", itsObj.Name) + Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{Namespace: itsObj.Namespace, Name: scaleOutPod1}, &corev1.Pod{}, true)).Should(Succeed()) + Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{Namespace: itsObj.Namespace, Name: scaleOutPod2}, &corev1.Pod{}, true)).Should(Succeed()) + mockPodReady(scaleOutPod1, scaleOutPod2) + + Eventually(func(g Gomega) { + dataLoads := map[string]int{} + memberJoins := map[string]int{} + for _, action := range actions { + switch action.name { + case "dataLoad": + dataLoads[action.targetPod]++ + case "memberJoin": + memberJoins[action.memberPod]++ + } + } + g.Expect(dataLoads[scaleOutPod1]).Should(BeNumerically(">=", 1)) + g.Expect(memberJoins[scaleOutPod1]).Should(BeNumerically(">=", 1)) + g.Expect(dataLoads[scaleOutPod2]).Should(BeNumerically(">=", 1)) + g.Expect(memberJoins[scaleOutPod2]).Should(BeNumerically(">=", 1)) + }).Should(Succeed()) + }) + + It("scale-in lifecycle actions", func() { + var ( + actions []string + leaveMemberNames []string + ) + testapps.MockKBAgentClient(func(recorder *kbacli.MockClientMockRecorder) { + recorder.Action(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req kbaproto.ActionRequest) (kbaproto.ActionResponse, error) { + actions = append(actions, req.Action) + if req.Action == "memberLeave" { + leaveMemberNames = append(leaveMemberNames, req.Parameters["KB_LEAVE_MEMBER_POD_NAME"]) + } + return kbaproto.ActionResponse{}, nil + }).AnyTimes() + }) + + createITSObj(itsName, func(f *testapps.MockInstanceSetFactory) { + f.Get().Spec.LifecycleActions = &workloads.LifecycleActions{ + MemberJoin: testapps.NewLifecycleAction("member-join"), + MemberLeave: testapps.NewLifecycleAction("member-leave"), + DataLoad: testapps.NewLifecycleAction("data-load"), + } + }) + + mockPodReady(fmt.Sprintf("%s-0", itsObj.Name)) + Expect(testapps.GetAndChangeObj(&testCtx, itsKey, func(its *workloads.InstanceSet) { + its.Spec.Replicas = ptr.To[int32](2) + })()).Should(Succeed()) + + scaleOutPodName := fmt.Sprintf("%s-1", itsObj.Name) + scaleOutPodKey := types.NamespacedName{Namespace: itsObj.Namespace, Name: scaleOutPodName} + Eventually(testapps.CheckObjExists(&testCtx, scaleOutPodKey, &corev1.Pod{}, true)).Should(Succeed()) + mockPodReady(scaleOutPodName) + + Eventually(testapps.CheckObj(&testCtx, itsKey, func(g Gomega, its *workloads.InstanceSet) { + g.Expect(its.Status.InstanceStatus).Should(HaveLen(2)) + for _, status := range its.Status.InstanceStatus { + if status.PodName == scaleOutPodName { + g.Expect(status.MemberJoined).ShouldNot(BeNil()) + g.Expect(*status.MemberJoined).Should(BeTrue()) + } + } + })).Should(Succeed()) + + Expect(testapps.GetAndChangeObj(&testCtx, itsKey, func(its *workloads.InstanceSet) { + its.Spec.Replicas = ptr.To[int32](1) + })()).Should(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(actions).Should(ContainElement("memberLeave")) + g.Expect(leaveMemberNames).Should(ContainElement(scaleOutPodName)) + }).Should(Succeed()) + Eventually(testapps.CheckObjExists(&testCtx, scaleOutPodKey, &corev1.Pod{}, false)).Should(Succeed()) + }) + + It("scale-in bootstrap lifecycle actions", func() { + var leaveMemberNames []string + testapps.MockKBAgentClient(func(recorder *kbacli.MockClientMockRecorder) { + recorder.Action(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req kbaproto.ActionRequest) (kbaproto.ActionResponse, error) { + if req.Action == "memberLeave" { + leaveMemberNames = append(leaveMemberNames, req.Parameters["KB_LEAVE_MEMBER_POD_NAME"]) + } + return kbaproto.ActionResponse{}, nil + }).AnyTimes() + }) + + createITSObj(itsName, func(f *testapps.MockInstanceSetFactory) { + f.Get().Spec.LifecycleActions = &workloads.LifecycleActions{ + MemberLeave: testapps.NewLifecycleAction("member-leave"), + } + }) + + Eventually(testapps.CheckObj(&testCtx, itsKey, func(g Gomega, its *workloads.InstanceSet) { + g.Expect(its.Status.InstanceStatus).Should(HaveLen(1)) + for _, status := range its.Status.InstanceStatus { + if status.PodName == fmt.Sprintf("%s-0", itsObj.Name) { + g.Expect(status.MemberJoined).ShouldNot(BeNil()) + g.Expect(*status.MemberJoined).Should(BeTrue()) + } + } + })).Should(Succeed()) + mockPodReady(fmt.Sprintf("%s-0", itsObj.Name)) + + Expect(testapps.GetAndChangeObj(&testCtx, itsKey, func(its *workloads.InstanceSet) { + its.Spec.Replicas = ptr.To[int32](0) + })()).Should(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(leaveMemberNames).Should(ContainElement(fmt.Sprintf("%s-0", itsObj.Name))) + }).Should(Succeed()) + }) + + It("best-effort parallel scale-in lifecycle actions", func() { + var leaveMemberNames []string + testapps.MockKBAgentClient(func(recorder *kbacli.MockClientMockRecorder) { + recorder.Action(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req kbaproto.ActionRequest) (kbaproto.ActionResponse, error) { + if req.Action == "memberLeave" { + leaveMemberNames = append(leaveMemberNames, req.Parameters["KB_LEAVE_MEMBER_POD_NAME"]) + } + return kbaproto.ActionResponse{}, nil + }).AnyTimes() + }) + + createITSObj(itsName, func(f *testapps.MockInstanceSetFactory) { + f.SetReplicas(3).SetRoles([]workloads.ReplicaRole{ + {Name: "leader", ParticipatesInQuorum: true, UpdatePriority: 5}, + {Name: "follower", ParticipatesInQuorum: true, UpdatePriority: 4}, + {Name: "learner", ParticipatesInQuorum: false, UpdatePriority: 2}, + }) + f.Get().Spec.LifecycleActions = &workloads.LifecycleActions{ + MemberLeave: testapps.NewLifecycleAction("member-leave"), + } + f.Get().Spec.MemberUpdateStrategy = ptr.To(workloads.BestEffortParallelUpdateStrategy) + f.Get().Spec.PodManagementPolicy = appsv1.ParallelPodManagement + }) + + mockPodRoleReady(fmt.Sprintf("%s-0", itsObj.Name), "leader") + mockPodRoleReady(fmt.Sprintf("%s-1", itsObj.Name), "follower") + mockPodRoleReady(fmt.Sprintf("%s-2", itsObj.Name), "learner") + + Expect(testapps.GetAndChangeObj(&testCtx, itsKey, func(its *workloads.InstanceSet) { + its.Spec.Replicas = ptr.To[int32](1) + })()).Should(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(leaveMemberNames).Should(HaveLen(2)) + g.Expect(leaveMemberNames[0]).Should(Equal(fmt.Sprintf("%s-2", itsObj.Name))) + g.Expect(leaveMemberNames[1]).Should(Equal(fmt.Sprintf("%s-1", itsObj.Name))) + }).Should(Succeed()) + }) + It("reconfigure", func() { By("mock reconfigure action calls") var ( @@ -529,7 +825,8 @@ var _ = Describe("InstanceSet Controller", func() { Eventually(testapps.CheckObj(&testCtx, itsKey, func(g Gomega, its *workloads.InstanceSet) { g.Expect(its.Status.InstanceStatus).Should(HaveLen(1)) g.Expect(its.Status.InstanceStatus[0]).Should(Equal(workloads.InstanceStatus{ - PodName: fmt.Sprintf("%s-0", itsObj.Name), + PodName: fmt.Sprintf("%s-0", itsObj.Name), + Provisioned: true, Configs: []workloads.InstanceConfigStatus{ { Name: "log", @@ -562,7 +859,8 @@ var _ = Describe("InstanceSet Controller", func() { Eventually(testapps.CheckObj(&testCtx, itsKey, func(g Gomega, its *workloads.InstanceSet) { g.Expect(its.Status.InstanceStatus).Should(HaveLen(1)) g.Expect(its.Status.InstanceStatus[0]).Should(Equal(workloads.InstanceStatus{ - PodName: fmt.Sprintf("%s-0", itsObj.Name), + PodName: fmt.Sprintf("%s-0", itsObj.Name), + Provisioned: true, Configs: []workloads.InstanceConfigStatus{ { Name: "log", @@ -620,7 +918,8 @@ var _ = Describe("InstanceSet Controller", func() { Eventually(testapps.CheckObj(&testCtx, itsKey, func(g Gomega, its *workloads.InstanceSet) { g.Expect(its.Status.InstanceStatus).Should(HaveLen(1)) g.Expect(its.Status.InstanceStatus[0]).Should(Equal(workloads.InstanceStatus{ - PodName: fmt.Sprintf("%s-0", itsObj.Name), + PodName: fmt.Sprintf("%s-0", itsObj.Name), + Provisioned: true, Configs: []workloads.InstanceConfigStatus{ { Name: "log", @@ -653,7 +952,8 @@ var _ = Describe("InstanceSet Controller", func() { Eventually(testapps.CheckObj(&testCtx, itsKey, func(g Gomega, its *workloads.InstanceSet) { g.Expect(its.Status.InstanceStatus).Should(HaveLen(1)) g.Expect(its.Status.InstanceStatus[0]).Should(Equal(workloads.InstanceStatus{ - PodName: fmt.Sprintf("%s-0", itsObj.Name), + PodName: fmt.Sprintf("%s-0", itsObj.Name), + Provisioned: true, Configs: []workloads.InstanceConfigStatus{ { Name: "log", @@ -691,7 +991,8 @@ var _ = Describe("InstanceSet Controller", func() { Eventually(testapps.CheckObj(&testCtx, itsKey, func(g Gomega, its *workloads.InstanceSet) { g.Expect(its.Status.InstanceStatus).Should(HaveLen(1)) g.Expect(its.Status.InstanceStatus[0]).Should(Equal(workloads.InstanceStatus{ - PodName: fmt.Sprintf("%s-0", itsObj.Name), + PodName: fmt.Sprintf("%s-0", itsObj.Name), + Provisioned: true, Configs: []workloads.InstanceConfigStatus{ { Name: "log", @@ -721,7 +1022,8 @@ var _ = Describe("InstanceSet Controller", func() { Eventually(testapps.CheckObj(&testCtx, itsKey, func(g Gomega, its *workloads.InstanceSet) { g.Expect(its.Status.InstanceStatus).Should(HaveLen(1)) g.Expect(its.Status.InstanceStatus[0]).Should(Equal(workloads.InstanceStatus{ - PodName: fmt.Sprintf("%s-0", itsObj.Name), + PodName: fmt.Sprintf("%s-0", itsObj.Name), + Provisioned: true, Configs: []workloads.InstanceConfigStatus{ { Name: "log", @@ -783,7 +1085,8 @@ var _ = Describe("InstanceSet Controller", func() { Eventually(testapps.CheckObj(&testCtx, itsKey, func(g Gomega, its *workloads.InstanceSet) { g.Expect(its.Status.InstanceStatus).Should(HaveLen(1)) g.Expect(its.Status.InstanceStatus[0]).Should(Equal(workloads.InstanceStatus{ - PodName: fmt.Sprintf("%s-0", itsObj.Name), + PodName: fmt.Sprintf("%s-0", itsObj.Name), + Provisioned: true, Configs: []workloads.InstanceConfigStatus{ { Name: "log", @@ -825,7 +1128,8 @@ var _ = Describe("InstanceSet Controller", func() { Eventually(testapps.CheckObj(&testCtx, itsKey, func(g Gomega, its *workloads.InstanceSet) { g.Expect(its.Status.InstanceStatus).Should(HaveLen(1)) g.Expect(its.Status.InstanceStatus[0]).Should(Equal(workloads.InstanceStatus{ - PodName: fmt.Sprintf("%s-0", itsObj.Name), + PodName: fmt.Sprintf("%s-0", itsObj.Name), + Provisioned: true, Configs: []workloads.InstanceConfigStatus{ { Name: "log", @@ -894,7 +1198,8 @@ var _ = Describe("InstanceSet Controller", func() { Eventually(testapps.CheckObj(&testCtx, itsKey, func(g Gomega, its *workloads.InstanceSet) { g.Expect(its.Status.InstanceStatus).Should(HaveLen(1)) g.Expect(its.Status.InstanceStatus[0]).Should(Equal(workloads.InstanceStatus{ - PodName: fmt.Sprintf("%s-0", itsObj.Name), + PodName: fmt.Sprintf("%s-0", itsObj.Name), + Provisioned: true, Configs: []workloads.InstanceConfigStatus{ { Name: "client", @@ -947,7 +1252,8 @@ var _ = Describe("InstanceSet Controller", func() { Eventually(testapps.CheckObj(&testCtx, itsKey, func(g Gomega, its *workloads.InstanceSet) { g.Expect(its.Status.InstanceStatus).Should(HaveLen(1)) g.Expect(its.Status.InstanceStatus[0]).Should(Equal(workloads.InstanceStatus{ - PodName: fmt.Sprintf("%s-0", itsObj.Name), + PodName: fmt.Sprintf("%s-0", itsObj.Name), + Provisioned: true, Configs: []workloads.InstanceConfigStatus{ { Name: "client", diff --git a/deploy/helm/crds/workloads.kubeblocks.io_instances.yaml b/deploy/helm/crds/workloads.kubeblocks.io_instances.yaml index e9699057a78..fbb98118e2e 100644 --- a/deploy/helm/crds/workloads.kubeblocks.io_instances.yaml +++ b/deploy/helm/crds/workloads.kubeblocks.io_instances.yaml @@ -1095,6 +1095,1305 @@ spec: description: Defines a set of hooks that customize the behavior of an Instance throughout its lifecycle. properties: + dataLoad: + description: |- + Defines the procedure for importing data into a replica. + InstanceSet only orchestrates the target replica side of this action. + Any source selection, dump, or streaming protocol remains the responsibility of the action implementation itself. + properties: + exec: + description: |- + Defines the command to run. + + + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. + + + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Specifies the name of the container within the same pod whose resources will be shared with the action. + This allows the action to utilize the specified container's resources without executing within it. + + + The name must match one of the containers defined in `componentDefinition.spec.runtime`. + + + The resources that can be shared are included: + + + - volume mounts + + + This field cannot be updated. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. + + + This field 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: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. + + + When specified, a dedicated container will be created using this image to execute the Action. + All actions with same image will share the same container. + + + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + grpc: + description: |- + Defines the gRPC call to issue. + + + This field cannot be updated. + properties: + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + description: Name of the method to invoke on the gRPC + service. + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "50051") or a named port defined in the container spec. + type: string + request: + additionalProperties: + type: string + description: |- + Request payload for the gRPC method. + + + Keys are proto field names (lowerCamelCase); values are strings that can include Go templates. + Templates are rendered with predefined action variables before the request is sent. + type: object + response: + description: Required response schema for the gRPC method. + properties: + message: + description: |- + Name of the field in the response whose value should be output. + Printed to stdout on success, or stderr on failure. + type: string + status: + description: |- + Name of the string field in the response that carries status information. + If non-empty, the action fails. + type: string + type: object + service: + description: Fully-qualified name of the gRPC service + to call. + type: string + required: + - method + - port + - service + type: object + http: + description: |- + Defines the HTTP request to perform. + + + This field cannot be updated. + properties: + body: + description: |- + Optional HTTP request body. + + + Supports Go text/template syntax; rendered with predefined variables before sending. + type: string + headers: + description: |- + Custom headers to set in the request. + Header values may use Go text/template syntax, rendered with predefined variables. + items: + description: HTTPHeader represents a single HTTP header + key/value pair. + properties: + name: + description: Name of the header field. + type: string + value: + description: Value of the header field. + type: string + required: + - name + - value + type: object + type: array + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + default: GET + description: |- + The HTTP method to use. + Defaults to "GET". + enum: + - GET + - POST + - PUT + - DELETE + - HEAD + - PATCH + type: string + path: + default: / + description: |- + The path to request on the HTTP server. + Defaults to "/" if not specified. + pattern: ^/.* + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "8080") or a named port defined in the container spec. + type: string + scheme: + default: HTTP + description: |- + The scheme to use for connecting to the host. + Defaults to "HTTP". + enum: + - HTTP + - HTTPS + type: string + required: + - port + type: object + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. + + + The conditions are as follows: + + + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. + + + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. + + + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. + + + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + Behavior based on the value: + - Positive (> 0): The action will be terminated after this many seconds. The maximum allowed value is 60. + - Zero (= 0): The timeout is managed by the system, defaulting to 30 seconds typically. + - Negative (< 0): No timeout is applied; the action runs until the command completes. + + + This field cannot be updated. + format: int32 + type: integer + type: object + memberJoin: + description: Defines the procedure to add a new replica into membership. + properties: + exec: + description: |- + Defines the command to run. + + + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. + + + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Specifies the name of the container within the same pod whose resources will be shared with the action. + This allows the action to utilize the specified container's resources without executing within it. + + + The name must match one of the containers defined in `componentDefinition.spec.runtime`. + + + The resources that can be shared are included: + + + - volume mounts + + + This field cannot be updated. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. + + + This field 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: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. + + + When specified, a dedicated container will be created using this image to execute the Action. + All actions with same image will share the same container. + + + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + grpc: + description: |- + Defines the gRPC call to issue. + + + This field cannot be updated. + properties: + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + description: Name of the method to invoke on the gRPC + service. + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "50051") or a named port defined in the container spec. + type: string + request: + additionalProperties: + type: string + description: |- + Request payload for the gRPC method. + + + Keys are proto field names (lowerCamelCase); values are strings that can include Go templates. + Templates are rendered with predefined action variables before the request is sent. + type: object + response: + description: Required response schema for the gRPC method. + properties: + message: + description: |- + Name of the field in the response whose value should be output. + Printed to stdout on success, or stderr on failure. + type: string + status: + description: |- + Name of the string field in the response that carries status information. + If non-empty, the action fails. + type: string + type: object + service: + description: Fully-qualified name of the gRPC service + to call. + type: string + required: + - method + - port + - service + type: object + http: + description: |- + Defines the HTTP request to perform. + + + This field cannot be updated. + properties: + body: + description: |- + Optional HTTP request body. + + + Supports Go text/template syntax; rendered with predefined variables before sending. + type: string + headers: + description: |- + Custom headers to set in the request. + Header values may use Go text/template syntax, rendered with predefined variables. + items: + description: HTTPHeader represents a single HTTP header + key/value pair. + properties: + name: + description: Name of the header field. + type: string + value: + description: Value of the header field. + type: string + required: + - name + - value + type: object + type: array + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + default: GET + description: |- + The HTTP method to use. + Defaults to "GET". + enum: + - GET + - POST + - PUT + - DELETE + - HEAD + - PATCH + type: string + path: + default: / + description: |- + The path to request on the HTTP server. + Defaults to "/" if not specified. + pattern: ^/.* + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "8080") or a named port defined in the container spec. + type: string + scheme: + default: HTTP + description: |- + The scheme to use for connecting to the host. + Defaults to "HTTP". + enum: + - HTTP + - HTTPS + type: string + required: + - port + type: object + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. + + + The conditions are as follows: + + + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. + + + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. + + + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. + + + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + Behavior based on the value: + - Positive (> 0): The action will be terminated after this many seconds. The maximum allowed value is 60. + - Zero (= 0): The timeout is managed by the system, defaulting to 30 seconds typically. + - Negative (< 0): No timeout is applied; the action runs until the command completes. + + + This field cannot be updated. + format: int32 + type: integer + type: object + memberLeave: + description: Defines the procedure to remove a replica from membership. + properties: + exec: + description: |- + Defines the command to run. + + + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. + + + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Specifies the name of the container within the same pod whose resources will be shared with the action. + This allows the action to utilize the specified container's resources without executing within it. + + + The name must match one of the containers defined in `componentDefinition.spec.runtime`. + + + The resources that can be shared are included: + + + - volume mounts + + + This field cannot be updated. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. + + + This field 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: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. + + + When specified, a dedicated container will be created using this image to execute the Action. + All actions with same image will share the same container. + + + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + grpc: + description: |- + Defines the gRPC call to issue. + + + This field cannot be updated. + properties: + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + description: Name of the method to invoke on the gRPC + service. + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "50051") or a named port defined in the container spec. + type: string + request: + additionalProperties: + type: string + description: |- + Request payload for the gRPC method. + + + Keys are proto field names (lowerCamelCase); values are strings that can include Go templates. + Templates are rendered with predefined action variables before the request is sent. + type: object + response: + description: Required response schema for the gRPC method. + properties: + message: + description: |- + Name of the field in the response whose value should be output. + Printed to stdout on success, or stderr on failure. + type: string + status: + description: |- + Name of the string field in the response that carries status information. + If non-empty, the action fails. + type: string + type: object + service: + description: Fully-qualified name of the gRPC service + to call. + type: string + required: + - method + - port + - service + type: object + http: + description: |- + Defines the HTTP request to perform. + + + This field cannot be updated. + properties: + body: + description: |- + Optional HTTP request body. + + + Supports Go text/template syntax; rendered with predefined variables before sending. + type: string + headers: + description: |- + Custom headers to set in the request. + Header values may use Go text/template syntax, rendered with predefined variables. + items: + description: HTTPHeader represents a single HTTP header + key/value pair. + properties: + name: + description: Name of the header field. + type: string + value: + description: Value of the header field. + type: string + required: + - name + - value + type: object + type: array + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + default: GET + description: |- + The HTTP method to use. + Defaults to "GET". + enum: + - GET + - POST + - PUT + - DELETE + - HEAD + - PATCH + type: string + path: + default: / + description: |- + The path to request on the HTTP server. + Defaults to "/" if not specified. + pattern: ^/.* + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "8080") or a named port defined in the container spec. + type: string + scheme: + default: HTTP + description: |- + The scheme to use for connecting to the host. + Defaults to "HTTP". + enum: + - HTTP + - HTTPS + type: string + required: + - port + type: object + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. + + + The conditions are as follows: + + + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. + + + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. + + + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. + + + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + Behavior based on the value: + - Positive (> 0): The action will be terminated after this many seconds. The maximum allowed value is 60. + - Zero (= 0): The timeout is managed by the system, defaulting to 30 seconds typically. + - Negative (< 0): No timeout is applied; the action runs until the command completes. + + + This field cannot be updated. + format: int32 + type: integer + type: object reconfigure: description: Defines the procedure that update replicas with new configuration. diff --git a/deploy/helm/crds/workloads.kubeblocks.io_instancesets.yaml b/deploy/helm/crds/workloads.kubeblocks.io_instancesets.yaml index e646b43d848..c73826228b4 100644 --- a/deploy/helm/crds/workloads.kubeblocks.io_instancesets.yaml +++ b/deploy/helm/crds/workloads.kubeblocks.io_instancesets.yaml @@ -2534,6 +2534,1305 @@ spec: description: Defines a set of hooks that customize the behavior of an Instance throughout its lifecycle. properties: + dataLoad: + description: |- + Defines the procedure for importing data into a replica. + InstanceSet only orchestrates the target replica side of this action. + Any source selection, dump, or streaming protocol remains the responsibility of the action implementation itself. + properties: + exec: + description: |- + Defines the command to run. + + + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. + + + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Specifies the name of the container within the same pod whose resources will be shared with the action. + This allows the action to utilize the specified container's resources without executing within it. + + + The name must match one of the containers defined in `componentDefinition.spec.runtime`. + + + The resources that can be shared are included: + + + - volume mounts + + + This field cannot be updated. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. + + + This field 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: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. + + + When specified, a dedicated container will be created using this image to execute the Action. + All actions with same image will share the same container. + + + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + grpc: + description: |- + Defines the gRPC call to issue. + + + This field cannot be updated. + properties: + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + description: Name of the method to invoke on the gRPC + service. + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "50051") or a named port defined in the container spec. + type: string + request: + additionalProperties: + type: string + description: |- + Request payload for the gRPC method. + + + Keys are proto field names (lowerCamelCase); values are strings that can include Go templates. + Templates are rendered with predefined action variables before the request is sent. + type: object + response: + description: Required response schema for the gRPC method. + properties: + message: + description: |- + Name of the field in the response whose value should be output. + Printed to stdout on success, or stderr on failure. + type: string + status: + description: |- + Name of the string field in the response that carries status information. + If non-empty, the action fails. + type: string + type: object + service: + description: Fully-qualified name of the gRPC service + to call. + type: string + required: + - method + - port + - service + type: object + http: + description: |- + Defines the HTTP request to perform. + + + This field cannot be updated. + properties: + body: + description: |- + Optional HTTP request body. + + + Supports Go text/template syntax; rendered with predefined variables before sending. + type: string + headers: + description: |- + Custom headers to set in the request. + Header values may use Go text/template syntax, rendered with predefined variables. + items: + description: HTTPHeader represents a single HTTP header + key/value pair. + properties: + name: + description: Name of the header field. + type: string + value: + description: Value of the header field. + type: string + required: + - name + - value + type: object + type: array + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + default: GET + description: |- + The HTTP method to use. + Defaults to "GET". + enum: + - GET + - POST + - PUT + - DELETE + - HEAD + - PATCH + type: string + path: + default: / + description: |- + The path to request on the HTTP server. + Defaults to "/" if not specified. + pattern: ^/.* + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "8080") or a named port defined in the container spec. + type: string + scheme: + default: HTTP + description: |- + The scheme to use for connecting to the host. + Defaults to "HTTP". + enum: + - HTTP + - HTTPS + type: string + required: + - port + type: object + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. + + + The conditions are as follows: + + + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. + + + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. + + + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. + + + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + Behavior based on the value: + - Positive (> 0): The action will be terminated after this many seconds. The maximum allowed value is 60. + - Zero (= 0): The timeout is managed by the system, defaulting to 30 seconds typically. + - Negative (< 0): No timeout is applied; the action runs until the command completes. + + + This field cannot be updated. + format: int32 + type: integer + type: object + memberJoin: + description: Defines the procedure to add a new replica into membership. + properties: + exec: + description: |- + Defines the command to run. + + + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. + + + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Specifies the name of the container within the same pod whose resources will be shared with the action. + This allows the action to utilize the specified container's resources without executing within it. + + + The name must match one of the containers defined in `componentDefinition.spec.runtime`. + + + The resources that can be shared are included: + + + - volume mounts + + + This field cannot be updated. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. + + + This field 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: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. + + + When specified, a dedicated container will be created using this image to execute the Action. + All actions with same image will share the same container. + + + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + grpc: + description: |- + Defines the gRPC call to issue. + + + This field cannot be updated. + properties: + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + description: Name of the method to invoke on the gRPC + service. + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "50051") or a named port defined in the container spec. + type: string + request: + additionalProperties: + type: string + description: |- + Request payload for the gRPC method. + + + Keys are proto field names (lowerCamelCase); values are strings that can include Go templates. + Templates are rendered with predefined action variables before the request is sent. + type: object + response: + description: Required response schema for the gRPC method. + properties: + message: + description: |- + Name of the field in the response whose value should be output. + Printed to stdout on success, or stderr on failure. + type: string + status: + description: |- + Name of the string field in the response that carries status information. + If non-empty, the action fails. + type: string + type: object + service: + description: Fully-qualified name of the gRPC service + to call. + type: string + required: + - method + - port + - service + type: object + http: + description: |- + Defines the HTTP request to perform. + + + This field cannot be updated. + properties: + body: + description: |- + Optional HTTP request body. + + + Supports Go text/template syntax; rendered with predefined variables before sending. + type: string + headers: + description: |- + Custom headers to set in the request. + Header values may use Go text/template syntax, rendered with predefined variables. + items: + description: HTTPHeader represents a single HTTP header + key/value pair. + properties: + name: + description: Name of the header field. + type: string + value: + description: Value of the header field. + type: string + required: + - name + - value + type: object + type: array + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + default: GET + description: |- + The HTTP method to use. + Defaults to "GET". + enum: + - GET + - POST + - PUT + - DELETE + - HEAD + - PATCH + type: string + path: + default: / + description: |- + The path to request on the HTTP server. + Defaults to "/" if not specified. + pattern: ^/.* + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "8080") or a named port defined in the container spec. + type: string + scheme: + default: HTTP + description: |- + The scheme to use for connecting to the host. + Defaults to "HTTP". + enum: + - HTTP + - HTTPS + type: string + required: + - port + type: object + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. + + + The conditions are as follows: + + + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. + + + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. + + + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. + + + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + Behavior based on the value: + - Positive (> 0): The action will be terminated after this many seconds. The maximum allowed value is 60. + - Zero (= 0): The timeout is managed by the system, defaulting to 30 seconds typically. + - Negative (< 0): No timeout is applied; the action runs until the command completes. + + + This field cannot be updated. + format: int32 + type: integer + type: object + memberLeave: + description: Defines the procedure to remove a replica from membership. + properties: + exec: + description: |- + Defines the command to run. + + + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. + + + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Specifies the name of the container within the same pod whose resources will be shared with the action. + This allows the action to utilize the specified container's resources without executing within it. + + + The name must match one of the containers defined in `componentDefinition.spec.runtime`. + + + The resources that can be shared are included: + + + - volume mounts + + + This field cannot be updated. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. + + + This field 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: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. + + + When specified, a dedicated container will be created using this image to execute the Action. + All actions with same image will share the same container. + + + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + grpc: + description: |- + Defines the gRPC call to issue. + + + This field cannot be updated. + properties: + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + description: Name of the method to invoke on the gRPC + service. + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "50051") or a named port defined in the container spec. + type: string + request: + additionalProperties: + type: string + description: |- + Request payload for the gRPC method. + + + Keys are proto field names (lowerCamelCase); values are strings that can include Go templates. + Templates are rendered with predefined action variables before the request is sent. + type: object + response: + description: Required response schema for the gRPC method. + properties: + message: + description: |- + Name of the field in the response whose value should be output. + Printed to stdout on success, or stderr on failure. + type: string + status: + description: |- + Name of the string field in the response that carries status information. + If non-empty, the action fails. + type: string + type: object + service: + description: Fully-qualified name of the gRPC service + to call. + type: string + required: + - method + - port + - service + type: object + http: + description: |- + Defines the HTTP request to perform. + + + This field cannot be updated. + properties: + body: + description: |- + Optional HTTP request body. + + + Supports Go text/template syntax; rendered with predefined variables before sending. + type: string + headers: + description: |- + Custom headers to set in the request. + Header values may use Go text/template syntax, rendered with predefined variables. + items: + description: HTTPHeader represents a single HTTP header + key/value pair. + properties: + name: + description: Name of the header field. + type: string + value: + description: Value of the header field. + type: string + required: + - name + - value + type: object + type: array + host: + description: |- + The target host to connect to. + Defaults to "127.0.0.1" if not specified. + type: string + method: + default: GET + description: |- + The HTTP method to use. + Defaults to "GET". + enum: + - GET + - POST + - PUT + - DELETE + - HEAD + - PATCH + type: string + path: + default: / + description: |- + The path to request on the HTTP server. + Defaults to "/" if not specified. + pattern: ^/.* + type: string + port: + description: |- + The port to access on the host. + It may be a numeric string (e.g., "8080") or a named port defined in the container spec. + type: string + scheme: + default: HTTP + description: |- + The scheme to use for connecting to the host. + Defaults to "HTTP". + enum: + - HTTP + - HTTPS + type: string + required: + - port + type: object + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + type: string + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. + + + The conditions are as follows: + + + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. + + + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. + + + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. + + + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + If not specified, the Action will be executed in the pod where the Action is triggered, such as the pod + to be removed or added; or a random pod if the Action is triggered at the component level, such as + post-provision or pre-terminate of the component. + + + This field cannot be updated. + enum: + - Any + - All + - Role + - Ordinal + type: string + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + Behavior based on the value: + - Positive (> 0): The action will be terminated after this many seconds. The maximum allowed value is 60. + - Zero (= 0): The timeout is managed by the system, defaulting to 30 seconds typically. + - Negative (< 0): No timeout is applied; the action runs until the command completes. + + + This field cannot be updated. + format: int32 + type: integer + type: object reconfigure: description: Defines the procedure that update replicas with new configuration. @@ -11789,10 +13088,20 @@ spec: - name type: object type: array + dataLoaded: + description: Represents whether the instance data is loaded. + type: boolean + memberJoined: + description: Represents whether the instance has joined the + cluster membership. + type: boolean podName: default: Unknown description: Represents the name of the pod. type: string + provisioned: + description: Represents whether the instance is provisioned. + type: boolean role: description: Represents the role of the instance observed. type: string diff --git a/docs/developer_docs/api-reference/cluster.md b/docs/developer_docs/api-reference/cluster.md index f12f3556228..b75e5340d8f 100644 --- a/docs/developer_docs/api-reference/cluster.md +++ b/docs/developer_docs/api-reference/cluster.md @@ -33950,6 +33950,42 @@ string +provisioned
+ +bool + + + +(Optional) +

Represents whether the instance is provisioned.

+ + + + +dataLoaded
+ +bool + + + +(Optional) +

Represents whether the instance data is loaded.

+ + + + +memberJoined
+ +bool + + + +(Optional) +

Represents whether the instance has joined the cluster membership.

+ + + + volumeExpansion
bool @@ -34387,6 +34423,50 @@ Action +memberJoin
+ + +Action + + + + +(Optional) +

Defines the procedure to add a new replica into membership.

+ + + + +memberLeave
+ + +Action + + + + +(Optional) +

Defines the procedure to remove a replica from membership.

+ + + + +dataLoad
+ + +Action + + + + +(Optional) +

Defines the procedure for importing data into a replica. +InstanceSet only orchestrates the target replica side of this action. +Any source selection, dump, or streaming protocol remains the responsibility of the action implementation itself.

+ + + + reconfigure
diff --git a/pkg/controller/builder/builder_instance_set.go b/pkg/controller/builder/builder_instance_set.go index 3c8df719774..d2bc197b2b4 100644 --- a/pkg/controller/builder/builder_instance_set.go +++ b/pkg/controller/builder/builder_instance_set.go @@ -133,6 +133,9 @@ func (builder *InstanceSetBuilder) SetLifecycleActions(lifecycleActions *kbappsv } if lifecycleActions != nil { builder.get().Spec.LifecycleActions.Switchover = lifecycleActions.Switchover + builder.get().Spec.LifecycleActions.MemberJoin = lifecycleActions.MemberJoin + builder.get().Spec.LifecycleActions.MemberLeave = lifecycleActions.MemberLeave + builder.get().Spec.LifecycleActions.DataLoad = lifecycleActions.DataLoad builder.get().Spec.LifecycleActions.Reconfigure = lifecycleActions.Reconfigure } if templateVars != nil { diff --git a/pkg/controller/component/kbagent.go b/pkg/controller/component/kbagent.go index 9b038b7692b..e838d3dc1da 100644 --- a/pkg/controller/component/kbagent.go +++ b/pkg/controller/component/kbagent.go @@ -96,28 +96,28 @@ func UpdateKBAgentContainer4HostNetwork(synthesizedComp *SynthesizedComponent) { synthesizedComp.PodSpec.Containers[idx] = *c } -func buildKBAgentTaskEnv(task proto.Task) (map[string]string, error) { - envVar, err := kbagent.BuildEnv4Worker([]proto.Task{task}) - if err != nil { - return nil, err - } - return map[string]string{ - envVar.Name: envVar.Value, - }, nil -} - -func updateKBAgentTaskEnv(envVars map[string]string, f func(proto.Task) *proto.Task) (map[string]string, error) { - envVar, err := kbagent.UpdateEnv4Worker(envVars, f) - if err != nil { - return nil, err - } - if envVar == nil { - return nil, nil - } - return map[string]string{ - envVar.Name: envVar.Value, - }, nil -} +// func buildKBAgentTaskEnv(task proto.Task) (map[string]string, error) { +// envVar, err := kbagent.BuildEnv4Worker([]proto.Task{task}) +// if err != nil { +// return nil, err +// } +// return map[string]string{ +// envVar.Name: envVar.Value, +// }, nil +// } +// +// func updateKBAgentTaskEnv(envVars map[string]string, f func(proto.Task) *proto.Task) (map[string]string, error) { +// envVar, err := kbagent.UpdateEnv4Worker(envVars, f) +// if err != nil { +// return nil, err +// } +// if envVar == nil { +// return nil, nil +// } +// return map[string]string{ +// envVar.Name: envVar.Value, +// }, nil +// } func buildKBAgentContainer(synthesizedComp *SynthesizedComponent) error { if !hasActionDefined(synthesizedComp) { diff --git a/pkg/controller/component/kbagent_task_event.go b/pkg/controller/component/kbagent_task_event.go deleted file mode 100644 index d48e576c3d0..00000000000 --- a/pkg/controller/component/kbagent_task_event.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright (C) 2022-2025 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -*/ - -package component - -import ( - "encoding/json" - "fmt" - - corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/tools/record" - "sigs.k8s.io/controller-runtime/pkg/client" - - intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" - "github.com/apecloud/kubeblocks/pkg/kbagent/proto" -) - -type KBAgentTaskEventHandler struct{} - -func (h *KBAgentTaskEventHandler) Handle(cli client.Client, reqCtx intctrlutil.RequestCtx, recorder record.EventRecorder, event *corev1.Event) error { - if !h.isTaskEvent(event) { - return nil - } - - taskEvent := &proto.TaskEvent{} - if err := json.Unmarshal([]byte(event.Message), taskEvent); err != nil { - return err - } - - return h.handleEvent(reqCtx, cli, event.InvolvedObject.Namespace, *taskEvent) -} - -func (h *KBAgentTaskEventHandler) isTaskEvent(event *corev1.Event) bool { - return event.ReportingController == proto.ProbeEventReportingController && - event.Reason == "task" && event.InvolvedObject.FieldPath == proto.ProbeEventFieldPath -} - -func (h *KBAgentTaskEventHandler) handleEvent(reqCtx intctrlutil.RequestCtx, cli client.Client, namespace string, event proto.TaskEvent) error { - if event.Task == newReplicaTask { - return handleNewReplicaTaskEvent(reqCtx.Log, reqCtx.Ctx, cli, namespace, event) - } - return fmt.Errorf("unsupported kind of task event: %s", event.Task) -} diff --git a/pkg/controller/component/replicas.go b/pkg/controller/component/replicas.go deleted file mode 100644 index 0e713fb05a7..00000000000 --- a/pkg/controller/component/replicas.go +++ /dev/null @@ -1,399 +0,0 @@ -/* -Copyright (C) 2022-2025 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -*/ - -package component - -import ( - "context" - "encoding/json" - "fmt" - "slices" - "strings" - "time" - - "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/client" - - workloads "github.com/apecloud/kubeblocks/apis/workloads/v1" - "github.com/apecloud/kubeblocks/pkg/constant" - intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" - "github.com/apecloud/kubeblocks/pkg/kbagent" - "github.com/apecloud/kubeblocks/pkg/kbagent/proto" -) - -const ( - replicaStatusAnnotationKey = "apps.kubeblocks.io/replicas-status" - - // new replicas task & event - newReplicaTask = "newReplica" - defaultNewReplicaTaskReportPeriodSeconds = 60 -) - -type ReplicasStatus struct { - Replicas int32 `json:"replicas"` - Status []ReplicaStatus `json:"status"` -} - -type ReplicaStatus struct { - Name string `json:"name"` - Generation string `json:"generation"` - CreationTimestamp time.Time `json:"creationTimestamp"` - DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` - Message string `json:"message,omitempty"` - Provisioned bool `json:"provisioned,omitempty"` - DataLoaded *bool `json:"dataLoaded,omitempty"` - MemberJoined *bool `json:"memberJoined,omitempty"` - Reconfigured *string `json:"reconfigured,omitempty"` // TODO: component status -} - -func BuildReplicasStatus(running, proto *workloads.InstanceSet) { - if running == nil || proto == nil { - return - } - annotations := running.Annotations - if annotations == nil { - return - } - message, ok := annotations[replicaStatusAnnotationKey] - if !ok { - return - } - if proto.Annotations == nil { - proto.Annotations = make(map[string]string) - } - proto.Annotations[replicaStatusAnnotationKey] = message -} - -func NewReplicasStatus(its *workloads.InstanceSet, replicas []string, hasMemberJoin, hasDataAction bool) error { - loaded := func() *bool { - if hasDataAction { - return ptr.To(false) - } - return nil - }() - joined := func() *bool { - if hasMemberJoin { - return ptr.To(false) - } - return nil - }() - return UpdateReplicasStatusFunc(its, func(status *ReplicasStatus) error { - status.Replicas = *its.Spec.Replicas - if status.Status == nil { - status.Status = make([]ReplicaStatus, 0) - } - for _, name := range replicas { - if slices.ContainsFunc(status.Status, func(s ReplicaStatus) bool { - return s.Name == name - }) { - continue - } - status.Status = append(status.Status, ReplicaStatus{ - Name: name, - Generation: compGenerationFromITS(its), - CreationTimestamp: time.Now(), - Provisioned: false, - DataLoaded: loaded, - MemberJoined: joined, - }) - } - return nil - }) -} - -func DeleteReplicasStatus(its *workloads.InstanceSet, replicas []string, f func(status ReplicaStatus)) error { - return UpdateReplicasStatusFunc(its, func(status *ReplicasStatus) error { - status.Replicas = *its.Spec.Replicas - status.Status = slices.DeleteFunc(status.Status, func(s ReplicaStatus) bool { - if slices.Contains(replicas, s.Name) { - if f != nil { - f(s) - } - return true - } - return false - }) - return nil - }) -} - -func StatusReplicasStatus(its *workloads.InstanceSet, replicas []string, hasMemberJoin, hasDataAction bool) error { - loaded := func() *bool { - if hasDataAction { - return ptr.To(true) - } - return nil - }() - joined := func() *bool { - if hasMemberJoin { - return ptr.To(true) - } - return nil - }() - return UpdateReplicasStatusFunc(its, func(status *ReplicasStatus) error { - status.Replicas = *its.Spec.Replicas - if status.Status == nil { - status.Status = make([]ReplicaStatus, 0) - } - for _, replica := range replicas { - i := slices.IndexFunc(status.Status, func(s ReplicaStatus) bool { - return s.Name == replica - }) - if i >= 0 { - status.Status[i].Provisioned = true - } else { - status.Status = append(status.Status, ReplicaStatus{ - Name: replica, - Generation: compGenerationFromITS(its), - CreationTimestamp: its.CreationTimestamp.Time, - Provisioned: true, - DataLoaded: loaded, - MemberJoined: joined, - }) - } - } - return nil - }) -} - -func UpdateReplicasStatusFunc(its *workloads.InstanceSet, f func(status *ReplicasStatus) error) error { - if f == nil { - return nil - } - - status, err := getReplicasStatus(its) - if err != nil { - return err - } - - if err = f(&status); err != nil { - return err - } - - return setReplicasStatus(its, status) -} - -func GetReplicasStatusFunc(its *workloads.InstanceSet, f func(ReplicaStatus) bool) ([]string, error) { - if f == nil { - return nil, nil - } - status, err := getReplicasStatus(its) - if err != nil { - return nil, err - } - replicas := make([]string, 0) - for _, s := range status.Status { - if f(s) { - replicas = append(replicas, s.Name) - } - } - return replicas, nil -} - -func NewReplicaTask(compName, uid string, source *corev1.Pod, replicas []string) (map[string]string, error) { - port, err := intctrlutil.GetPortByName(*source, kbagent.ContainerName, kbagent.DefaultStreamingPortName) - if err != nil { - return nil, err - } - task := proto.Task{ - Instance: compName, - Task: newReplicaTask, - UID: uid, - Replicas: strings.Join(replicas, ","), - NotifyAtFinish: true, - ReportPeriodSeconds: defaultNewReplicaTaskReportPeriodSeconds, - NewReplica: &proto.NewReplicaTask{ - Remote: intctrlutil.PodFQDN(source.Namespace, compName, source.Name), - Port: port, - Replicas: strings.Join(replicas, ","), - }, - } - return buildKBAgentTaskEnv(task) -} - -func compGenerationFromITS(its *workloads.InstanceSet) string { - if its == nil { - return "" - } - annotations := its.Annotations - if annotations == nil { - return "" - } - return annotations[constant.KubeBlocksGenerationKey] -} - -func getReplicasStatus(its *workloads.InstanceSet) (ReplicasStatus, error) { - if its == nil { - return ReplicasStatus{}, nil - } - annotations := its.GetAnnotations() - if annotations == nil { - return ReplicasStatus{}, nil - } - message, ok := annotations[replicaStatusAnnotationKey] - if !ok { - return ReplicasStatus{}, nil - } - status := &ReplicasStatus{} - err := json.Unmarshal([]byte(message), &status) - if err != nil { - return ReplicasStatus{}, err - } - return *status, nil -} - -func setReplicasStatus(its *workloads.InstanceSet, status ReplicasStatus) error { - if its == nil { - return nil - } - out, err := json.Marshal(&status) - if err != nil { - return err - } - annotations := its.GetAnnotations() - if annotations == nil { - annotations = make(map[string]string) - } - annotations[replicaStatusAnnotationKey] = string(out) - its.SetAnnotations(annotations) - return nil -} - -func handleNewReplicaTaskEvent(logger logr.Logger, ctx context.Context, cli client.Client, namespace string, event proto.TaskEvent) error { - key := types.NamespacedName{ - Namespace: namespace, - Name: event.Instance, - } - its := &workloads.InstanceSet{} - if err := cli.Get(ctx, key, its); err != nil { - logger.Error(err, "get ITS failed when handle new replica task event", - "code", event.Code, "finished", !event.EndTime.IsZero(), "message", event.Message) - return err - } - - var err error - finished := !event.EndTime.IsZero() - switch { - case finished && event.Code == 0: - err = handleNewReplicaTaskEvent4Finished(ctx, cli, its, event) - case finished: - err = handleNewReplicaTaskEvent4Failed(ctx, cli, its, event) - default: - err = handleNewReplicaTaskEvent4Unfinished(ctx, cli, its, event) - } - if err != nil { - logger.Error(err, "handle new replica task event failed", - "code", event.Code, "finished", finished, "message", event.Message) - } else { - logger.Info("handle new replica task event success", - "code", event.Code, "finished", finished, "message", event.Message) - } - return err -} - -func handleNewReplicaTaskEvent4Finished(ctx context.Context, cli client.Client, its *workloads.InstanceSet, event proto.TaskEvent) error { - if err := func() error { - envKey := types.NamespacedName{ - Namespace: its.Namespace, - Name: constant.GetCompEnvCMName(its.Name), - } - obj := &corev1.ConfigMap{} - err := cli.Get(ctx, envKey, obj) - if err != nil { - return err - } - - parameters, err := updateKBAgentTaskEnv(obj.Data, func(task proto.Task) *proto.Task { - if task.Task == newReplicaTask { - replicas := strings.Split(task.Replicas, ",") - replicas = slices.DeleteFunc(replicas, func(r string) bool { - return r == event.Replica - }) - if len(replicas) == 0 { - return nil - } - task.Replicas = strings.Join(replicas, ",") - if task.NewReplica != nil { - task.NewReplica.Replicas = task.Replicas - } - } - return &task - }) - if err != nil { - return err - } - if parameters == nil { - return nil // do nothing - } - - if obj.Data == nil { - obj.Data = make(map[string]string) - } - for k, v := range parameters { - obj.Data[k] = v - } - return cli.Update(ctx, obj) - }(); err != nil { - return err - } - return updateReplicaStatusFunc(ctx, cli, its, event.Replica, func(status *ReplicaStatus) error { - status.Message = "" - status.Provisioned = true - status.DataLoaded = ptr.To(true) - return nil - }) -} - -func handleNewReplicaTaskEvent4Unfinished(ctx context.Context, cli client.Client, its *workloads.InstanceSet, event proto.TaskEvent) error { - return updateReplicaStatusFunc(ctx, cli, its, event.Replica, func(status *ReplicaStatus) error { - status.Message = event.Message - status.Provisioned = true - status.DataLoaded = ptr.To(false) - return nil - }) -} - -func handleNewReplicaTaskEvent4Failed(ctx context.Context, cli client.Client, its *workloads.InstanceSet, event proto.TaskEvent) error { - return updateReplicaStatusFunc(ctx, cli, its, event.Replica, func(status *ReplicaStatus) error { - status.Message = event.Message - status.Provisioned = true - return nil - }) -} - -func updateReplicaStatusFunc(ctx context.Context, cli client.Client, - its *workloads.InstanceSet, replicaName string, f func(*ReplicaStatus) error) error { - if err := UpdateReplicasStatusFunc(its, func(status *ReplicasStatus) error { - for i := range status.Status { - if status.Status[i].Name == replicaName { - if f != nil { - return f(&status.Status[i]) - } - return nil - } - } - return fmt.Errorf("replica %s not found", replicaName) - }); err != nil { - return err - } - return cli.Update(ctx, its) -} diff --git a/pkg/controller/component/replicas_test.go b/pkg/controller/component/replicas_test.go deleted file mode 100644 index 8841f564f0b..00000000000 --- a/pkg/controller/component/replicas_test.go +++ /dev/null @@ -1,293 +0,0 @@ -/* -Copyright (C) 2022-2025 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -*/ - -package component - -import ( - "slices" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" - - workloads "github.com/apecloud/kubeblocks/apis/workloads/v1" - "github.com/apecloud/kubeblocks/pkg/constant" -) - -var _ = Describe("replicas", func() { - var ( - its *workloads.InstanceSet - replicas []string - ) - - cleanEnv := func() { - // must wait till resources deleted and no longer existed before the testcases start, - // otherwise if later it needs to create some new resource objects with the same name, - // in race conditions, it will find the existence of old objects, resulting failure to - // create the new objects. - By("clean resources") - } - - BeforeEach(func() { - cleanEnv() - }) - - AfterEach(func() { - cleanEnv() - }) - - Context("status", func() { - BeforeEach(func() { - its = &workloads.InstanceSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testCtx.DefaultNamespace, - Name: "test-cluster-its", - CreationTimestamp: metav1.Now(), - Annotations: map[string]string{ - constant.KubeBlocksGenerationKey: "1", - }, - }, - Spec: workloads.InstanceSetSpec{ - Replicas: ptr.To[int32](3), - }, - } - replicas = []string{"test-cluster-its-0", "test-cluster-its-1", "test-cluster-its-2"} - }) - - It("status init replicas", func() { - Expect(StatusReplicasStatus(its, replicas, true, true)).Should(Succeed()) - Expect(its.Annotations).Should(HaveKey(replicaStatusAnnotationKey)) - - status, err := getReplicasStatus(its) - Expect(err).Should(BeNil()) - Expect(status.Replicas).Should(Equal(int32(3))) - Expect(status.Status).Should(HaveLen(int(status.Replicas))) - for _, s := range status.Status { - Expect(replicas).Should(ContainElement(s.Name)) - Expect(s.Generation).Should(Equal("1")) - Expect(s.CreationTimestamp.Equal(its.CreationTimestamp.Time)).Should(BeTrue()) - Expect(s.Provisioned).Should(BeTrue()) - Expect(s.DataLoaded).ShouldNot(BeNil()) - Expect(*s.DataLoaded).Should(BeTrue()) - Expect(s.MemberJoined).ShouldNot(BeNil()) - Expect(*s.MemberJoined).Should(BeTrue()) - } - }) - - It("new replicas", func() { - Expect(StatusReplicasStatus(its, replicas, true, true)).Should(Succeed()) - - its.Annotations[constant.KubeBlocksGenerationKey] = "2" - its.Spec.Replicas = ptr.To[int32](5) - newReplicas := []string{"test-cluster-its-3", "test-cluster-its-4"} - Expect(NewReplicasStatus(its, newReplicas, true, true)).Should(Succeed()) - - status, err := getReplicasStatus(its) - Expect(err).Should(BeNil()) - Expect(status.Replicas).Should(Equal(int32(5))) - Expect(status.Status).Should(HaveLen(int(status.Replicas))) - for _, s := range status.Status { - if slices.Contains(newReplicas, s.Name) { - Expect(s.Generation).Should(Equal("2")) - Expect(s.CreationTimestamp.Equal(its.CreationTimestamp.Time)).Should(BeFalse()) - Expect(s.Provisioned).Should(BeFalse()) - Expect(s.DataLoaded).ShouldNot(BeNil()) - Expect(*s.DataLoaded).Should(BeFalse()) - Expect(s.MemberJoined).ShouldNot(BeNil()) - Expect(*s.MemberJoined).Should(BeFalse()) - } - } - }) - - It("delete replicas", func() { - Expect(StatusReplicasStatus(its, replicas, true, true)).Should(Succeed()) - - its.Annotations[constant.KubeBlocksGenerationKey] = "2" - its.Spec.Replicas = ptr.To[int32](2) - deleteReplicas := []string{"test-cluster-its-2"} - Expect(DeleteReplicasStatus(its, deleteReplicas, func(s ReplicaStatus) { - Expect(s.Provisioned).Should(BeTrue()) - Expect(s.DataLoaded).ShouldNot(BeNil()) - Expect(*s.DataLoaded).Should(BeTrue()) - Expect(s.MemberJoined).ShouldNot(BeNil()) - Expect(*s.MemberJoined).Should(BeTrue()) - })).Should(Succeed()) - - status, err := getReplicasStatus(its) - Expect(err).Should(BeNil()) - Expect(status.Replicas).Should(Equal(int32(2))) - Expect(status.Status).Should(HaveLen(int(status.Replicas))) - }) - - It("status new replicas", func() { - Expect(StatusReplicasStatus(its, replicas, true, true)).Should(Succeed()) - - its.Annotations[constant.KubeBlocksGenerationKey] = "2" - its.Spec.Replicas = ptr.To[int32](5) - newReplicas := []string{"test-cluster-its-3", "test-cluster-its-4"} - Expect(NewReplicasStatus(its, newReplicas, true, true)).Should(Succeed()) - - replicas = append(replicas, "test-cluster-its-3") - Expect(StatusReplicasStatus(its, replicas, true, true)).Should(Succeed()) - - status, err := getReplicasStatus(its) - Expect(err).Should(BeNil()) - for _, s := range status.Status { - if s.Name == "test-cluster-its-3" { - Expect(s.Provisioned).Should(BeTrue()) // provisioned - Expect(s.DataLoaded).ShouldNot(BeNil()) - Expect(*s.DataLoaded).Should(BeFalse()) // not loaded - Expect(s.MemberJoined).ShouldNot(BeNil()) - Expect(*s.MemberJoined).Should(BeFalse()) // not joined - } - } - }) - - It("delete new replicas", func() { - Expect(StatusReplicasStatus(its, replicas, true, true)).Should(Succeed()) - - its.Annotations[constant.KubeBlocksGenerationKey] = "2" - its.Spec.Replicas = ptr.To[int32](5) - newReplicas := []string{"test-cluster-its-3", "test-cluster-its-4"} - Expect(NewReplicasStatus(its, newReplicas, true, true)).Should(Succeed()) - - its.Annotations[constant.KubeBlocksGenerationKey] = "3" - its.Spec.Replicas = ptr.To[int32](4) - deleteReplicas := []string{"test-cluster-its-4"} - Expect(DeleteReplicasStatus(its, deleteReplicas, func(s ReplicaStatus) { - Expect(s.Provisioned).Should(BeFalse()) - Expect(s.DataLoaded).ShouldNot(BeNil()) - Expect(*s.DataLoaded).Should(BeFalse()) - Expect(s.MemberJoined).ShouldNot(BeNil()) - Expect(*s.MemberJoined).Should(BeFalse()) - })).Should(Succeed()) - }) - - // It("task event for new replicas - succeed", func() { - // Expect(StatusReplicasStatus(its, replicas, true, true)).Should(Succeed()) - // - // its.Annotations[constant.KubeBlocksGenerationKey] = "2" - // its.Spec.Replicas = ptr.To[int32](5) - // newReplicas := []string{"test-cluster-its-3", "test-cluster-its-4"} - // Expect(NewReplicasStatus(its, newReplicas, true, true)).Should(Succeed()) - // - // cli := testutil.NewK8sMockClient() - // cli.MockGetMethod(testutil.WithGetReturned(func(key client.ObjectKey, obj client.Object) error { - // // TODO: mock - // return fmt.Errorf("not found") - // }, testutil.WithAnyTimes())) - // cli.MockUpdateMethod(testutil.WithSucceed(testutil.WithAnyTimes())) - // event := proto.TaskEvent{ - // Instance: "test-cluster-its", - // Replica: "test-cluster-its-3", - // EndTime: time.Now(), - // Code: 0, - // } - // Expect(handleNewReplicaTaskEvent(logger, testCtx.Ctx, cli.Client(), testCtx.DefaultNamespace, event)).Should(Succeed()) - // - // status, err := getReplicasStatus(its) - // Expect(err).Should(BeNil()) - // for _, s := range status.Status { - // if s.Name == "test-cluster-its-3" { - // Expect(s.Provisioned).Should(BeTrue()) // provisioned - // Expect(s.DataLoaded).ShouldNot(BeNil()) - // Expect(*s.DataLoaded).Should(BeTrue()) // loaded - // Expect(s.MemberJoined).ShouldNot(BeNil()) - // Expect(*s.MemberJoined).Should(BeFalse()) // not joined - // } - // } - // }) - // - // It("task event for new replicas - failed", func() { - // Expect(StatusReplicasStatus(its, replicas, true, true)).Should(Succeed()) - // - // its.Annotations[constant.KubeBlocksGenerationKey] = "2" - // its.Spec.Replicas = ptr.To[int32](5) - // newReplicas := []string{"test-cluster-its-3", "test-cluster-its-4"} - // Expect(NewReplicasStatus(its, newReplicas, true, true)).Should(Succeed()) - // - // cli := testutil.NewK8sMockClient() - // cli.MockGetMethod(testutil.WithGetReturned(func(key client.ObjectKey, obj client.Object) error { - // // TODO: mock - // return fmt.Errorf("not found") - // }, testutil.WithAnyTimes())) - // cli.MockUpdateMethod(testutil.WithSucceed(testutil.WithAnyTimes())) - // event := proto.TaskEvent{ - // Instance: "test-cluster-its", - // Replica: "test-cluster-its-3", - // EndTime: time.Now(), - // Code: -1, - // Message: "failed", - // } - // Expect(handleNewReplicaTaskEvent(logger, testCtx.Ctx, cli.Client(), testCtx.DefaultNamespace, event)).Should(Succeed()) - // - // status, err := getReplicasStatus(its) - // Expect(err).Should(BeNil()) - // for _, s := range status.Status { - // if s.Name == "test-cluster-its-3" { - // Expect(s.Provisioned).Should(BeTrue()) // provisioned - // Expect(s.DataLoaded).ShouldNot(BeNil()) - // Expect(*s.DataLoaded).Should(BeFalse()) // not loaded - // Expect(s.MemberJoined).ShouldNot(BeNil()) - // Expect(*s.MemberJoined).Should(BeFalse()) // not joined - // Expect(s.Message).Should(Equal("failed")) - // } - // } - // }) - // - // It("task event for new replicas - in progress", func() { - // Expect(StatusReplicasStatus(its, replicas, true, true)).Should(Succeed()) - // - // its.Annotations[constant.KubeBlocksGenerationKey] = "2" - // its.Spec.Replicas = ptr.To[int32](5) - // newReplicas := []string{"test-cluster-its-3", "test-cluster-its-4"} - // Expect(NewReplicasStatus(its, newReplicas, true, true)).Should(Succeed()) - // - // cli := testutil.NewK8sMockClient() - // cli.MockGetMethod(testutil.WithGetReturned(func(key client.ObjectKey, obj client.Object) error { - // // TODO: mock - // return fmt.Errorf("not found") - // }, testutil.WithAnyTimes())) - // cli.MockUpdateMethod(testutil.WithSucceed(testutil.WithAnyTimes())) - // event := proto.TaskEvent{ - // Instance: "test-cluster-its", - // Replica: "test-cluster-its-3", - // // EndTime: time.Now(), - // Code: 0, - // Message: "90", - // } - // Expect(handleNewReplicaTaskEvent(logger, testCtx.Ctx, cli.Client(), testCtx.DefaultNamespace, event)).Should(Succeed()) - // - // status, err := getReplicasStatus(its) - // Expect(err).Should(BeNil()) - // for _, s := range status.Status { - // if s.Name == "test-cluster-its-3" { - // Expect(s.Provisioned).Should(BeTrue()) // provisioned - // Expect(s.DataLoaded).ShouldNot(BeNil()) - // Expect(*s.DataLoaded).Should(BeFalse()) // not loaded - // Expect(s.MemberJoined).ShouldNot(BeNil()) - // Expect(*s.MemberJoined).Should(BeFalse()) // not joined - // Expect(s.Message).Should(Equal("90")) - // } - // } - // }) - }) -}) diff --git a/pkg/controller/instanceset/reconciler_instance_alignment.go b/pkg/controller/instanceset/reconciler_instance_alignment.go index fdefc061614..3db9fa5a89f 100644 --- a/pkg/controller/instanceset/reconciler_instance_alignment.go +++ b/pkg/controller/instanceset/reconciler_instance_alignment.go @@ -20,15 +20,21 @@ along with this program. If not, see . package instanceset import ( + "errors" + "slices" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/sets" + "sigs.k8s.io/controller-runtime/pkg/client" kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" workloads "github.com/apecloud/kubeblocks/apis/workloads/v1" "github.com/apecloud/kubeblocks/pkg/constant" + "github.com/apecloud/kubeblocks/pkg/controller/graph" "github.com/apecloud/kubeblocks/pkg/controller/instancetemplate" "github.com/apecloud/kubeblocks/pkg/controller/kubebuilderx" + "github.com/apecloud/kubeblocks/pkg/controller/lifecycle" "github.com/apecloud/kubeblocks/pkg/controller/model" intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" ) @@ -182,14 +188,31 @@ func (r *instanceAlignmentReconciler) Reconcile(tree *kubebuilderx.ObjectTree) ( } } + if retryAfter, err := r.reconcileScaleOutLifecycle(tree, its); err != nil { + return kubebuilderx.Continue, err + } else if retryAfter { + return kubebuilderx.RetryAfter(0), nil + } + // delete useless instances priorities := make(map[string]int) sortObjects(oldInstanceList, priorities, false) + serialLifecycle := getMemberUpdateStrategy(its) == workloads.SerialUpdateStrategy + scaleInBatchNames, err := r.selectScaleInBatchNames(its, oldInstanceList, deleteNameSet) + if err != nil { + return kubebuilderx.Continue, err + } + if len(scaleInBatchNames) > 0 && concurrency < len(scaleInBatchNames) { + concurrency = len(scaleInBatchNames) + } for _, object := range oldInstanceList { pod, _ := object.(*corev1.Pod) if _, ok := deleteNameSet[pod.Name]; !ok { continue } + if len(scaleInBatchNames) > 0 && !scaleInBatchNames.Has(pod.Name) { + continue + } if !isOrderedReady && concurrency <= 0 { break } @@ -199,6 +222,13 @@ func (r *instanceAlignmentReconciler) Reconcile(tree *kubebuilderx.ObjectTree) ( its.Name, pod.Name) } + if retryAfter, err := r.reconcileScaleInLifecycle(tree, its, pod); err != nil { + return kubebuilderx.Continue, err + } else if retryAfter { + return kubebuilderx.RetryAfter(0), nil + } + status := findInstanceStatus(its, pod.Name) + joined := status != nil && status.MemberJoined != nil && *status.MemberJoined if err := tree.Delete(pod); err != nil { return kubebuilderx.Continue, err } @@ -221,10 +251,177 @@ func (r *instanceAlignmentReconciler) Reconcile(tree *kubebuilderx.ObjectTree) ( if isOrderedReady { break } + if serialLifecycle && joined { + break + } concurrency-- } return kubebuilderx.Continue, nil } +func (r *instanceAlignmentReconciler) reconcileScaleOutLifecycle(tree *kubebuilderx.ObjectTree, its *workloads.InstanceSet) (bool, error) { + if its.Spec.LifecycleActions == nil { + return false, nil + } + pods := sortedPods(tree.List(&corev1.Pod{})) + serialLifecycle := getMemberUpdateStrategy(its) == workloads.SerialUpdateStrategy + for _, pod := range pods { + status := findInstanceStatus(its, pod.Name) + if status == nil || !intctrlutil.IsPodAvailable(pod, its.Spec.MinReadySeconds) { + continue + } + if status.DataLoaded != nil && !*status.DataLoaded { + done, err := r.runDataLoad(tree, its, pod, status) + if err != nil { + return false, err + } + if serialLifecycle || !done { + return true, nil + } + } + if status.MemberJoined != nil && !*status.MemberJoined { + done, err := r.runMemberJoin(tree, its, pod, status) + if err != nil { + return false, err + } + if serialLifecycle || !done { + return true, nil + } + } + } + return false, nil +} + +func (r *instanceAlignmentReconciler) runDataLoad(tree *kubebuilderx.ObjectTree, its *workloads.InstanceSet, pod *corev1.Pod, status *workloads.InstanceStatus) (bool, error) { + // InstanceSet only orchestrates target-side initialization here. + // Source-side data export/streaming remains an implementation detail of the lifecycle action itself. + lfa, err := newLifecycleAction(its, tree, pod) + if err != nil { + return false, err + } + if err = lfa.DataLoad(tree.Context, tree.Reader, nil); err != nil { + if errors.Is(err, lifecycle.ErrActionNotDefined) { + done := true + status.DataLoaded = &done + return true, nil + } + return false, err + } + done := true + status.DataLoaded = &done + return true, nil +} + +func (r *instanceAlignmentReconciler) runMemberJoin(tree *kubebuilderx.ObjectTree, its *workloads.InstanceSet, pod *corev1.Pod, status *workloads.InstanceStatus) (bool, error) { + lfa, err := newLifecycleAction(its, tree, pod) + if err != nil { + return false, err + } + if err = lfa.MemberJoin(tree.Context, tree.Reader, nil); err != nil { + if errors.Is(err, lifecycle.ErrActionNotDefined) { + done := true + status.MemberJoined = &done + return true, nil + } + return false, err + } + done := true + status.MemberJoined = &done + return true, nil +} + +func (r *instanceAlignmentReconciler) reconcileScaleInLifecycle(tree *kubebuilderx.ObjectTree, its *workloads.InstanceSet, pod *corev1.Pod) (bool, error) { + status := findInstanceStatus(its, pod.Name) + if status == nil || status.MemberJoined == nil || !*status.MemberJoined { + return false, nil + } + lfa, err := newLifecycleAction(its, tree, pod) + if err != nil { + return false, err + } + if err = lfa.MemberLeave(tree.Context, tree.Reader, nil); err != nil { + if errors.Is(err, lifecycle.ErrActionNotDefined) { + done := false + status.MemberJoined = &done + return false, nil + } + return false, err + } + done := false + status.MemberJoined = &done + return false, nil +} + +func (r *instanceAlignmentReconciler) selectScaleInBatchNames(its *workloads.InstanceSet, oldInstanceList []client.Object, deleteNameSet sets.Set[string]) (sets.Set[string], error) { + if its.Spec.LifecycleActions == nil || its.Spec.LifecycleActions.MemberLeave == nil || deleteNameSet.Len() == 0 { + return nil, nil + } + pods := make([]corev1.Pod, 0, deleteNameSet.Len()) + for _, object := range oldInstanceList { + pod := object.(*corev1.Pod) + if deleteNameSet.Has(pod.Name) { + pods = append(pods, *pod) + } + } + if len(pods) == 0 { + return nil, nil + } + plan := &realUpdatePlan{ + its: *its, + pods: pods, + dag: graph.NewDAG(), + isPodUpdated: func(_ *workloads.InstanceSet, _ *corev1.Pod) (bool, error) { + return false, nil + }, + } + selected, err := plan.Execute() + if err != nil { + return nil, err + } + if len(selected) == 0 { + return nil, nil + } + names := sets.New[string]() + for _, pod := range selected { + names.Insert(pod.Name) + } + return names, nil +} + +func findInstanceStatus(its *workloads.InstanceSet, podName string) *workloads.InstanceStatus { + for i := range its.Status.InstanceStatus { + if its.Status.InstanceStatus[i].PodName == podName { + return &its.Status.InstanceStatus[i] + } + } + return nil +} + +func sortedPods(objects []client.Object) []*corev1.Pod { + pods := make([]*corev1.Pod, 0, len(objects)) + for _, obj := range objects { + pods = append(pods, obj.(*corev1.Pod)) + } + slices.SortFunc(pods, func(a, b *corev1.Pod) int { + aParent, aOrdinal := parseParentNameAndOrdinal(a.Name) + bParent, bOrdinal := parseParentNameAndOrdinal(b.Name) + if aParent != bParent { + if aParent < bParent { + return -1 + } + return 1 + } + switch { + case aOrdinal < bOrdinal: + return -1 + case aOrdinal > bOrdinal: + return 1 + default: + return 0 + } + }) + return pods +} + var _ kubebuilderx.Reconciler = &instanceAlignmentReconciler{} diff --git a/pkg/controller/instanceset/reconciler_instance_alignment_test.go b/pkg/controller/instanceset/reconciler_instance_alignment_test.go index f23e281aeba..7759477d3c3 100644 --- a/pkg/controller/instanceset/reconciler_instance_alignment_test.go +++ b/pkg/controller/instanceset/reconciler_instance_alignment_test.go @@ -20,20 +20,28 @@ along with this program. If not, see . package instanceset import ( + "context" "fmt" "slices" + "time" + "github.com/golang/mock/gomock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" workloads "github.com/apecloud/kubeblocks/apis/workloads/v1" "github.com/apecloud/kubeblocks/pkg/controller/builder" "github.com/apecloud/kubeblocks/pkg/controller/kubebuilderx" + kbacli "github.com/apecloud/kubeblocks/pkg/kbagent/client" + kbagentproto "github.com/apecloud/kubeblocks/pkg/kbagent/proto" + testapps "github.com/apecloud/kubeblocks/pkg/testutil/apps" ) var _ = Describe("replicas alignment reconciler test", func() { @@ -47,6 +55,17 @@ var _ = Describe("replicas alignment reconciler test", func() { }) Context("PreCondition & Reconcile", func() { + makePodAvailable := func(pod *corev1.Pod) { + pod.Status.Phase = corev1.PodRunning + pod.Status.Conditions = []corev1.PodCondition{ + { + Type: corev1.PodReady, + Status: corev1.ConditionTrue, + LastTransitionTime: metav1.NewTime(time.Now().Add(-time.Second)), + }, + } + } + It("should work well", func() { By("PreCondition") its.Generation = 1 @@ -181,5 +200,283 @@ var _ = Describe("replicas alignment reconciler test", func() { } } }) + + It("serially advances scale-out lifecycle one step per reconcile", func() { + var actions []kbagentproto.ActionRequest + testapps.MockKBAgentClient(func(recorder *kbacli.MockClientMockRecorder) { + recorder.Action(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req kbagentproto.ActionRequest) (kbagentproto.ActionResponse, error) { + actions = append(actions, req) + return kbagentproto.ActionResponse{}, nil + }).AnyTimes() + }) + + its.Spec.LifecycleActions = &workloads.LifecycleActions{ + MemberJoin: testapps.NewLifecycleAction("member-join"), + DataLoad: testapps.NewLifecycleAction("data-load"), + } + its.Spec.MemberUpdateStrategy = ptr.To(workloads.SerialUpdateStrategy) + its.Status.InstanceStatus = []workloads.InstanceStatus{ + {PodName: its.Name + "-0", Provisioned: true, MemberJoined: boolPtr(true)}, + {PodName: its.Name + "-1", Provisioned: true, DataLoaded: boolPtr(false), MemberJoined: boolPtr(false)}, + } + + tree := kubebuilderx.NewObjectTree() + tree.SetRoot(its) + pod0 := builder.NewPodBuilder(namespace, its.Name+"-0").GetObject() + pod1 := builder.NewPodBuilder(namespace, its.Name+"-1").GetObject() + makePodAvailable(pod0) + makePodAvailable(pod1) + Expect(tree.Add(pod0, pod1)).Should(Succeed()) + + r := &instanceAlignmentReconciler{} + retry, err := r.reconcileScaleOutLifecycle(tree, its) + Expect(err).Should(BeNil()) + Expect(retry).Should(BeTrue()) + Expect(actions).Should(HaveLen(1)) + Expect(actions[0].Action).Should(Equal("dataLoad")) + Expect(actions[0].Parameters["KB_TARGET_POD_NAME"]).Should(Equal(pod1.Name)) + Expect(*findInstanceStatus(its, pod1.Name).DataLoaded).Should(BeTrue()) + Expect(*findInstanceStatus(its, pod1.Name).MemberJoined).Should(BeFalse()) + + retry, err = r.reconcileScaleOutLifecycle(tree, its) + Expect(err).Should(BeNil()) + Expect(retry).Should(BeTrue()) + Expect(actions).Should(HaveLen(2)) + Expect(actions[1].Action).Should(Equal("memberJoin")) + Expect(actions[1].Parameters["KB_JOIN_MEMBER_POD_NAME"]).Should(Equal(pod1.Name)) + Expect(*findInstanceStatus(its, pod1.Name).MemberJoined).Should(BeTrue()) + }) + + It("parallel lifecycle advances all pending scale-out replicas in one reconcile", func() { + var actions []kbagentproto.ActionRequest + testapps.MockKBAgentClient(func(recorder *kbacli.MockClientMockRecorder) { + recorder.Action(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req kbagentproto.ActionRequest) (kbagentproto.ActionResponse, error) { + actions = append(actions, req) + return kbagentproto.ActionResponse{}, nil + }).AnyTimes() + }) + + its.Spec.LifecycleActions = &workloads.LifecycleActions{ + MemberJoin: testapps.NewLifecycleAction("member-join"), + DataLoad: testapps.NewLifecycleAction("data-load"), + } + its.Spec.MemberUpdateStrategy = ptr.To(workloads.ParallelUpdateStrategy) + its.Status.InstanceStatus = []workloads.InstanceStatus{ + {PodName: its.Name + "-0", Provisioned: true, MemberJoined: boolPtr(true)}, + {PodName: its.Name + "-1", Provisioned: true, DataLoaded: boolPtr(false), MemberJoined: boolPtr(false)}, + {PodName: its.Name + "-2", Provisioned: true, DataLoaded: boolPtr(false), MemberJoined: boolPtr(false)}, + } + + tree := kubebuilderx.NewObjectTree() + tree.SetRoot(its) + for i := 0; i < 3; i++ { + pod := builder.NewPodBuilder(namespace, fmt.Sprintf("%s-%d", its.Name, i)).GetObject() + makePodAvailable(pod) + Expect(tree.Add(pod)).Should(Succeed()) + } + + r := &instanceAlignmentReconciler{} + retry, err := r.reconcileScaleOutLifecycle(tree, its) + Expect(err).Should(BeNil()) + Expect(retry).Should(BeFalse()) + Expect(actions).Should(HaveLen(4)) + Expect(actions[0].Action).Should(Equal("dataLoad")) + Expect(actions[1].Action).Should(Equal("memberJoin")) + Expect(actions[2].Action).Should(Equal("dataLoad")) + Expect(actions[3].Action).Should(Equal("memberJoin")) + for _, podName := range []string{its.Name + "-1", its.Name + "-2"} { + status := findInstanceStatus(its, podName) + Expect(*status.DataLoaded).Should(BeTrue()) + Expect(*status.MemberJoined).Should(BeTrue()) + } + }) + + It("serially scales in one joined replica per reconcile", func() { + var leaveNames []string + testapps.MockKBAgentClient(func(recorder *kbacli.MockClientMockRecorder) { + recorder.Action(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req kbagentproto.ActionRequest) (kbagentproto.ActionResponse, error) { + if req.Action == "memberLeave" { + leaveNames = append(leaveNames, req.Parameters["KB_LEAVE_MEMBER_POD_NAME"]) + } + return kbagentproto.ActionResponse{}, nil + }).AnyTimes() + }) + + replicas := int32(1) + its.Spec.Replicas = &replicas + its.Spec.PodManagementPolicy = appsv1.ParallelPodManagement + its.Spec.LifecycleActions = &workloads.LifecycleActions{ + MemberLeave: testapps.NewLifecycleAction("member-leave"), + } + its.Spec.MemberUpdateStrategy = ptr.To(workloads.SerialUpdateStrategy) + its.Status.InstanceStatus = []workloads.InstanceStatus{ + {PodName: its.Name + "-0", Provisioned: true, MemberJoined: boolPtr(true)}, + {PodName: its.Name + "-1", Provisioned: true, MemberJoined: boolPtr(true)}, + {PodName: its.Name + "-2", Provisioned: true, MemberJoined: boolPtr(true)}, + } + + tree := kubebuilderx.NewObjectTree() + tree.SetRoot(its) + for i := 0; i < 3; i++ { + pod := builder.NewPodBuilder(namespace, fmt.Sprintf("%s-%d", its.Name, i)).GetObject() + makePodAvailable(pod) + Expect(tree.Add(pod)).Should(Succeed()) + } + + reconciler = NewReplicasAlignmentReconciler() + res, err := reconciler.Reconcile(tree) + Expect(err).Should(BeNil()) + Expect(res).Should(Equal(kubebuilderx.Continue)) + Expect(leaveNames).Should(HaveLen(1)) + Expect(tree.List(&corev1.Pod{})).Should(HaveLen(2)) + }) + + It("retries scale-in when member leave fails without deleting the pod", func() { + attempts := 0 + testapps.MockKBAgentClient(func(recorder *kbacli.MockClientMockRecorder) { + recorder.Action(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req kbagentproto.ActionRequest) (kbagentproto.ActionResponse, error) { + if req.Action != "memberLeave" { + return kbagentproto.ActionResponse{}, nil + } + attempts++ + if attempts == 1 { + return kbagentproto.ActionResponse{}, fmt.Errorf("temporary leave failure") + } + return kbagentproto.ActionResponse{}, nil + }).AnyTimes() + }) + + replicas := int32(1) + its.Spec.Replicas = &replicas + its.Spec.PodManagementPolicy = appsv1.ParallelPodManagement + its.Spec.LifecycleActions = &workloads.LifecycleActions{ + MemberLeave: testapps.NewLifecycleAction("member-leave"), + } + its.Spec.MemberUpdateStrategy = ptr.To(workloads.SerialUpdateStrategy) + its.Status.InstanceStatus = []workloads.InstanceStatus{ + {PodName: its.Name + "-0", Provisioned: true, MemberJoined: boolPtr(true)}, + {PodName: its.Name + "-1", Provisioned: true, MemberJoined: boolPtr(true)}, + } + + tree := kubebuilderx.NewObjectTree() + tree.SetRoot(its) + for i := 0; i < 2; i++ { + pod := builder.NewPodBuilder(namespace, fmt.Sprintf("%s-%d", its.Name, i)).GetObject() + makePodAvailable(pod) + Expect(tree.Add(pod)).Should(Succeed()) + } + + reconciler = NewReplicasAlignmentReconciler() + res, err := reconciler.Reconcile(tree) + Expect(err).Should(HaveOccurred()) + Expect(res).Should(Equal(kubebuilderx.Continue)) + Expect(tree.List(&corev1.Pod{})).Should(HaveLen(2)) + Expect(*findInstanceStatus(its, its.Name+"-1").MemberJoined).Should(BeTrue()) + + res, err = reconciler.Reconcile(tree) + Expect(err).Should(BeNil()) + Expect(res).Should(Equal(kubebuilderx.Continue)) + Expect(attempts).Should(Equal(2)) + Expect(tree.List(&corev1.Pod{})).Should(HaveLen(1)) + Expect(*findInstanceStatus(its, its.Name+"-1").MemberJoined).Should(BeFalse()) + }) + + It("best-effort parallel lifecycle advances all pending scale-out replicas in one reconcile", func() { + var actions []kbagentproto.ActionRequest + testapps.MockKBAgentClient(func(recorder *kbacli.MockClientMockRecorder) { + recorder.Action(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req kbagentproto.ActionRequest) (kbagentproto.ActionResponse, error) { + actions = append(actions, req) + return kbagentproto.ActionResponse{}, nil + }).AnyTimes() + }) + + its.Spec.LifecycleActions = &workloads.LifecycleActions{ + MemberJoin: testapps.NewLifecycleAction("member-join"), + DataLoad: testapps.NewLifecycleAction("data-load"), + } + its.Spec.MemberUpdateStrategy = ptr.To(workloads.BestEffortParallelUpdateStrategy) + its.Status.InstanceStatus = []workloads.InstanceStatus{ + {PodName: its.Name + "-0", Provisioned: true, MemberJoined: boolPtr(true)}, + {PodName: its.Name + "-1", Provisioned: true, DataLoaded: boolPtr(false), MemberJoined: boolPtr(false)}, + {PodName: its.Name + "-2", Provisioned: true, DataLoaded: boolPtr(false), MemberJoined: boolPtr(false)}, + } + + tree := kubebuilderx.NewObjectTree() + tree.SetRoot(its) + for i := 0; i < 3; i++ { + pod := builder.NewPodBuilder(namespace, fmt.Sprintf("%s-%d", its.Name, i)).GetObject() + makePodAvailable(pod) + Expect(tree.Add(pod)).Should(Succeed()) + } + + r := &instanceAlignmentReconciler{} + retry, err := r.reconcileScaleOutLifecycle(tree, its) + Expect(err).Should(BeNil()) + Expect(retry).Should(BeFalse()) + Expect(actions).Should(HaveLen(4)) + Expect(actions[0].Action).Should(Equal("dataLoad")) + Expect(actions[1].Action).Should(Equal("memberJoin")) + Expect(actions[2].Action).Should(Equal("dataLoad")) + Expect(actions[3].Action).Should(Equal("memberJoin")) + for _, podName := range []string{its.Name + "-1", its.Name + "-2"} { + status := findInstanceStatus(its, podName) + Expect(*status.DataLoaded).Should(BeTrue()) + Expect(*status.MemberJoined).Should(BeTrue()) + } + }) + + It("best-effort parallel scale-in processes the first role-safe batch in one reconcile", func() { + var leaveNames []string + testapps.MockKBAgentClient(func(recorder *kbacli.MockClientMockRecorder) { + recorder.Action(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req kbagentproto.ActionRequest) (kbagentproto.ActionResponse, error) { + if req.Action == "memberLeave" { + leaveNames = append(leaveNames, req.Parameters["KB_LEAVE_MEMBER_POD_NAME"]) + } + return kbagentproto.ActionResponse{}, nil + }).AnyTimes() + }) + + replicas := int32(1) + its.Spec.Replicas = &replicas + its.Spec.PodManagementPolicy = appsv1.ParallelPodManagement + its.Spec.LifecycleActions = &workloads.LifecycleActions{ + MemberLeave: testapps.NewLifecycleAction("member-leave"), + } + its.Spec.MemberUpdateStrategy = ptr.To(workloads.BestEffortParallelUpdateStrategy) + + tree := kubebuilderx.NewObjectTree() + tree.SetRoot(its) + roleNames := []string{"follower", "logger", "", "learner", "candidate", "leader", "learner"} + for i, roleName := range roleNames { + pod := builder.NewPodBuilder(namespace, fmt.Sprintf("%s-%d", its.Name, i)).GetObject() + if len(roleName) > 0 { + pod.Labels = map[string]string{RoleLabelKey: roleName} + } + makePodAvailable(pod) + Expect(tree.Add(pod)).Should(Succeed()) + } + its.Status.InstanceStatus = []workloads.InstanceStatus{ + {PodName: its.Name + "-0", Provisioned: true, MemberJoined: boolPtr(true), Role: "follower"}, + {PodName: its.Name + "-1", Provisioned: true, MemberJoined: boolPtr(true), Role: "logger"}, + {PodName: its.Name + "-2", Provisioned: true, MemberJoined: boolPtr(true)}, + {PodName: its.Name + "-3", Provisioned: true, MemberJoined: boolPtr(true), Role: "learner"}, + {PodName: its.Name + "-4", Provisioned: true, MemberJoined: boolPtr(true), Role: "candidate"}, + {PodName: its.Name + "-5", Provisioned: true, MemberJoined: boolPtr(true), Role: "leader"}, + {PodName: its.Name + "-6", Provisioned: true, MemberJoined: boolPtr(true), Role: "learner"}, + } + + reconciler = NewReplicasAlignmentReconciler() + res, err := reconciler.Reconcile(tree) + Expect(err).Should(BeNil()) + Expect(res).Should(Equal(kubebuilderx.Continue)) + Expect(leaveNames).Should(ConsistOf(its.Name+"-1", its.Name+"-2", its.Name+"-3", its.Name+"-4", its.Name+"-6")) + Expect(tree.List(&corev1.Pod{})).Should(HaveLen(2)) + + res, err = reconciler.Reconcile(tree) + Expect(err).Should(BeNil()) + Expect(res).Should(Equal(kubebuilderx.Continue)) + Expect(leaveNames).Should(HaveLen(6)) + Expect(tree.List(&corev1.Pod{})).Should(HaveLen(1)) + }) }) }) diff --git a/pkg/controller/instanceset/reconciler_status.go b/pkg/controller/instanceset/reconciler_status.go index 7942d2d4235..94fd607f990 100644 --- a/pkg/controller/instanceset/reconciler_status.go +++ b/pkg/controller/instanceset/reconciler_status.go @@ -312,10 +312,16 @@ func buildFailureCondition(its *workloads.InstanceSet, pods []*corev1.Pod) (*met func setInstanceStatus(tree *kubebuilderx.ObjectTree, its *workloads.InstanceSet, pods []*corev1.Pod) error { instanceStatus := make([]workloads.InstanceStatus, 0) + oldStatusMap := make(map[string]workloads.InstanceStatus, len(its.Status.InstanceStatus)) + for _, status := range its.Status.InstanceStatus { + oldStatusMap[status.PodName] = status + } for _, pod := range pods { status := workloads.InstanceStatus{ PodName: pod.Name, } + oldStatus, ok := oldStatusMap[pod.Name] + syncInstanceLifecycleStatus(its, &status, pod, oldStatus, ok, len(oldStatusMap) > 0) instanceStatus = append(instanceStatus, status) } @@ -364,6 +370,49 @@ func syncMemberStatus(its *workloads.InstanceSet, instanceStatus []workloads.Ins } } +func syncInstanceLifecycleStatus(its *workloads.InstanceSet, status *workloads.InstanceStatus, pod *corev1.Pod, oldStatus workloads.InstanceStatus, oldStatusExists bool, hasObservedReplicas bool) { + status.Provisioned = true + status.DataLoaded = initLifecycleBool(oldStatus.DataLoaded, shouldTrackDataLoad(its, oldStatus.DataLoaded, oldStatusExists, hasObservedReplicas)) + status.MemberJoined = initMemberJoinedStatus(its, pod, oldStatus.MemberJoined, oldStatusExists, hasObservedReplicas) +} + +func initLifecycleBool(old *bool, shouldTrack bool) *bool { + if !shouldTrack { + return nil + } + if old != nil { + return old + } + defaultValue := false + return &defaultValue +} + +func shouldTrackDataLoad(its *workloads.InstanceSet, old *bool, oldStatusExists bool, hasObservedReplicas bool) bool { + return its.Spec.LifecycleActions != nil && + its.Spec.LifecycleActions.DataLoad != nil && + (old != nil || (!oldStatusExists && hasObservedReplicas)) +} + +func shouldTrackMembership(its *workloads.InstanceSet, old *bool, oldStatusExists bool, hasObservedReplicas bool) bool { + return its.Spec.LifecycleActions != nil && + (its.Spec.LifecycleActions.MemberJoin != nil || its.Spec.LifecycleActions.MemberLeave != nil) && + (old != nil || !oldStatusExists || !hasObservedReplicas) +} + +func initMemberJoinedStatus(its *workloads.InstanceSet, pod *corev1.Pod, old *bool, oldStatusExists bool, hasObservedReplicas bool) *bool { + if !shouldTrackMembership(its, old, oldStatusExists, hasObservedReplicas) { + return nil + } + if old != nil { + return old + } + defaultValue := !hasObservedReplicas + if !defaultValue && !oldStatusExists { + defaultValue = false + } + return &defaultValue +} + func syncInstanceConfigStatus(_ *workloads.InstanceSet, instanceStatus []workloads.InstanceStatus, pods []*corev1.Pod) error { for _, pod := range pods { configs, err := configsFromPod(pod) diff --git a/pkg/controller/instanceset/reconciler_status_test.go b/pkg/controller/instanceset/reconciler_status_test.go index f604135b7b0..20d6c7dfe27 100644 --- a/pkg/controller/instanceset/reconciler_status_test.go +++ b/pkg/controller/instanceset/reconciler_status_test.go @@ -37,6 +37,10 @@ import ( intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" ) +func boolPtr(v bool) *bool { + return &v +} + var _ = Describe("status reconciler test", func() { BeforeEach(func() { its = builder.NewInstanceSetBuilder(namespace, name). @@ -408,6 +412,83 @@ var _ = Describe("status reconciler test", func() { Expect(its.Status.InstanceStatus[1].Role).Should(Equal("leader")) Expect(its.Status.InstanceStatus[2].PodName).Should(Equal("pod-2")) Expect(its.Status.InstanceStatus[2].Role).Should(Equal("")) + Expect(its.Status.InstanceStatus[0].Provisioned).Should(BeTrue()) + Expect(its.Status.InstanceStatus[1].Provisioned).Should(BeTrue()) + Expect(its.Status.InstanceStatus[2].Provisioned).Should(BeTrue()) + }) + + It("should preserve lifecycle status for tracked actions", func() { + pods := []*corev1.Pod{ + builder.NewPodBuilder(namespace, "pod-0").GetObject(), + builder.NewPodBuilder(namespace, "pod-1").GetObject(), + } + readyCondition := corev1.PodCondition{ + Type: corev1.PodReady, + Status: corev1.ConditionTrue, + } + pods[0].Status.Conditions = append(pods[0].Status.Conditions, readyCondition) + replicas := int32(2) + its.Spec.Replicas = &replicas + its.Spec.LifecycleActions = &workloads.LifecycleActions{ + MemberJoin: &workloads.Action{}, + DataLoad: &workloads.Action{}, + } + its.Status.InstanceStatus = []workloads.InstanceStatus{ + { + PodName: "pod-0", + Provisioned: true, + DataLoaded: boolPtr(false), + MemberJoined: boolPtr(false), + VolumeExpansion: true, + }, + } + + Expect(setInstanceStatus(nil, its, pods)).Should(Succeed()) + + Expect(its.Status.InstanceStatus).Should(HaveLen(2)) + Expect(its.Status.InstanceStatus[0].PodName).Should(Equal("pod-0")) + Expect(its.Status.InstanceStatus[0].Provisioned).Should(BeTrue()) + Expect(*its.Status.InstanceStatus[0].DataLoaded).Should(BeFalse()) + Expect(*its.Status.InstanceStatus[0].MemberJoined).Should(BeFalse()) + Expect(its.Status.InstanceStatus[1].PodName).Should(Equal("pod-1")) + Expect(its.Status.InstanceStatus[1].Provisioned).Should(BeTrue()) + Expect(*its.Status.InstanceStatus[1].DataLoaded).Should(BeFalse()) + Expect(*its.Status.InstanceStatus[1].MemberJoined).Should(BeFalse()) + }) + + It("should drop stale lifecycle status for pods that no longer exist", func() { + pods := []*corev1.Pod{ + builder.NewPodBuilder(namespace, "pod-0").GetObject(), + } + readyCondition := corev1.PodCondition{ + Type: corev1.PodReady, + Status: corev1.ConditionTrue, + } + pods[0].Status.Conditions = append(pods[0].Status.Conditions, readyCondition) + replicas := int32(1) + its.Spec.Replicas = &replicas + its.Spec.LifecycleActions = &workloads.LifecycleActions{ + MemberJoin: &workloads.Action{}, + } + its.Status.InstanceStatus = []workloads.InstanceStatus{ + { + PodName: "pod-0", + Provisioned: true, + MemberJoined: boolPtr(true), + }, + { + PodName: "pod-1", + Provisioned: true, + MemberJoined: boolPtr(true), + }, + } + + Expect(setInstanceStatus(nil, its, pods)).Should(Succeed()) + + Expect(its.Status.InstanceStatus).Should(HaveLen(1)) + Expect(its.Status.InstanceStatus[0].PodName).Should(Equal("pod-0")) + Expect(its.Status.InstanceStatus[0].Provisioned).Should(BeTrue()) + Expect(*its.Status.InstanceStatus[0].MemberJoined).Should(BeTrue()) }) }) }) diff --git a/pkg/controller/instanceset/tree_loader.go b/pkg/controller/instanceset/tree_loader.go index 01524c56a35..7f961b958e8 100644 --- a/pkg/controller/instanceset/tree_loader.go +++ b/pkg/controller/instanceset/tree_loader.go @@ -50,6 +50,7 @@ func (r *treeLoader) Load(ctx context.Context, reader client.Reader, req ctrl.Re } tree.Context = ctx + tree.Reader = reader tree.EventRecorder = recorder tree.Logger = logger tree.SetFinalizer(finalizer) diff --git a/pkg/controller/instanceset/utils.go b/pkg/controller/instanceset/utils.go index db376c56be7..0e26d188c1d 100644 --- a/pkg/controller/instanceset/utils.go +++ b/pkg/controller/instanceset/utils.go @@ -210,6 +210,9 @@ func newLifecycleAction(its *workloads.InstanceSet, tree *kubebuilderx.ObjectTre compName = its.Labels[constant.KBAppComponentLabelKey] lifecycleActions = &kbappsv1.ComponentLifecycleActions{ Switchover: its.Spec.LifecycleActions.Switchover, + MemberJoin: its.Spec.LifecycleActions.MemberJoin, + MemberLeave: its.Spec.LifecycleActions.MemberLeave, + DataLoad: its.Spec.LifecycleActions.DataLoad, Reconfigure: its.Spec.LifecycleActions.Reconfigure, } ) diff --git a/pkg/controller/instanceset/utils_test.go b/pkg/controller/instanceset/utils_test.go index 1da9f744ce9..e72d1cd17bd 100644 --- a/pkg/controller/instanceset/utils_test.go +++ b/pkg/controller/instanceset/utils_test.go @@ -221,6 +221,17 @@ var _ = Describe("utils test", func() { }, } Expect(its.IsInstanceSetReady()).Should(BeTrue()) + + By("set lifecycle status to not ready") + its.Status.InstanceStatus[0].DataLoaded = boolPtr(false) + Expect(its.IsInstanceSetReady()).Should(BeFalse()) + + By("set lifecycle status to ready again") + its.Status.InstanceStatus[0].DataLoaded = boolPtr(true) + its.Status.InstanceStatus[0].MemberJoined = boolPtr(true) + its.Status.InstanceStatus[1].MemberJoined = boolPtr(true) + its.Status.InstanceStatus[2].MemberJoined = boolPtr(true) + Expect(its.IsInstanceSetReady()).Should(BeTrue()) }) }) diff --git a/pkg/controller/kubebuilderx/reconciler.go b/pkg/controller/kubebuilderx/reconciler.go index 02fe83e2343..e4877981c85 100644 --- a/pkg/controller/kubebuilderx/reconciler.go +++ b/pkg/controller/kubebuilderx/reconciler.go @@ -60,6 +60,7 @@ func (o SkipToReconcile) ApplyToObject(opts *ObjectOptions) { type ObjectTree struct { // TODO(free6om): should find a better place to hold these two params? context.Context + client.Reader record.EventRecorder logr.Logger @@ -157,6 +158,7 @@ func (t *ObjectTree) DeepCopy() (*ObjectTree, error) { out.childrenOptions = childrenOptions out.finalizer = t.finalizer out.Context = t.Context + out.Reader = t.Reader out.EventRecorder = t.EventRecorder out.Logger = t.Logger return out, nil diff --git a/pkg/controller/lifecycle/kbagent.go b/pkg/controller/lifecycle/kbagent.go index d2e8ae35d9b..c91034eac63 100644 --- a/pkg/controller/lifecycle/kbagent.go +++ b/pkg/controller/lifecycle/kbagent.go @@ -114,6 +114,13 @@ func (a *kbagent) MemberLeave(ctx context.Context, cli client.Reader, opts *Opti return a.ignoreOutput(a.checkedCallAction(ctx, cli, a.lifecycleActions.MemberLeave, lfa, opts)) } +func (a *kbagent) DataLoad(ctx context.Context, cli client.Reader, opts *Options) error { + lfa := &dataLoad{ + pod: a.pod, + } + return a.ignoreOutput(a.checkedCallAction(ctx, cli, a.lifecycleActions.DataLoad, lfa, opts)) +} + func (a *kbagent) Reconfigure(ctx context.Context, cli client.Reader, opts *Options, args map[string]string) error { lfa := &reconfigure{ args: args, diff --git a/pkg/controller/lifecycle/lfa_data.go b/pkg/controller/lifecycle/lfa_data.go new file mode 100644 index 00000000000..878d7dc30a1 --- /dev/null +++ b/pkg/controller/lifecycle/lfa_data.go @@ -0,0 +1,50 @@ +/* +Copyright (C) 2022-2025 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package lifecycle + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + targetPodNameVar = "KB_TARGET_POD_NAME" +) + +type dataLoad struct { + pod *corev1.Pod +} + +var _ lifecycleAction = &dataLoad{} + +func (a *dataLoad) name() string { + return "dataLoad" +} + +func (a *dataLoad) parameters(ctx context.Context, cli client.Reader) (map[string]string, error) { + if a.pod == nil { + return nil, nil + } + return map[string]string{ + targetPodNameVar: a.pod.Name, + }, nil +} diff --git a/pkg/controller/lifecycle/lifecycle.go b/pkg/controller/lifecycle/lifecycle.go index 81c178fa537..9608dad1bc6 100644 --- a/pkg/controller/lifecycle/lifecycle.go +++ b/pkg/controller/lifecycle/lifecycle.go @@ -49,6 +49,8 @@ type Lifecycle interface { MemberLeave(ctx context.Context, cli client.Reader, opts *Options) error + DataLoad(ctx context.Context, cli client.Reader, opts *Options) error + // Readonly(ctx context.Context, cli client.Reader, opts *Options) error // Readwrite(ctx context.Context, cli client.Reader, opts *Options) error diff --git a/pkg/controller/lifecycle/lifecycle_test.go b/pkg/controller/lifecycle/lifecycle_test.go index f254a04783f..e574e34a789 100644 --- a/pkg/controller/lifecycle/lifecycle_test.go +++ b/pkg/controller/lifecycle/lifecycle_test.go @@ -425,6 +425,29 @@ var _ = Describe("lifecycle", func() { Expect(output).Should(Equal([]byte(val))) }) + It("data load builtin vars", func() { + lifecycleActions.DataLoad = &appsv1.Action{ + Exec: &appsv1.ExecAction{ + Command: []string{"/bin/bash", "-c", "echo -n data-load"}, + }, + } + lifecycle, err := New(namespace, clusterName, compName, lifecycleActions, nil, pods[0], pods) + Expect(err).Should(BeNil()) + Expect(lifecycle).ShouldNot(BeNil()) + + mockKBAgentClient(func(recorder *kbacli.MockClientMockRecorder) { + recorder.Action(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req proto.ActionRequest) (proto.ActionResponse, error) { + Expect(req.Action).Should(Equal("dataLoad")) + Expect(req.Parameters).ShouldNot(BeNil()) + Expect(req.Parameters[targetPodNameVar]).Should(Equal(pods[0].Name)) + return proto.ActionResponse{}, nil + }).AnyTimes() + }) + + err = lifecycle.DataLoad(ctx, k8sClient, nil) + Expect(err).Should(BeNil()) + }) + It("precondition", func() { clusterReady := appsv1.ClusterReadyPreConditionType lifecycleActions.PostProvision.PreCondition = &clusterReady diff --git a/pkg/kbagent/setup.go b/pkg/kbagent/setup.go index a20aa27394b..7af5c9850f4 100644 --- a/pkg/kbagent/setup.go +++ b/pkg/kbagent/setup.go @@ -79,52 +79,52 @@ func BuildEnv4Server(actions []proto.Action, probes []proto.Probe, streaming []s return append(util.DefaultEnvVars(), envVars...), nil } -func BuildEnv4Worker(tasks []proto.Task) (*corev1.EnvVar, error) { - dt, err := serializeTask(tasks) - if err != nil { - return nil, err - } - return &corev1.EnvVar{ - Name: taskEnvName, - Value: dt, - }, nil -} - -func UpdateEnv4Worker(envVars map[string]string, f func(proto.Task) *proto.Task) (*corev1.EnvVar, error) { - if envVars == nil { - return nil, nil - } - dt, ok := envVars[taskEnvName] - if !ok || len(dt) == 0 { - return nil, nil // has no task - } - - tasks, err := deserializeTask(dt) - if err != nil { - return nil, err - } - - for i := 0; i < len(tasks); i++ { - if f != nil { - task := f(tasks[i]) - if task != nil { - tasks[i] = *task - } else { - tasks = append(tasks[:i], tasks[i+1:]...) - i-- - } - } - } - - dt, err = serializeTask(tasks) - if err != nil { - return nil, err - } - return &corev1.EnvVar{ - Name: taskEnvName, - Value: dt, - }, nil -} +// func BuildEnv4Worker(tasks []proto.Task) (*corev1.EnvVar, error) { +// dt, err := serializeTask(tasks) +// if err != nil { +// return nil, err +// } +// return &corev1.EnvVar{ +// Name: taskEnvName, +// Value: dt, +// }, nil +// } +// +// func UpdateEnv4Worker(envVars map[string]string, f func(proto.Task) *proto.Task) (*corev1.EnvVar, error) { +// if envVars == nil { +// return nil, nil +// } +// dt, ok := envVars[taskEnvName] +// if !ok || len(dt) == 0 { +// return nil, nil // has no task +// } +// +// tasks, err := deserializeTask(dt) +// if err != nil { +// return nil, err +// } +// +// for i := 0; i < len(tasks); i++ { +// if f != nil { +// task := f(tasks[i]) +// if task != nil { +// tasks[i] = *task +// } else { +// tasks = append(tasks[:i], tasks[i+1:]...) +// i-- +// } +// } +// } +// +// dt, err = serializeTask(tasks) +// if err != nil { +// return nil, err +// } +// return &corev1.EnvVar{ +// Name: taskEnvName, +// Value: dt, +// }, nil +// } func Launch(logger logr.Logger, config server.Config) (bool, error) { envVars := util.EnvL2M(os.Environ()) @@ -267,13 +267,13 @@ func streamingService(services []service.Service) service.Service { return nil } -func serializeTask(tasks []proto.Task) (string, error) { - dt, err := json.Marshal(tasks) - if err != nil { - return "", nil - } - return string(dt), nil -} +// func serializeTask(tasks []proto.Task) (string, error) { +// dt, err := json.Marshal(tasks) +// if err != nil { +// return "", nil +// } +// return string(dt), nil +// } func deserializeTask(dt string) ([]proto.Task, error) { tasks := make([]proto.Task, 0)