oracle/controllers/instancecontroller/instance_controller_parameters.go (247 lines of code) (raw):

// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package instancecontroller import ( "context" "errors" "fmt" "reflect" "strings" "time" maintenance "github.com/GoogleCloudPlatform/elcarro-oracle-operator/common/pkg/maintenance" v1alpha1 "github.com/GoogleCloudPlatform/elcarro-oracle-operator/oracle/api/v1alpha1" "github.com/GoogleCloudPlatform/elcarro-oracle-operator/oracle/controllers" "github.com/GoogleCloudPlatform/elcarro-oracle-operator/oracle/pkg/k8s" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) // reservedParameters holds the list of parameters that aren't allowed for modification. var reservedParameters = map[string]bool{ "audit_file_dest": true, "audit_trail": true, "compatible": true, "control_files": true, "db_block_size": true, "db_recovery_file_dest": true, "diagnostic_dest": true, "dispatchers": true, "enable_pluggable_database": true, "filesystemio_options": true, "local_listener": true, "remote_login_passwordfile": true, "undo_tablespace": true, "log_archive_dest_1": true, "log_archive_dest_state_1": true, "log_archive_format": true, "standby_file_management": true, } func (r *InstanceReconciler) recordEventAndUpdateStatus(ctx context.Context, inst *v1alpha1.Instance, conditionStatus v1.ConditionStatus, reason, msg string, log logr.Logger) { if conditionStatus == v1.ConditionTrue { r.Recorder.Eventf(inst, corev1.EventTypeNormal, reason, msg) } else { r.Recorder.Eventf(inst, corev1.EventTypeWarning, reason, msg) } k8s.InstanceUpsertCondition(&inst.Status, k8s.Ready, conditionStatus, reason, msg) if err := r.Status().Update(ctx, inst); err != nil { log.Error(err, "failed to update the instance status") } } // fetchCurrentParameterState infers the type and current value of the // parameters by querying the database and is used for the following purpose, // * The parameter type (static or dynamic) will be used for deciding whether // // a database restart is required. // // * The current parameter value will be used for rollback if the parameter // // update fails or the database is non-functional after the restart. func fetchCurrentParameterState(ctx context.Context, r client.Reader, dbClientFactory controllers.DatabaseClientFactory, inst v1alpha1.Instance) (map[string]string, map[string]string, error) { spec := inst.Spec var unacceptableParams []string var keys []string for k := range spec.Parameters { if _, ok := reservedParameters[k]; ok { unacceptableParams = append(unacceptableParams, k) } keys = append(keys, k) } if len(unacceptableParams) != 0 { return nil, nil, fmt.Errorf("fetchCurrentParameterState: parameter list contains reserved parameters:%v", unacceptableParams) } staticParams := make(map[string]string) dynamicParams := make(map[string]string) setParameterReq := &controllers.GetParameterTypeValueRequest{ Keys: keys, } response, err := controllers.GetParameterTypeValue(ctx, r, dbClientFactory, inst.Namespace, inst.Name, *setParameterReq) if err != nil { return nil, nil, fmt.Errorf("fetchCurrentParameterState: error while querying parameter type:%v", err) } // Check if static parameters are specified and restart is required. restartRequired := false paramType := response.Types paramValues := response.Values for i := 0; i < len(paramType); i++ { if paramType[i] == "FALSE" { restartRequired = restartRequired || paramType[i] == "FALSE" staticParams[keys[i]] = paramValues[i] } else { dynamicParams[keys[i]] = paramValues[i] } } // If restart is required, check if the restartTimeRange is specified in the config. if restartRequired && !maintenance.HasValidTimeRanges(spec.MaintenanceWindow) { return nil, nil, errors.New("maintenanceWindow for db downtime not specified for static parameter update") } currentTime := time.Now() inMaintenanceWindow := maintenance.InRange(spec.MaintenanceWindow, currentTime) if !inMaintenanceWindow { return nil, nil, errors.New("current time is not in a maintenance window that allows db restarts") } return staticParams, dynamicParams, nil } func (r *InstanceReconciler) setParameters(ctx context.Context, inst v1alpha1.Instance, log logr.Logger) (bool, error) { log.Info("Parameters are ", "parameters:", inst.Spec.Parameters) requireDatabaseRestart := false var keys []string for k, v := range inst.Spec.Parameters { isStatic, err := controllers.SetParameter(ctx, r.DatabaseClientFactory, r.Client, inst.Namespace, inst.Name, k, v) if err != nil { log.Error(err, "setParameters: error while running SetParameter query") return requireDatabaseRestart, err } keys = append(keys, k) requireDatabaseRestart = requireDatabaseRestart || isStatic log.Info("setParameters: requireDatabaseRestart", "requireDatabaseRestart", requireDatabaseRestart) } getParameterReq := &controllers.GetParameterTypeValueRequest{ Keys: keys, } response, err := controllers.GetParameterTypeValue(ctx, r, r.DatabaseClientFactory, inst.Namespace, inst.Name, *getParameterReq) if err != nil { log.Error(err, "setParameters: error while running GetParameterTypeValue query") return false, err } paramValues := response.Values for i := 0; i < len(keys); i++ { if inst.Spec.Parameters[keys[i]] != paramValues[i] && // For certain parameter types Oracle converts them to uppercase before storing // For eg boolean (true/false) units(char/byte) strings.ToUpper(inst.Spec.Parameters[keys[i]]) != paramValues[i] { msg := fmt.Sprintf("setParameters: parameter update for %s with value %s was rejected by database and instead set to %s", keys[i], inst.Spec.Parameters[keys[i]], paramValues[i]) log.Error(err, msg) //Oracle does unit conversion while storing certain memory parameters like sga_target, pga_aggregate_target. //Thereby there is no foolproof to confirm if the parameter update silently failed. Thereby we just log the //parameters (whose values don't match the exact user provided values) instead of throwing an error. //return false, errors.New(msg) } } log.Info("setParameters: SQL commands executed successfully") return requireDatabaseRestart, nil } // parameterUpdateStateMachine guides the transition of the parameter update // workflow to the next possible state based on the current state and the outcome // of the task associated with the current state. func (r *InstanceReconciler) parameterUpdateStateMachine(ctx context.Context, req ctrl.Request, inst v1alpha1.Instance, log logr.Logger) (ctrl.Result, error) { if !isParameterUpdateStateMachineEntryCondition(&inst) { return ctrl.Result{}, nil } // If the current parameter state is equal to the requested state skip the update if eq := reflect.DeepEqual(inst.Spec.Parameters, inst.Status.CurrentParameters); eq { return ctrl.Result{}, nil } // If the last failed parameter update is equal to the requested state skip it. if eq := reflect.DeepEqual(inst.Spec.Parameters, inst.Status.LastFailedParameterUpdate); eq { return ctrl.Result{}, nil } if result, err := r.sanityCheckTimeRange(inst, log); err != nil { return result, err } log.Info("parameterUpdateStateMachine: Entered state machine") instanceReadyCond := k8s.FindCondition(inst.Status.Conditions, k8s.Ready) switch instanceReadyCond.Reason { case k8s.CreateComplete: inst.Status.CurrentActiveStateMachine = controllers.ParameterUpdateStateMachine _, dynamicParamsRollbackState, err := fetchCurrentParameterState(ctx, r, r.DatabaseClientFactory, inst) if err != nil { msg := "parameterUpdateStateMachine: Sanity check failed for instance parameters" r.recordEventAndUpdateStatus(ctx, &inst, v1.ConditionFalse, k8s.ParameterUpdateRollbackInProgress, fmt.Sprintf("%s: %v", msg, err), log) return ctrl.Result{Requeue: true}, err } inst.Status.CurrentParameters = dynamicParamsRollbackState msg := "parameterUpdateStateMachine: parameter update in progress" r.recordEventAndUpdateStatus(ctx, &inst, v1.ConditionFalse, k8s.ParameterUpdateInProgress, msg, log) log.Info("parameterUpdateStateMachine: SM CreateComplete -> ParameterUpdateInProgress") case k8s.ParameterUpdateInProgress: restartRequired, err := r.setParameters(ctx, inst, log) if err != nil { msg := "parameterUpdateStateMachine: Error while setting instance parameters" r.recordEventAndUpdateStatus(ctx, &inst, v1.ConditionFalse, k8s.ParameterUpdateRollbackInProgress, fmt.Sprintf("%s: %v", msg, err), log) log.Info("parameterUpdateStateMachine: SM ParameterUpdateInProgress -> ParameterUpdateRollbackInProgress") return ctrl.Result{Requeue: true}, nil } if restartRequired { log.Info("parameterUpdateStateMachine: static parameter specified in config, scheduling restart to activate them") if err := controllers.BounceDatabase(ctx, r, r.DatabaseClientFactory, inst.Namespace, inst.Name, controllers.BounceDatabaseRequest{ Sid: inst.Spec.CDBName, }); err != nil { msg := "parameterUpdateStateMachine: error while restarting database after setting static parameters" r.recordEventAndUpdateStatus(ctx, &inst, v1.ConditionFalse, k8s.ParameterUpdateRollbackInProgress, fmt.Sprintf("%s: %v", msg, err), log) log.Info("parameterUpdateStateMachine: SM ParameterUpdateInProgress -> ParameterUpdateRollbackInProgress") return ctrl.Result{Requeue: true}, nil } } r.recordEventAndUpdateStatus(ctx, &inst, v1.ConditionFalse, k8s.ParameterUpdateComplete, "", log) log.Info("parameterUpdateStateMachine: SM ParameterUpdateInProgress -> ParameterUpdateComplete") return ctrl.Result{Requeue: true}, nil case k8s.ParameterUpdateComplete: inst.Status.CurrentParameters = inst.Spec.Parameters msg := "parameterUpdateStateMachine: Parameter update successful" r.recordEventAndUpdateStatus(ctx, &inst, v1.ConditionTrue, k8s.CreateComplete, msg, log) inst.Status.CurrentActiveStateMachine = "" log.Info("parameterUpdateStateMachine: SM ParameterUpdateComplete -> CreateComplete") return ctrl.Result{}, nil case k8s.ParameterUpdateRollbackInProgress: if err := r.initiateRecovery(ctx, inst, inst.Status.CurrentParameters, log); err != nil { log.Info("parameterUpdateStateMachine: recovery failed, instance currently in irrecoverable state", "err", err) return ctrl.Result{}, err } inst.Status.LastFailedParameterUpdate = inst.Spec.Parameters msg := "parameterUpdateStateMachine: instance recovered after bad parameter update" r.recordEventAndUpdateStatus(ctx, &inst, v1.ConditionTrue, k8s.CreateComplete, msg, log) inst.Status.CurrentActiveStateMachine = "" log.Info("parameterUpdateStateMachine: SM ParameterUpdateRollbackInProgress -> CreateComplete") return ctrl.Result{}, nil } return ctrl.Result{}, nil } func isParameterUpdateStateMachineEntryCondition(inst *v1alpha1.Instance) bool { instanceReadyCond := k8s.FindCondition(inst.Status.Conditions, k8s.Ready) dbInstanceCond := k8s.FindCondition(inst.Status.Conditions, k8s.DatabaseInstanceReady) return controllers.ParameterUpdateStateMachine == inst.Status.CurrentActiveStateMachine || (k8s.ConditionStatusEquals(instanceReadyCond, v1.ConditionTrue) && k8s.ConditionStatusEquals(dbInstanceCond, v1.ConditionTrue) && inst.Spec.Parameters != nil) } // initiateRecovery will recover the config file (which contains the static // parameters) to the last known working copy if the static // parameter update failed (which caused the database to be non-functional // after a restart). func (r *InstanceReconciler) initiateRecovery(ctx context.Context, inst v1alpha1.Instance, dynamicParams map[string]string, log logr.Logger) error { log.Info("initiateRecovery: initiating recovery of config file") if err := controllers.RecoverConfigFile(ctx, r.DatabaseClientFactory, r.Client, inst.Namespace, inst.Name, inst.Spec.CDBName); err != nil { msg := "initiateRecovery: error while recovering config file" log.Info(msg, "err", err) return err } if err := controllers.BounceDatabase(ctx, r, r.DatabaseClientFactory, inst.Namespace, inst.Name, controllers.BounceDatabaseRequest{ Sid: inst.Spec.CDBName, }); err != nil { return err } log.Info("initiateRecovery: database bounced completed successfully") // Rollback all the dynamic parameter updates after the database has recovered for k, v := range dynamicParams { _, err := controllers.SetParameter(ctx, r.DatabaseClientFactory, r.Client, inst.Namespace, inst.Name, k, v) if err != nil { log.Error(err, "initiateRecovery: error while rolling back dynamic parameters") return err } } log.Info("initiateRecovery: rolling back of dynamic parameters completed successfully", "dynamicParams", dynamicParams) return nil } func (r *InstanceReconciler) sanityCheckTimeRange(inst v1alpha1.Instance, log logr.Logger) (ctrl.Result, error) { if !maintenance.HasValidTimeRanges(inst.Spec.MaintenanceWindow) { return ctrl.Result{}, fmt.Errorf("MaintenanceWindow specification is not valid: %+v", inst.Spec.MaintenanceWindow) } now := time.Now() if maintenance.InRange(inst.Spec.MaintenanceWindow, now) { return ctrl.Result{}, nil } nextStart, _, err := maintenance.NextWindow(inst.Spec.MaintenanceWindow, now) // If there is no future maintenance windows (next window), return an error. if err != nil { return ctrl.Result{}, errors.New("current time is past the maintenance time range") } // Otherwise: requeue for processing when the maintenance window opens up. restartWaitTime := nextStart.Sub(now) log.Info("parameterUpdateStateMachine: Wait time before restart ", "restartWaitTime", restartWaitTime.Seconds()) return ctrl.Result{RequeueAfter: restartWaitTime}, errors.New("current time is not within the maintenance window") } func mapsToStringArray(parameterMap map[string]string) []string { var parameters []string for k, v := range parameterMap { parameters = append(parameters, fmt.Sprintf("%s=%s", k, v)) } return parameters }