diff --git a/apis/apps/v1alpha1/opsrequest_conditions.go b/apis/apps/v1alpha1/opsrequest_conditions.go index 318239aa2db..cd98df016c7 100644 --- a/apis/apps/v1alpha1/opsrequest_conditions.go +++ b/apis/apps/v1alpha1/opsrequest_conditions.go @@ -274,7 +274,7 @@ func NewReconfigureCondition(ops *OpsRequest) *metav1.Condition { LastTransitionTime: metav1.Now(), Message: fmt.Sprintf("Start to reconfigure in Cluster: %s, Component: %s", ops.Spec.ClusterRef, - ops.Spec.Reconfigure.ComponentName), + getComponentName(ops.Spec)), } } @@ -300,7 +300,7 @@ func NewReconfigureRunningCondition(ops *OpsRequest, conditionType string, confi } message := fmt.Sprintf("Reconfiguring in Cluster: %s, Component: %s, ConfigSpec: %s", ops.Spec.ClusterRef, - ops.Spec.Reconfigure.ComponentName, + getComponentName(ops.Spec), configSpecName) if len(info) > 0 { message = message + ", info: " + info[0] @@ -314,6 +314,13 @@ func NewReconfigureRunningCondition(ops *OpsRequest, conditionType string, confi } } +func getComponentName(request OpsRequestSpec) string { + if request.Reconfigure != nil { + return request.Reconfigure.ComponentName + } + return "" +} + // NewReconfigureFailedCondition creates a condition for the failed reconfigure. func NewReconfigureFailedCondition(ops *OpsRequest, err error) *metav1.Condition { var msg string diff --git a/apis/apps/v1alpha1/opsrequest_types.go b/apis/apps/v1alpha1/opsrequest_types.go index 9afdd7861a4..84da930a5c5 100644 --- a/apis/apps/v1alpha1/opsrequest_types.go +++ b/apis/apps/v1alpha1/opsrequest_types.go @@ -102,12 +102,22 @@ type OpsRequestSpec struct { // +listMapKey=componentName VerticalScalingList []VerticalScaling `json:"verticalScaling,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"componentName"` + // Deprecate: replace by reconfigures. + // reconfigure defines the variables that need to input when updating configuration. // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="forbidden to update spec.reconfigure" // +kubebuilder:validation:XValidation:rule="self.configurations.size() > 0", message="Value can not be empty" Reconfigure *Reconfigure `json:"reconfigure,omitempty"` + // reconfigure defines the variables that need to input when updating configuration. + // +optional + // +patchMergeKey=componentName + // +patchStrategy=merge,retainKeys + // +listType=map + // +listMapKey=componentName + Reconfigures []Reconfigure `json:"reconfigures,omitempty"` + // expose defines services the component needs to expose. // +optional // +patchMergeKey=componentName @@ -469,10 +479,16 @@ type OpsRequestStatus struct { // +optional CancelTimestamp metav1.Time `json:"cancelTimestamp,omitempty"` + // Deprecate: replace by ReconfiguringStatusAsComponent. + // reconfiguringStatus defines the status information of reconfiguring. // +optional ReconfiguringStatus *ReconfiguringStatus `json:"reconfiguringStatus,omitempty"` + // reconfiguringStatus defines the status information of reconfiguring. + // +optional + ReconfiguringStatusAsComponent map[string]*ReconfiguringStatus `json:"reconfiguringStatusAsComponent,omitempty"` + // conditions describes opsRequest detail status. // +optional // +patchMergeKey=type @@ -576,9 +592,16 @@ type OpsRequestComponentStatus struct { } type ReconfiguringStatus struct { + // conditions describes reconfiguring detail status. + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty"` + // configurationStatus describes the status of the component reconfiguring. // +kubebuilder:validation:Required - // +kubebuilder:validation:MinItems=1 // +patchMergeKey=name // +patchStrategy=merge,retainKeys // +listType=map diff --git a/apis/apps/v1alpha1/opsrequest_webhook.go b/apis/apps/v1alpha1/opsrequest_webhook.go index 8e476733fd7..4b98a038d4c 100644 --- a/apis/apps/v1alpha1/opsrequest_webhook.go +++ b/apis/apps/v1alpha1/opsrequest_webhook.go @@ -287,9 +287,21 @@ func (r *OpsRequest) validateReconfigure(cluster *Cluster) error { return nil } reconfigure := r.Spec.Reconfigure - if reconfigure == nil { + if reconfigure == nil && len(r.Spec.Reconfigures) == 0 { return notEmptyError("spec.reconfigure") } + if reconfigure != nil { + return r.validateReconfigureParams(cluster, reconfigure) + } + for _, reconfigure := range r.Spec.Reconfigures { + if err := r.validateReconfigureParams(cluster, &reconfigure); err != nil { + return err + } + } + return nil +} + +func (r *OpsRequest) validateReconfigureParams(cluster *Cluster, reconfigure *Reconfigure) error { if cluster.Spec.GetComponentByName(reconfigure.ComponentName) == nil { return fmt.Errorf("component %s not found", reconfigure.ComponentName) } diff --git a/apis/apps/v1alpha1/zz_generated.deepcopy.go b/apis/apps/v1alpha1/zz_generated.deepcopy.go index b1a33d850ad..26a3016a788 100644 --- a/apis/apps/v1alpha1/zz_generated.deepcopy.go +++ b/apis/apps/v1alpha1/zz_generated.deepcopy.go @@ -2828,6 +2828,13 @@ func (in *OpsRequestSpec) DeepCopyInto(out *OpsRequestSpec) { *out = new(Reconfigure) (*in).DeepCopyInto(*out) } + if in.Reconfigures != nil { + in, out := &in.Reconfigures, &out.Reconfigures + *out = make([]Reconfigure, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.ExposeList != nil { in, out := &in.ExposeList, &out.ExposeList *out = make([]Expose, len(*in)) @@ -2886,6 +2893,21 @@ func (in *OpsRequestStatus) DeepCopyInto(out *OpsRequestStatus) { *out = new(ReconfiguringStatus) (*in).DeepCopyInto(*out) } + if in.ReconfiguringStatusAsComponent != nil { + in, out := &in.ReconfiguringStatusAsComponent, &out.ReconfiguringStatusAsComponent + *out = make(map[string]*ReconfiguringStatus, len(*in)) + for key, val := range *in { + var outVal *ReconfiguringStatus + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = new(ReconfiguringStatus) + (*in).DeepCopyInto(*out) + } + (*out)[key] = outVal + } + } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]metav1.Condition, len(*in)) @@ -3218,6 +3240,13 @@ func (in *Reconfigure) DeepCopy() *Reconfigure { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ReconfiguringStatus) DeepCopyInto(out *ReconfiguringStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.ConfigurationStatus != nil { in, out := &in.ConfigurationStatus, &out.ConfigurationStatus *out = make([]ConfigurationItemStatus, len(*in)) diff --git a/config/crd/bases/apps.kubeblocks.io_opsrequests.yaml b/config/crd/bases/apps.kubeblocks.io_opsrequests.yaml index c53c092181d..49ef16339d5 100644 --- a/config/crd/bases/apps.kubeblocks.io_opsrequests.yaml +++ b/config/crd/bases/apps.kubeblocks.io_opsrequests.yaml @@ -280,6 +280,92 @@ spec: rule: self == oldSelf - message: Value can not be empty rule: self.configurations.size() > 0 + reconfigures: + description: reconfigure defines the variables that need to input + when updating configuration. + items: + description: Reconfigure defines the variables that need to input + when updating configuration. + properties: + componentName: + description: componentName cluster component name. + type: string + configurations: + description: configurations defines which components perform + the operation. + items: + properties: + keys: + description: keys is used to set the parameters to be + updated. + items: + properties: + fileContent: + description: fileContent indicates the configuration + file content. update whole file. + type: string + key: + description: key indicates the key name of ConfigMap. + type: string + parameters: + description: Setting the list of parameters for + a single configuration file. update specified + the parameters. + items: + properties: + key: + description: key is name of the parameter + to be updated. + type: string + value: + description: parameter values to be updated. + if set nil, the parameter defined by the + key field will be deleted from the configuration + file. + type: string + required: + - key + type: object + type: array + required: + - key + type: object + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - key + x-kubernetes-list-type: map + name: + description: name is a config template name. + maxLength: 63 + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + policy: + description: policy defines the upgrade policy. + enum: + - simple + - parallel + - rolling + - autoReload + - operatorSyncUpdate + type: string + required: + - keys + - name + type: object + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + required: + - componentName + - configurations + type: object + type: array + x-kubernetes-list-map-keys: + - componentName + x-kubernetes-list-type: map restart: description: restart the specified components. items: @@ -1043,6 +1129,81 @@ spec: description: reconfiguringStatus defines the status information of reconfiguring. properties: + conditions: + description: conditions describes reconfiguring detail status. + items: + description: "Condition contains details for one aspect of the + current state of this API Resource. --- This struct is intended + for direct use as an array at the field path .status.conditions. + \ For example, \n type FooStatus struct{ // Represents the + observations of a foo's current state. // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type + // +patchStrategy=merge // +listType=map // +listMapKey=type + Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` + \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be + when the underlying condition changed. If that is not + known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if + .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values + and meanings for this field, and whether the values are + considered a guaranteed API. The value should be a CamelCase + string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map configurationStatus: description: configurationStatus describes the status of the component reconfiguring. @@ -1113,7 +1274,6 @@ spec: required: - name type: object - minItems: 1 type: array x-kubernetes-list-map-keys: - name @@ -1121,6 +1281,167 @@ spec: required: - configurationStatus type: object + reconfiguringStatusAsComponent: + additionalProperties: + properties: + conditions: + description: conditions describes reconfiguring detail status. + items: + description: "Condition contains details for one aspect of + the current state of this API Resource. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, \n type FooStatus struct{ + // Represents the observations of a foo's current state. + // Known .status.conditions.type are: \"Available\", \"Progressing\", + and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields + }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should + be when the underlying condition changed. If that is + not known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, + if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier + indicating the reason for the condition's last transition. + Producers of specific condition types may define expected + values and meanings for this field, and whether the + values are considered a guaranteed API. The value should + be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + configurationStatus: + description: configurationStatus describes the status of the + component reconfiguring. + items: + properties: + expectedCount: + default: -1 + description: expectedCount describes the number of expected + reconfiguring. + format: int32 + type: integer + lastAppliedConfiguration: + additionalProperties: + type: string + description: LastAppliedConfiguration describes the last + configuration. + type: object + lastStatus: + description: lastStatus describes the last status for + the reconfiguring controller. + type: string + message: + description: message describes the details about this + operation. + type: string + name: + description: name is a config template name. + maxLength: 63 + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + status: + description: status describes the current state of the + reconfiguring state machine. + type: string + succeedCount: + default: 0 + description: succeedCount describes the number of successful + reconfiguring. + format: int32 + type: integer + updatePolicy: + description: updatePolicy describes the policy of reconfiguring. + enum: + - simple + - parallel + - rolling + - autoReload + - operatorSyncUpdate + type: string + updatedParameters: + description: updatedParameters describes the updated parameters. + properties: + addedKeys: + additionalProperties: + type: string + description: addedKeys describes the key added. + type: object + deletedKeys: + additionalProperties: + type: string + description: deletedKeys describes the key deleted. + type: object + updatedKeys: + additionalProperties: + type: string + description: updatedKeys describes the key updated. + type: object + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + required: + - configurationStatus + type: object + description: reconfiguringStatus defines the status information of + reconfiguring. + type: object startTimestamp: description: startTimestamp The time when the OpsRequest started processing. format: date-time diff --git a/controllers/apps/operations/ops_util.go b/controllers/apps/operations/ops_util.go index 19e26b90ac2..3ec452798ef 100644 --- a/controllers/apps/operations/ops_util.go +++ b/controllers/apps/operations/ops_util.go @@ -22,7 +22,6 @@ package operations import ( "context" "fmt" - "math" "reflect" "time" @@ -351,6 +350,13 @@ func isOpsRequestFailedPhase(opsRequestPhase appsv1alpha1.OpsPhase) bool { return opsRequestPhase == appsv1alpha1.OpsFailedPhase } +// patchReconfigureOpsStatus when Reconfigure is running, we should update status to OpsRequest.Status.ConfigurationStatus. +// +// NOTES: +// opsStatus describes status of OpsRequest. +// reconfiguringStatus describes status of reconfiguring operation, which contains multiple configuration templates. +// cmStatus describes status of configmap, it is uniquely associated with a configuration template, which contains multiple keys, each key is name of a configuration file. +// execStatus describes the result of the execution of the state machine, which is designed to solve how to conduct the reconfiguring operation, such as whether to restart, how to send a signal to the process. func updateReconfigureStatusByCM(reconfiguringStatus *appsv1alpha1.ReconfiguringStatus, tplName string, handleReconfigureStatus handleReconfigureOpsStatus) error { for i, cmStatus := range reconfiguringStatus.ConfigurationStatus { @@ -370,43 +376,6 @@ func updateReconfigureStatusByCM(reconfiguringStatus *appsv1alpha1.Reconfiguring return handleReconfigureStatus(cmStatus) } -// patchReconfigureOpsStatus when Reconfigure is running, we should update status to OpsRequest.Status.ConfigurationStatus. -// -// NOTES: -// opsStatus describes status of OpsRequest. -// reconfiguringStatus describes status of reconfiguring operation, which contains multiple configuration templates. -// cmStatus describes status of configmap, it is uniquely associated with a configuration template, which contains multiple keys, each key is name of a configuration file. -// execStatus describes the result of the execution of the state machine, which is designed to solve how to conduct the reconfiguring operation, such as whether to restart, how to send a signal to the process. -func patchReconfigureOpsStatus( - opsRes *OpsResource, - tplName string, - handleReconfigureStatus handleReconfigureOpsStatus) error { - var opsRequest = opsRes.OpsRequest - if opsRequest.Status.ReconfiguringStatus == nil { - opsRequest.Status.ReconfiguringStatus = &appsv1alpha1.ReconfiguringStatus{ - ConfigurationStatus: make([]appsv1alpha1.ConfigurationItemStatus, 0), - } - } - - reconfiguringStatus := opsRequest.Status.ReconfiguringStatus - return updateReconfigureStatusByCM(reconfiguringStatus, tplName, handleReconfigureStatus) -} - -// getSlowestReconfiguringProgress gets the progress of the reconfiguring operations. -func getSlowestReconfiguringProgress(status []appsv1alpha1.ConfigurationItemStatus) string { - slowest := appsv1alpha1.ConfigurationItemStatus{ - SucceedCount: math.MaxInt32, - ExpectedCount: -1, - } - - for _, st := range status { - if st.SucceedCount < slowest.SucceedCount { - slowest = st - } - } - return fmt.Sprintf("%d/%d", slowest.SucceedCount, slowest.ExpectedCount) -} - func getTargetResourcesOfLastComponent(lastConfiguration appsv1alpha1.LastConfiguration, compName string, resourceKey appsv1alpha1.ComponentResourceKey) []string { lastComponentConfigs := lastConfiguration.Components[compName] return lastComponentConfigs.TargetResources[resourceKey] diff --git a/controllers/apps/operations/reconfigure.go b/controllers/apps/operations/reconfigure.go index 480d032021d..949031dcc84 100644 --- a/controllers/apps/operations/reconfigure.go +++ b/controllers/apps/operations/reconfigure.go @@ -20,9 +20,11 @@ along with this program. If not, see . package operations import ( + "fmt" "time" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -68,9 +70,6 @@ func handleReconfigureStatusProgress(result *appsv1alpha1.ReconcileDetail, opsSt cmStatus.Message = result.ErrMessage cmStatus.Status = string(phase) } - if cmStatus.SucceedCount != core.Unconfirmed && cmStatus.ExpectedCount != core.Unconfirmed { - opsStatus.Progress = getSlowestReconfiguringProgress(opsStatus.ReconfiguringStatus.ConfigurationStatus) - } return } } @@ -90,15 +89,14 @@ func handleNewReconfigureRequest(configPatch *core.ConfigPatchInfo, lastAppliedC } } -func (r *reconfigureAction) syncDependResources(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource) (*intctrlutil.Fetcher, error) { +func (r *reconfigureAction) syncDependResources(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource, configSpec appsv1alpha1.ConfigurationItem, componentName string) (*intctrlutil.Fetcher, error) { ops := &opsRes.OpsRequest.Spec - configSpec := ops.Reconfigure.Configurations[0] fetcher := intctrlutil.NewResourceFetcher(&intctrlutil.ResourceCtx{ Context: reqCtx.Ctx, Client: cli, Namespace: opsRes.Cluster.Namespace, ClusterName: ops.ClusterRef, - ComponentName: ops.Reconfigure.ComponentName, + ComponentName: componentName, }) err := fetcher.Cluster(). @@ -113,65 +111,144 @@ func (r *reconfigureAction) syncDependResources(reqCtx intctrlutil.RequestCtx, c return fetcher, nil } -func (r *reconfigureAction) ReconcileAction(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource) (appsv1alpha1.OpsPhase, time.Duration, error) { - status := opsRes.OpsRequest.Status - if len(status.Conditions) == 0 { - return status.Phase, 30 * time.Second, nil +func (r *reconfigureAction) ReconcileAction(reqCtx intctrlutil.RequestCtx, cli client.Client, resource *OpsResource) (appsv1alpha1.OpsPhase, time.Duration, error) { + var ( + isFinished = true + opsRequest = resource.OpsRequest.Spec + ) + + // Node: support multiple component + opsDeepCopy := resource.OpsRequest.DeepCopy() + statusAsComponents := make([]appsv1alpha1.ConfigurationItemStatus, 0) + for _, reconfigureParams := range fromReconfigureOperations(opsRequest, reqCtx, cli, resource) { + phase, err := r.doSyncReconfigureStatus(reconfigureParams) + switch { + case err != nil: + return "", 30 * time.Second, err + case phase == appsv1alpha1.OpsFailedPhase: + return appsv1alpha1.OpsFailedPhase, 0, nil + case phase != appsv1alpha1.OpsSucceedPhase: + isFinished = false + } + statusAsComponents = append(statusAsComponents, reconfigureParams.configurationStatus.ConfigurationStatus[0]) } - if isNoChange(status.Conditions) { - return appsv1alpha1.OpsSucceedPhase, 0, nil + + phase := appsv1alpha1.OpsRunningPhase + if isFinished { + phase = appsv1alpha1.OpsSucceedPhase } + return syncReconfigureForOps(reqCtx, cli, resource, statusAsComponents, opsDeepCopy, phase) +} - ops := &opsRes.OpsRequest.Spec - if ops.Reconfigure == nil || len(ops.Reconfigure.Configurations) == 0 { - return appsv1alpha1.OpsFailedPhase, 0, nil +func fromReconfigureOperations(request appsv1alpha1.OpsRequestSpec, reqCtx intctrlutil.RequestCtx, cli client.Client, resource *OpsResource) (reconfigures []reconfigureParams) { + var operations []appsv1alpha1.Reconfigure + + if request.Reconfigure != nil { + operations = append(operations, *request.Reconfigure) } + operations = append(operations, request.Reconfigures...) - resource, err := r.syncDependResources(reqCtx, cli, opsRes) - if err != nil { + for _, reconfigure := range operations { + if len(reconfigure.Configurations) == 0 { + continue + } + reconfigures = append(reconfigures, reconfigureParams{ + resource: resource, + reqCtx: reqCtx, + cli: cli, + clusterName: resource.Cluster.Name, + componentName: reconfigure.ComponentName, + opsRequest: resource.OpsRequest, + configurationItem: reconfigure.Configurations[0], + configurationStatus: initReconfigureStatus(resource.OpsRequest, reconfigure.ComponentName), + }) + } + return reconfigures +} + +func syncReconfigureForOps(reqCtx intctrlutil.RequestCtx, cli client.Client, resource *OpsResource, statusAsComponents []appsv1alpha1.ConfigurationItemStatus, opsDeepCopy *appsv1alpha1.OpsRequest, phase appsv1alpha1.OpsPhase) (appsv1alpha1.OpsPhase, time.Duration, error) { + succeedCount := 0 + expectedCount := 0 + opsRequest := resource.OpsRequest + invalidProgress := false + for _, status := range statusAsComponents { + if status.SucceedCount < 0 || status.ExpectedCount < 0 { + invalidProgress = true + break + } + succeedCount += int(status.SucceedCount) + expectedCount += int(status.ExpectedCount) + } + if !invalidProgress { + opsRequest.Status.Progress = fmt.Sprintf("%d/%d", succeedCount, expectedCount) + } + if err := PatchOpsStatusWithOpsDeepCopy(reqCtx.Ctx, cli, resource, opsDeepCopy, phase); err != nil { return "", 30 * time.Second, err } - configSpec := ops.Reconfigure.Configurations[0] + return phase, 30 * time.Second, nil +} + +func (r *reconfigureAction) doSyncReconfigureStatus(params reconfigureParams) (appsv1alpha1.OpsPhase, error) { + if isNoChange(params.configurationStatus.Conditions) { + params.reqCtx.Log. + WithValues("clusterName", params.clusterName). + WithValues("componentName", params.configurationItem.Name). + Info("the reconfiguring operation has no change!") + return appsv1alpha1.OpsSucceedPhase, nil + } + + configSpec := params.configurationItem + resource, err := r.syncDependResources(params.reqCtx, + params.cli, params.resource, configSpec, params.componentName) + if err != nil { + return "", err + } + item := resource.ConfigurationObj.Spec.GetConfigurationItem(configSpec.Name) itemStatus := resource.ConfigurationObj.Status.GetItemStatus(configSpec.Name) if item == nil || itemStatus == nil { - return appsv1alpha1.OpsRunningPhase, 30 * time.Second, nil + return appsv1alpha1.OpsRunningPhase, nil } switch phase := reconfiguringPhase(resource, *item, itemStatus); phase { case appsv1alpha1.CCreatingPhase, appsv1alpha1.CInitPhase: - return appsv1alpha1.OpsFailedPhase, 0, core.MakeError("the configuration is creating or initializing, is not ready to reconfigure") + return appsv1alpha1.OpsFailedPhase, core.MakeError("the configuration is creating or initializing, is not ready to reconfigure") case appsv1alpha1.CFailedAndPausePhase: - return syncStatus(cli, reqCtx, opsRes, itemStatus, phase, appsv1alpha1.OpsFailedPhase, appsv1alpha1.ReasonReconfigureFailed) + return appsv1alpha1.OpsFailedPhase, + syncStatus(params.configurationStatus, params.resource, itemStatus, phase) case appsv1alpha1.CFinishedPhase: - return syncStatus(cli, reqCtx, opsRes, itemStatus, phase, appsv1alpha1.OpsSucceedPhase, appsv1alpha1.ReasonReconfigureSucceed) + return appsv1alpha1.OpsSucceedPhase, + syncStatus(params.configurationStatus, params.resource, itemStatus, phase) default: - return syncStatus(cli, reqCtx, opsRes, itemStatus, phase, appsv1alpha1.OpsRunningPhase, appsv1alpha1.ReasonReconfigureRunning) + return appsv1alpha1.OpsRunningPhase, + syncStatus(params.configurationStatus, params.resource, itemStatus, phase) } } func (r *reconfigureAction) Action(reqCtx intctrlutil.RequestCtx, cli client.Client, resource *OpsResource) error { - var ( - opsRequest = resource.OpsRequest - spec = &opsRequest.Spec - clusterName = spec.ClusterRef - componentName = spec.Reconfigure.ComponentName - reconfigure = spec.Reconfigure - ) + opsRequest := resource.OpsRequest.Spec + // Node: support multiple component + for _, reconfigureParams := range fromReconfigureOperations(opsRequest, reqCtx, cli, resource) { + if err := r.doReconfiguring(reconfigureParams); err != nil { + return err + } + } + return nil +} - if !needReconfigure(opsRequest) { +func (r *reconfigureAction) doReconfiguring(params reconfigureParams) error { + if !needReconfigure(params.opsRequest, params.configurationStatus) { return nil } - // TODO support multi tpl conditions merge - item := reconfigure.Configurations[0] + item := params.configurationItem opsPipeline := newPipeline(reconfigureContext{ - cli: cli, - reqCtx: reqCtx, - resource: resource, + cli: params.cli, + reqCtx: params.reqCtx, + resource: params.resource, config: item, - clusterName: clusterName, - componentName: componentName, + clusterName: params.clusterName, + componentName: params.componentName, }) result := opsPipeline. @@ -185,34 +262,32 @@ func (r *reconfigureAction) Action(reqCtx intctrlutil.RequestCtx, cli client.Cli Complete() if result.err != nil { - return processMergedFailed(resource, result.failed, result.err) + return processMergedFailed(params.resource, result.failed, result.err) } - reqCtx.Recorder.Eventf(resource.OpsRequest, + params.reqCtx.Recorder.Eventf(params.resource.OpsRequest, corev1.EventTypeNormal, appsv1alpha1.ReasonReconfigurePersisted, - "the reconfiguring operation of component[%s] in cluster[%s] merged successfully", componentName, clusterName) + "the reconfiguring operation of component[%s] in cluster[%s] merged successfully", params.componentName, params.clusterName) // merged successfully - if err := patchReconfigureOpsStatus(resource, opsPipeline.configSpec.Name, + if err := updateReconfigureStatusByCM(params.configurationStatus, opsPipeline.configSpec.Name, handleNewReconfigureRequest(result.configPatch, result.lastAppliedConfigs)); err != nil { return err } - condition := constructReconfiguringConditions(result, resource, opsPipeline.configSpec) - resource.OpsRequest.SetStatusCondition(*condition) + condition := constructReconfiguringConditions(result, params.resource, opsPipeline.configSpec) + meta.SetStatusCondition(¶ms.configurationStatus.Conditions, *condition) return nil } -func needReconfigure(request *appsv1alpha1.OpsRequest) bool { +func needReconfigure(request *appsv1alpha1.OpsRequest, status *appsv1alpha1.ReconfiguringStatus) bool { // Update params to configmap - if request.Spec.Type != appsv1alpha1.ReconfiguringType || - request.Spec.Reconfigure == nil || - len(request.Spec.Reconfigure.Configurations) == 0 { + if request.Spec.Type != appsv1alpha1.ReconfiguringType { return false } // Check if the reconfiguring operation has been processed. - for _, condition := range request.Status.Conditions { + for _, condition := range status.Conditions { if isExpectedPhase(condition, []string{appsv1alpha1.ReasonReconfigurePersisted, appsv1alpha1.ReasonReconfigureNoChanged}, metav1.ConditionTrue) { return false } @@ -220,23 +295,15 @@ func needReconfigure(request *appsv1alpha1.OpsRequest) bool { return true } -func syncStatus(cli client.Client, - reqCtx intctrlutil.RequestCtx, +func syncStatus(reconfiguringStatus *appsv1alpha1.ReconfiguringStatus, opsRes *OpsResource, status *appsv1alpha1.ConfigurationItemDetailStatus, - phase appsv1alpha1.ConfigurationPhase, - opsPhase appsv1alpha1.OpsPhase, - reconfigurePhase string) (appsv1alpha1.OpsPhase, time.Duration, error) { - opsDeepCopy := opsRes.OpsRequest.DeepCopy() - if err := patchReconfigureOpsStatus(opsRes, status.Name, - handleReconfigureStatusProgress(status.ReconcileDetail, &opsRes.OpsRequest.Status, phase)); err != nil { - return "", 30 * time.Second, err - } - if err := PatchOpsStatusWithOpsDeepCopy(reqCtx.Ctx, cli, opsRes, opsDeepCopy, appsv1alpha1.OpsRunningPhase, - appsv1alpha1.NewReconfigureRunningCondition(opsRes.OpsRequest, reconfigurePhase, status.Name)); err != nil { - return "", 30 * time.Second, err - } - return opsPhase, 30 * time.Second, nil + phase appsv1alpha1.ConfigurationPhase) error { + err := updateReconfigureStatusByCM(reconfiguringStatus, status.Name, + handleReconfigureStatusProgress(status.ReconcileDetail, &opsRes.OpsRequest.Status, phase)) + meta.SetStatusCondition(&reconfiguringStatus.Conditions, *appsv1alpha1.NewReconfigureRunningCondition( + opsRes.OpsRequest, string(phase), status.Name)) + return err } func reconfiguringPhase(resource *intctrlutil.Fetcher, @@ -265,3 +332,25 @@ func isNoChange(conditions []metav1.Condition) bool { } return false } + +func initReconfigureStatus(opsRequest *appsv1alpha1.OpsRequest, componentName string) *appsv1alpha1.ReconfiguringStatus { + status := &opsRequest.Status + if componentName == "" || (opsRequest.Spec.Reconfigure != nil && opsRequest.Spec.Reconfigure.ComponentName == componentName) { + if status.ReconfiguringStatus == nil { + status.ReconfiguringStatus = &appsv1alpha1.ReconfiguringStatus{ + ConfigurationStatus: make([]appsv1alpha1.ConfigurationItemStatus, 0), + } + } + return status.ReconfiguringStatus + } + + if status.ReconfiguringStatusAsComponent == nil { + status.ReconfiguringStatusAsComponent = make(map[string]*appsv1alpha1.ReconfiguringStatus) + } + if _, ok := status.ReconfiguringStatusAsComponent[componentName]; !ok { + status.ReconfiguringStatusAsComponent[componentName] = &appsv1alpha1.ReconfiguringStatus{ + ConfigurationStatus: make([]appsv1alpha1.ConfigurationItemStatus, 0), + } + } + return status.ReconfiguringStatusAsComponent[componentName] +} diff --git a/controllers/apps/operations/reconfigure_pipeline.go b/controllers/apps/operations/reconfigure_pipeline.go index a83fb733e0a..be63445bf57 100644 --- a/controllers/apps/operations/reconfigure_pipeline.go +++ b/controllers/apps/operations/reconfigure_pipeline.go @@ -196,10 +196,11 @@ func (p *pipeline) UpdateOpsLabel() *pipeline { } request := p.resource.OpsRequest - deepObject := request.DeepCopy() + newRequest := request.DeepCopy() + deepObject := client.MergeFrom(newRequest.DeepCopy()) formatter := p.configConstraint.Spec.FormatterConfig - updateOpsLabelWithReconfigure(request, p.updatedParameters, p.ConfigMapObj.Data, formatter) - return p.cli.Patch(p.reqCtx.Ctx, request, client.MergeFrom(deepObject)) + updateOpsLabelWithReconfigure(newRequest, p.updatedParameters, p.ConfigMapObj.Data, formatter) + return p.cli.Patch(p.reqCtx.Ctx, newRequest, deepObject) } return p.Wrap(updateFn) diff --git a/controllers/apps/operations/type.go b/controllers/apps/operations/type.go index cea94bde474..cfc6326f470 100644 --- a/controllers/apps/operations/type.go +++ b/controllers/apps/operations/type.go @@ -66,6 +66,18 @@ type OpsBehaviour struct { OpsHandler OpsHandler } +type reconfigureParams struct { + resource *OpsResource + reqCtx intctrlutil.RequestCtx + cli client.Client + + clusterName string + componentName string + opsRequest *appsv1alpha1.OpsRequest + configurationItem appsv1alpha1.ConfigurationItem + configurationStatus *appsv1alpha1.ReconfiguringStatus +} + type OpsResource struct { OpsRequest *appsv1alpha1.OpsRequest Cluster *appsv1alpha1.Cluster diff --git a/deploy/helm/crds/apps.kubeblocks.io_opsrequests.yaml b/deploy/helm/crds/apps.kubeblocks.io_opsrequests.yaml index c53c092181d..49ef16339d5 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_opsrequests.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_opsrequests.yaml @@ -280,6 +280,92 @@ spec: rule: self == oldSelf - message: Value can not be empty rule: self.configurations.size() > 0 + reconfigures: + description: reconfigure defines the variables that need to input + when updating configuration. + items: + description: Reconfigure defines the variables that need to input + when updating configuration. + properties: + componentName: + description: componentName cluster component name. + type: string + configurations: + description: configurations defines which components perform + the operation. + items: + properties: + keys: + description: keys is used to set the parameters to be + updated. + items: + properties: + fileContent: + description: fileContent indicates the configuration + file content. update whole file. + type: string + key: + description: key indicates the key name of ConfigMap. + type: string + parameters: + description: Setting the list of parameters for + a single configuration file. update specified + the parameters. + items: + properties: + key: + description: key is name of the parameter + to be updated. + type: string + value: + description: parameter values to be updated. + if set nil, the parameter defined by the + key field will be deleted from the configuration + file. + type: string + required: + - key + type: object + type: array + required: + - key + type: object + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - key + x-kubernetes-list-type: map + name: + description: name is a config template name. + maxLength: 63 + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + policy: + description: policy defines the upgrade policy. + enum: + - simple + - parallel + - rolling + - autoReload + - operatorSyncUpdate + type: string + required: + - keys + - name + type: object + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + required: + - componentName + - configurations + type: object + type: array + x-kubernetes-list-map-keys: + - componentName + x-kubernetes-list-type: map restart: description: restart the specified components. items: @@ -1043,6 +1129,81 @@ spec: description: reconfiguringStatus defines the status information of reconfiguring. properties: + conditions: + description: conditions describes reconfiguring detail status. + items: + description: "Condition contains details for one aspect of the + current state of this API Resource. --- This struct is intended + for direct use as an array at the field path .status.conditions. + \ For example, \n type FooStatus struct{ // Represents the + observations of a foo's current state. // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type + // +patchStrategy=merge // +listType=map // +listMapKey=type + Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` + \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be + when the underlying condition changed. If that is not + known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if + .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values + and meanings for this field, and whether the values are + considered a guaranteed API. The value should be a CamelCase + string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map configurationStatus: description: configurationStatus describes the status of the component reconfiguring. @@ -1113,7 +1274,6 @@ spec: required: - name type: object - minItems: 1 type: array x-kubernetes-list-map-keys: - name @@ -1121,6 +1281,167 @@ spec: required: - configurationStatus type: object + reconfiguringStatusAsComponent: + additionalProperties: + properties: + conditions: + description: conditions describes reconfiguring detail status. + items: + description: "Condition contains details for one aspect of + the current state of this API Resource. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, \n type FooStatus struct{ + // Represents the observations of a foo's current state. + // Known .status.conditions.type are: \"Available\", \"Progressing\", + and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields + }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should + be when the underlying condition changed. If that is + not known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, + if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier + indicating the reason for the condition's last transition. + Producers of specific condition types may define expected + values and meanings for this field, and whether the + values are considered a guaranteed API. The value should + be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + configurationStatus: + description: configurationStatus describes the status of the + component reconfiguring. + items: + properties: + expectedCount: + default: -1 + description: expectedCount describes the number of expected + reconfiguring. + format: int32 + type: integer + lastAppliedConfiguration: + additionalProperties: + type: string + description: LastAppliedConfiguration describes the last + configuration. + type: object + lastStatus: + description: lastStatus describes the last status for + the reconfiguring controller. + type: string + message: + description: message describes the details about this + operation. + type: string + name: + description: name is a config template name. + maxLength: 63 + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + status: + description: status describes the current state of the + reconfiguring state machine. + type: string + succeedCount: + default: 0 + description: succeedCount describes the number of successful + reconfiguring. + format: int32 + type: integer + updatePolicy: + description: updatePolicy describes the policy of reconfiguring. + enum: + - simple + - parallel + - rolling + - autoReload + - operatorSyncUpdate + type: string + updatedParameters: + description: updatedParameters describes the updated parameters. + properties: + addedKeys: + additionalProperties: + type: string + description: addedKeys describes the key added. + type: object + deletedKeys: + additionalProperties: + type: string + description: deletedKeys describes the key deleted. + type: object + updatedKeys: + additionalProperties: + type: string + description: updatedKeys describes the key updated. + type: object + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + required: + - configurationStatus + type: object + description: reconfiguringStatus defines the status information of + reconfiguring. + type: object startTimestamp: description: startTimestamp The time when the OpsRequest started processing. format: date-time diff --git a/pkg/cli/cmd/cluster/config_edit.go b/pkg/cli/cmd/cluster/config_edit.go index aa5d20d0325..f25554a80c9 100644 --- a/pkg/cli/cmd/cluster/config_edit.go +++ b/pkg/cli/cmd/cluster/config_edit.go @@ -227,7 +227,7 @@ func NewEditConfigureCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) o := &editConfigOptions{ configOpsOptions: configOpsOptions{ editMode: true, - OperationsOptions: newBaseOperationsOptions(f, streams, appsv1alpha1.ReconfiguringType, false), + OperationsOptions: newBaseOperationsOptions(f, streams, appsv1alpha1.ReconfiguringType, true), }} cmd := &cobra.Command{ diff --git a/pkg/cli/cmd/cluster/config_ops.go b/pkg/cli/cmd/cluster/config_ops.go index 14f75217c32..53de045bd65 100644 --- a/pkg/cli/cmd/cluster/config_ops.go +++ b/pkg/cli/cmd/cluster/config_ops.go @@ -35,7 +35,6 @@ import ( "github.com/apecloud/kubeblocks/pkg/cli/printer" "github.com/apecloud/kubeblocks/pkg/cli/types" "github.com/apecloud/kubeblocks/pkg/cli/util" - "github.com/apecloud/kubeblocks/pkg/cli/util/flags" "github.com/apecloud/kubeblocks/pkg/cli/util/prompt" cfgcm "github.com/apecloud/kubeblocks/pkg/configuration/config_manager" "github.com/apecloud/kubeblocks/pkg/configuration/core" @@ -79,6 +78,10 @@ func (o *configOpsOptions) Complete() error { } } + if len(o.ComponentNames) > 0 { + o.ComponentName = o.ComponentNames[0] + } + wrapper, err := newConfigWrapper(o.CreateOptions, o.Name, o.ComponentName, o.CfgTemplateName, o.CfgFile, o.KeyValues) if err != nil { return err @@ -116,7 +119,9 @@ func (o *configOpsOptions) Validate() error { o.CfgFile = o.wrapper.ConfigFile() o.CfgTemplateName = o.wrapper.ConfigSpecName() - o.ComponentNames = []string{o.wrapper.ComponentName()} + if len(o.ComponentNames) == 0 { + o.ComponentNames = []string{o.wrapper.ComponentName()} + } if o.editMode { return nil @@ -240,7 +245,7 @@ func (o *configOpsOptions) buildReconfigureCommonFlags(cmd *cobra.Command, f cmd "For available templates and configs, refer to: 'kbcli cluster describe-config'.") cmd.Flags().StringVar(&o.CfgFile, "config-file", "", "Specify the name of the configuration file to be updated (e.g. for mysql: --config-file=my.cnf). "+ "For available templates and configs, refer to: 'kbcli cluster describe-config'.") - flags.AddComponentFlag(f, cmd, &o.ComponentName, "Specify the name of Component to be updated. If the cluster has only one component, unset the parameter.") + // flags.AddComponentFlag(f, cmd, &o.ComponentName, "Specify the name of Component to be updated. If the cluster has only one component, unset the parameter.") cmd.Flags().BoolVar(&o.ForceRestart, "force-restart", false, "Boolean flag to restart component. Default with false.") cmd.Flags().StringVar(&o.LocalFilePath, "local-file", "", "Specify the local configuration file to be updated.") cmd.Flags().BoolVar(&o.replaceFile, "replace", false, "Boolean flag to enable replacing config file. Default with false.") @@ -250,10 +255,10 @@ func (o *configOpsOptions) buildReconfigureCommonFlags(cmd *cobra.Command, f cmd func NewReconfigureCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := &configOpsOptions{ editMode: false, - OperationsOptions: newBaseOperationsOptions(f, streams, appsv1alpha1.ReconfiguringType, false), + OperationsOptions: newBaseOperationsOptions(f, streams, appsv1alpha1.ReconfiguringType, true), } cmd := &cobra.Command{ - Use: "configure NAME --set key=value[,key=value] [--component=component-name] [--config-spec=config-spec-name] [--config-file=config-file]", + Use: "configure NAME --set key=value[,key=value] [--components=component1-name,component2-name] [--config-spec=config-spec-name] [--config-file=config-file]", Short: "Configure parameters with the specified components in the cluster.", Example: createReconfigureExample, ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), diff --git a/pkg/cli/cmd/cluster/describe_ops.go b/pkg/cli/cmd/cluster/describe_ops.go index 6c3700beffa..f7cd5912591 100644 --- a/pkg/cli/cmd/cluster/describe_ops.go +++ b/pkg/cli/cmd/cluster/describe_ops.go @@ -332,11 +332,21 @@ func (o *describeOpsOptions) getVolumeExpansionCommand(spec appsv1alpha1.OpsRequ // getReconfiguringCommand gets the command of the VolumeExpansion command. func (o *describeOpsOptions) getReconfiguringCommand(spec appsv1alpha1.OpsRequestSpec) []string { - var ( - updatedParams = spec.Reconfigure - componentName = updatedParams.ComponentName - ) + if spec.Reconfigure != nil { + return generateReconfiguringCommand(spec.ClusterRef, spec.Reconfigure, []string{spec.Reconfigure.ComponentName}) + } + + if len(spec.Reconfigures) == 0 { + return nil + } + components := make([]string, len(spec.Reconfigures)) + for i, reconfigure := range spec.Reconfigures { + components[i] = reconfigure.ComponentName + } + return generateReconfiguringCommand(spec.ClusterRef, &spec.Reconfigures[0], components) +} +func generateReconfiguringCommand(clusterRef string, updatedParams *appsv1alpha1.Reconfigure, components []string) []string { if len(updatedParams.Configurations) == 0 { return nil } @@ -350,8 +360,8 @@ func (o *describeOpsOptions) getReconfiguringCommand(spec appsv1alpha1.OpsReques commandArgs = append(commandArgs, "kbcli") commandArgs = append(commandArgs, "cluster") commandArgs = append(commandArgs, "configure") - commandArgs = append(commandArgs, spec.ClusterRef) - commandArgs = append(commandArgs, fmt.Sprintf("--components=%s", componentName)) + commandArgs = append(commandArgs, clusterRef) + commandArgs = append(commandArgs, fmt.Sprintf("--components=%s", strings.Join(components, ","))) commandArgs = append(commandArgs, fmt.Sprintf("--config-spec=%s", configuration.Name)) config := configuration.Keys[0] diff --git a/pkg/cli/create/template/cluster_operations_template.cue b/pkg/cli/create/template/cluster_operations_template.cue index fcf7f7bfd78..b5d9267e35f 100644 --- a/pkg/cli/create/template/cluster_operations_template.cue +++ b/pkg/cli/create/template/cluster_operations_template.cue @@ -120,24 +120,49 @@ content: { }] } if options.type == "Reconfiguring" { - reconfigure: { - componentName: options.componentNames[0] - configurations: [ { - name: options.cfgTemplateName - if options.forceRestart { - policy: "simple" - } - keys: [{ - key: options.cfgFile - if options.fileContent != "" { - fileContent: options.fileContent + if len(options.componentNames) == 1 { + reconfigure: { + componentName: options.componentNames[0] + configurations: [ { + name: options.cfgTemplateName + if options.forceRestart { + policy: "simple" } - if options.hasPatch { - parameters: [ for k, v in options.keyValues { - key: k - value: v - }] + keys: [{ + key: options.cfgFile + if options.fileContent != "" { + fileContent: options.fileContent + } + if options.hasPatch { + parameters: [ for k, v in options.keyValues { + key: k + value: v + }] + } + }] + }] + } + } + if len(options.componentNames) > 1 { + reconfigures: [ for _, cName in options.componentNames { + componentName: cName + configurations: [ { + name: options.cfgTemplateName + if options.forceRestart { + policy: "simple" } + keys: [{ + key: options.cfgFile + if options.fileContent != "" { + fileContent: options.fileContent + } + if options.hasPatch { + parameters: [ for k, v in options.keyValues { + key: k + value: v + }] + } + }] }] }] } diff --git a/pkg/unstructured/xml_config.go b/pkg/unstructured/xml_config.go index 2e16733f4f6..1cb0ff2eab8 100644 --- a/pkg/unstructured/xml_config.go +++ b/pkg/unstructured/xml_config.go @@ -113,7 +113,8 @@ func (x *xmlConfig) SubConfig(key string) ConfigObject { } func (x *xmlConfig) Marshal() (string, error) { - b, err := x.data.Xml() + // b, err := x.data.Xml() + b, err := x.data.XmlIndent("", " ") if err != nil { return "", err }