diff --git a/Makefile b/Makefile index aae1a48f1..4f5dd72cc 100644 --- a/Makefile +++ b/Makefile @@ -351,7 +351,7 @@ golint: get-ci-tools .PHONY: operator-lint operator-lint: $(LOCALBIN) gowork ## Runs operator-lint - GOBIN=$(LOCALBIN) go install github.com/gibizer/operator-lint@v0.3.0 + GOBIN=$(LOCALBIN) go install github.com/gibizer/operator-lint@v0.5.0 go vet -vettool=$(LOCALBIN)/operator-lint ./... ./api/... .PHONY: gowork diff --git a/api/bases/nova.openstack.org_nova.yaml b/api/bases/nova.openstack.org_nova.yaml index d1098a93a..af7461db2 100644 --- a/api/bases/nova.openstack.org_nova.yaml +++ b/api/bases/nova.openstack.org_nova.yaml @@ -465,6 +465,28 @@ spec: type: object type: object type: object + dbPurge: + description: DBPurge defines the parameters for the DB archiving + and purging cron job + properties: + archiveAge: + default: 30 + description: ArchiveAge defines the minimuma age of the + records in days that can be moved to the shadow tables. + minimum: 1 + type: integer + purgeAge: + default: 90 + description: PurgeAge defines the minimum age of the records + in days that can be deleted from the shadow tables + minimum: 1 + type: integer + schedule: + default: 0 0 * * * + description: Schedule defines when to run the DB maintenance + job in a cron format. By default it runs every midnight. + type: string + type: object hasAPIAccess: description: HasAPIAccess defines if this Cell is configured to have access to the API DB and message bus. diff --git a/api/bases/nova.openstack.org_novacells.yaml b/api/bases/nova.openstack.org_novacells.yaml index eaed164bc..bf2ab2c99 100644 --- a/api/bases/nova.openstack.org_novacells.yaml +++ b/api/bases/nova.openstack.org_novacells.yaml @@ -147,6 +147,28 @@ spec: type: object type: object type: object + dbPurge: + description: DBPurge defines the parameters for the DB archiving and + purging cron job + properties: + archiveAge: + default: 30 + description: ArchiveAge defines the minimuma age of the records + in days that can be moved to the shadow tables. + minimum: 1 + type: integer + purgeAge: + default: 90 + description: PurgeAge defines the minimum age of the records in + days that can be deleted from the shadow tables + minimum: 1 + type: integer + schedule: + default: 0 0 * * * + description: Schedule defines when to run the DB maintenance job + in a cron format. By default it runs every midnight. + type: string + type: object keystoneAuthURL: description: KeystoneAuthURL - the URL that the service in the cell can use to talk to keystone diff --git a/api/bases/nova.openstack.org_novaconductors.yaml b/api/bases/nova.openstack.org_novaconductors.yaml index a485e87ff..61a5d8512 100644 --- a/api/bases/nova.openstack.org_novaconductors.yaml +++ b/api/bases/nova.openstack.org_novaconductors.yaml @@ -83,6 +83,28 @@ spec: added to to /etc//.conf.d directory as custom.conf file. type: string + dbPurge: + description: DBPurge defines the parameters for the DB archiving and + purging cron job + properties: + archiveAge: + default: 30 + description: ArchiveAge defines the minimuma age of the records + in days that can be moved to the shadow tables. + minimum: 1 + type: integer + purgeAge: + default: 90 + description: PurgeAge defines the minimum age of the records in + days that can be deleted from the shadow tables + minimum: 1 + type: integer + schedule: + default: 0 0 * * * + description: Schedule defines when to run the DB maintenance job + in a cron format. By default it runs every midnight. + type: string + type: object keystoneAuthURL: description: KeystoneAuthURL - the URL that the nova-conductor service can use to talk to keystone diff --git a/api/v1beta1/novacell_types.go b/api/v1beta1/novacell_types.go index e0bf1888b..5f77b07d8 100644 --- a/api/v1beta1/novacell_types.go +++ b/api/v1beta1/novacell_types.go @@ -95,6 +95,10 @@ type NovaCellTemplate struct { // MemcachedInstance is the name of the Memcached CR that the services in the cell will use. // If defined then this takes precedence over Nova.Spec.MemcachedInstance for this cel MemcachedInstance string `json:"memcachedInstance"` + + // +kubebuilder:validation:Optional + // DBPurge defines the parameters for the DB archiving and purging cron job + DBPurge NovaCellDBPurge `json:"dbPurge"` } // NovaCellSpec defines the desired state of NovaCell @@ -188,6 +192,34 @@ type NovaCellSpec struct { // +kubebuilder:validation:Required // MemcachedInstance is the name of the Memcached CR that all nova service will use. MemcachedInstance string `json:"memcachedInstance"` + + // +kubebuilder:validation:Optional + // DBPurge defines the parameters for the DB archiving and purging cron job + DBPurge NovaCellDBPurge `json:"dbPurge"` +} + +// NovaCellDBPurge defines the parameters for the DB archiving and purging +// cron job +type NovaCellDBPurge struct { + // +kubebuilder:validation:Optional + // +kubebuilder:default="0 0 * * *" + // Schedule defines when to run the DB maintenance job in a cron format. + // By default it runs every midnight. + Schedule *string `json:"schedule"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default=30 + // +kubebuilder:validation:Minimum=1 + // ArchiveAge defines the minimuma age of the records in days that can be + // moved to the shadow tables. + ArchiveAge *int `json:"archiveAge"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default=90 + // +kubebuilder:validation:Minimum=1 + // PurgeAge defines the minimum age of the records in days that can be + // deleted from the shadow tables + PurgeAge *int `json:"purgeAge"` } // NovaCellStatus defines the observed state of NovaCell diff --git a/api/v1beta1/novaconductor_types.go b/api/v1beta1/novaconductor_types.go index 14af475a7..846619a44 100644 --- a/api/v1beta1/novaconductor_types.go +++ b/api/v1beta1/novaconductor_types.go @@ -131,6 +131,10 @@ type NovaConductorSpec struct { // +kubebuilder:validation:Required // MemcachedInstance is the name of the Memcached CR that all nova service will use. MemcachedInstance string `json:"memcachedInstance"` + + // +kubebuilder:validation:Optional + // DBPurge defines the parameters for the DB archiving and purging cron job + DBPurge NovaCellDBPurge `json:"dbPurge"` } // NovaConductorStatus defines the observed state of NovaConductor @@ -202,6 +206,7 @@ func NewNovaConductorSpec( TLS: novaCell.TLS, PreserveJobs: novaCell.PreserveJobs, MemcachedInstance: novaCell.MemcachedInstance, + DBPurge: novaCell.DBPurge, } return conductorSpec } diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index b15ab9b76..ef111e65a 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -330,6 +330,36 @@ func (in *NovaCell) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NovaCellDBPurge) DeepCopyInto(out *NovaCellDBPurge) { + *out = *in + if in.Schedule != nil { + in, out := &in.Schedule, &out.Schedule + *out = new(string) + **out = **in + } + if in.ArchiveAge != nil { + in, out := &in.ArchiveAge, &out.ArchiveAge + *out = new(int) + **out = **in + } + if in.PurgeAge != nil { + in, out := &in.PurgeAge, &out.PurgeAge + *out = new(int) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NovaCellDBPurge. +func (in *NovaCellDBPurge) DeepCopy() *NovaCellDBPurge { + if in == nil { + return nil + } + out := new(NovaCellDBPurge) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NovaCellDefaults) DeepCopyInto(out *NovaCellDefaults) { *out = *in @@ -398,6 +428,7 @@ func (in *NovaCellSpec) DeepCopyInto(out *NovaCellSpec) { } } out.TLS = in.TLS + in.DBPurge.DeepCopyInto(&out.DBPurge) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NovaCellSpec. @@ -467,6 +498,7 @@ func (in *NovaCellTemplate) DeepCopyInto(out *NovaCellTemplate) { } } out.PasswordSelectors = in.PasswordSelectors + in.DBPurge.DeepCopyInto(&out.DBPurge) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NovaCellTemplate. @@ -755,6 +787,7 @@ func (in *NovaConductorSpec) DeepCopyInto(out *NovaConductorSpec) { *out = *in in.NovaServiceBase.DeepCopyInto(&out.NovaServiceBase) out.TLS = in.TLS + in.DBPurge.DeepCopyInto(&out.DBPurge) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NovaConductorSpec. diff --git a/config/crd/bases/nova.openstack.org_nova.yaml b/config/crd/bases/nova.openstack.org_nova.yaml index d1098a93a..af7461db2 100644 --- a/config/crd/bases/nova.openstack.org_nova.yaml +++ b/config/crd/bases/nova.openstack.org_nova.yaml @@ -465,6 +465,28 @@ spec: type: object type: object type: object + dbPurge: + description: DBPurge defines the parameters for the DB archiving + and purging cron job + properties: + archiveAge: + default: 30 + description: ArchiveAge defines the minimuma age of the + records in days that can be moved to the shadow tables. + minimum: 1 + type: integer + purgeAge: + default: 90 + description: PurgeAge defines the minimum age of the records + in days that can be deleted from the shadow tables + minimum: 1 + type: integer + schedule: + default: 0 0 * * * + description: Schedule defines when to run the DB maintenance + job in a cron format. By default it runs every midnight. + type: string + type: object hasAPIAccess: description: HasAPIAccess defines if this Cell is configured to have access to the API DB and message bus. diff --git a/config/crd/bases/nova.openstack.org_novacells.yaml b/config/crd/bases/nova.openstack.org_novacells.yaml index eaed164bc..bf2ab2c99 100644 --- a/config/crd/bases/nova.openstack.org_novacells.yaml +++ b/config/crd/bases/nova.openstack.org_novacells.yaml @@ -147,6 +147,28 @@ spec: type: object type: object type: object + dbPurge: + description: DBPurge defines the parameters for the DB archiving and + purging cron job + properties: + archiveAge: + default: 30 + description: ArchiveAge defines the minimuma age of the records + in days that can be moved to the shadow tables. + minimum: 1 + type: integer + purgeAge: + default: 90 + description: PurgeAge defines the minimum age of the records in + days that can be deleted from the shadow tables + minimum: 1 + type: integer + schedule: + default: 0 0 * * * + description: Schedule defines when to run the DB maintenance job + in a cron format. By default it runs every midnight. + type: string + type: object keystoneAuthURL: description: KeystoneAuthURL - the URL that the service in the cell can use to talk to keystone diff --git a/config/crd/bases/nova.openstack.org_novaconductors.yaml b/config/crd/bases/nova.openstack.org_novaconductors.yaml index a485e87ff..61a5d8512 100644 --- a/config/crd/bases/nova.openstack.org_novaconductors.yaml +++ b/config/crd/bases/nova.openstack.org_novaconductors.yaml @@ -83,6 +83,28 @@ spec: added to to /etc//.conf.d directory as custom.conf file. type: string + dbPurge: + description: DBPurge defines the parameters for the DB archiving and + purging cron job + properties: + archiveAge: + default: 30 + description: ArchiveAge defines the minimuma age of the records + in days that can be moved to the shadow tables. + minimum: 1 + type: integer + purgeAge: + default: 90 + description: PurgeAge defines the minimum age of the records in + days that can be deleted from the shadow tables + minimum: 1 + type: integer + schedule: + default: 0 0 * * * + description: Schedule defines when to run the DB maintenance job + in a cron format. By default it runs every midnight. + type: string + type: object keystoneAuthURL: description: KeystoneAuthURL - the URL that the nova-conductor service can use to talk to keystone diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 64af954f5..1f5f360ae 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -39,6 +39,18 @@ rules: - patch - update - watch +- apiGroups: + - batch + resources: + - cronjobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - batch resources: diff --git a/controllers/nova_controller.go b/controllers/nova_controller.go index f49571856..286fff7f3 100644 --- a/controllers/nova_controller.go +++ b/controllers/nova_controller.go @@ -876,6 +876,7 @@ func (r *NovaReconciler) ensureCell( TLS: instance.Spec.APIServiceTemplate.TLS.Ca, PreserveJobs: instance.Spec.PreserveJobs, MemcachedInstance: getMemcachedInstance(instance, cellTemplate), + DBPurge: cellTemplate.DBPurge, } if cellTemplate.HasAPIAccess { cellSpec.APIDatabaseHostname = apiDB.GetDatabaseHostname() diff --git a/controllers/novaconductor_controller.go b/controllers/novaconductor_controller.go index d305dc726..0aeefc051 100644 --- a/controllers/novaconductor_controller.go +++ b/controllers/novaconductor_controller.go @@ -40,6 +40,7 @@ import ( memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1" common "github.com/openstack-k8s-operators/lib-common/modules/common" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + "github.com/openstack-k8s-operators/lib-common/modules/common/cronjob" "github.com/openstack-k8s-operators/lib-common/modules/common/env" helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" job "github.com/openstack-k8s-operators/lib-common/modules/common/job" @@ -73,6 +74,7 @@ func (r *NovaConductorReconciler) GetLogger(ctx context.Context) logr.Logger { //+kubebuilder:rbac:groups=k8s.cni.cncf.io,resources=network-attachment-definitions,verbs=get;list;watch //+kubebuilder:rbac:groups=memcached.openstack.org,resources=memcacheds,verbs=get;list;watch;update; //+kubebuilder:rbac:groups=memcached.openstack.org,resources=memcacheds/finalizers,verbs=update +// +kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete; // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -274,6 +276,11 @@ func (r *NovaConductorReconciler) Reconcile(ctx context.Context, req ctrl.Reques return result, err } + err = r.ensureDBPurgeCronJob(ctx, h, instance, serviceAnnotations) + if err != nil { + return ctrl.Result{}, err + } + // clean up nova services from nova db should be always a last step in reconcile err = r.cleanServiceFromNovaDb(ctx, h, instance, secret, Log) if err != nil { @@ -348,6 +355,11 @@ func (r *NovaConductorReconciler) initConditions( condition.InitReason, condition.MemcachedReadyInitMessage, ), + condition.UnknownCondition( + condition.CronJobReadyCondition, + condition.InitReason, + condition.CronJobReadyInitMessage, + ), ) instance.Status.Conditions.Init(&cl) @@ -553,6 +565,34 @@ func (r *NovaConductorReconciler) ensureDeployment( return ctrl.Result{}, nil } +func (r *NovaConductorReconciler) ensureDBPurgeCronJob( + ctx context.Context, + h *helper.Helper, + instance *novav1.NovaConductor, + annotations map[string]string, +) error { + serviceLabels := map[string]string{ + common.AppSelector: NovaConductorLabelPrefix, + } + cronDef := novaconductor.DBPurgeCronJob(instance, serviceLabels, annotations) + cronjob := cronjob.NewCronJob(cronDef, r.RequeueTimeout) + + _, err := cronjob.CreateOrPatch(ctx, h) + if err != nil { + instance.Status.Conditions.Set(condition.FalseCondition( + condition.CronJobReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.CronJobReadyErrorMessage, + err.Error())) + return err + } + + instance.Status.Conditions.MarkTrue( + condition.CronJobReadyCondition, condition.CronJobReadyMessage) + return nil +} + func (r *NovaConductorReconciler) cleanServiceFromNovaDb( ctx context.Context, h *helper.Helper, @@ -700,6 +740,7 @@ func (r *NovaConductorReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&novav1.NovaConductor{}). Owns(&v1.StatefulSet{}). Owns(&batchv1.Job{}). + Owns(&batchv1.CronJob{}). Owns(&corev1.Secret{}). // watch the input secrets Watches( diff --git a/pkg/novaconductor/dbpurge.go b/pkg/novaconductor/dbpurge.go new file mode 100644 index 000000000..15eb64a3d --- /dev/null +++ b/pkg/novaconductor/dbpurge.go @@ -0,0 +1,99 @@ +package novaconductor + +import ( + "fmt" + "strings" + + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + "github.com/openstack-k8s-operators/lib-common/modules/common/env" + novav1 "github.com/openstack-k8s-operators/nova-operator/api/v1beta1" + "github.com/openstack-k8s-operators/nova-operator/pkg/nova" +) + +func DBPurgeCronJob( + instance *novav1.NovaConductor, + labels map[string]string, + annotations map[string]string, +) *batchv1.CronJob { + args := []string{"-c", nova.KollaServiceCommand} + + envVars := map[string]env.Setter{} + envVars["KOLLA_CONFIG_STRATEGY"] = env.SetValue("COPY_ALWAYS") + envVars["KOLLA_BOOTSTRAP"] = env.SetValue("true") + + envVars["ARCHIVE_AGE"] = env.SetValue(fmt.Sprintf("%d", *instance.Spec.DBPurge.ArchiveAge)) + envVars["PURGE_AGE"] = env.SetValue(fmt.Sprintf("%d", *instance.Spec.DBPurge.PurgeAge)) + + env := env.MergeEnvs([]corev1.EnvVar{}, envVars) + + volumes := []corev1.Volume{ + nova.GetConfigVolume(nova.GetServiceConfigSecretName(instance.Name)), + nova.GetScriptVolume(nova.GetScriptSecretName(instance.Name)), + } + volumeMounts := []corev1.VolumeMount{ + nova.GetConfigVolumeMount(), + nova.GetScriptVolumeMount(), + nova.GetKollaConfigVolumeMount("nova-conductor-dbpurge"), + } + + // add CA cert if defined + if instance.Spec.TLS.CaBundleSecretName != "" { + volumes = append(volumes, instance.Spec.TLS.CreateVolume()) + volumeMounts = append(volumeMounts, instance.Spec.TLS.CreateVolumeMounts(nil)...) + } + + // we want to hide the fact that the job is created by the conductor + // controller, but we don't have direct access to the Cell CR name, so we + // remove the known conductor suffix from the Conductor CR name. + name := strings.TrimSuffix(instance.Name, "-conductor") + "-db-purge" + + cron := &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: instance.Namespace, + Labels: labels, + }, + Spec: batchv1.CronJobSpec{ + Schedule: *instance.Spec.DBPurge.Schedule, + ConcurrencyPolicy: batchv1.ForbidConcurrent, + JobTemplate: batchv1.JobTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + Annotations: annotations, + }, + Spec: batchv1.JobSpec{ + Parallelism: ptr.To[int32](1), + Completions: ptr.To[int32](1), + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyOnFailure, + ServiceAccountName: instance.Spec.ServiceAccount, + Volumes: volumes, + Containers: []corev1.Container{ + { + Name: "nova-manage", + Command: []string{ + "/bin/bash", + }, + Args: args, + Image: instance.Spec.ContainerImage, + SecurityContext: &corev1.SecurityContext{ + RunAsUser: ptr.To(nova.NovaUserID), + }, + Env: env, + VolumeMounts: volumeMounts, + }, + }, + }, + }, + }, + }, + }, + } + + return cron +} diff --git a/templates/novaconductor/bin/dbpurge.sh b/templates/novaconductor/bin/dbpurge.sh new file mode 100755 index 000000000..82be3967c --- /dev/null +++ b/templates/novaconductor/bin/dbpurge.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Copyright 2024. +# +# 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. + +set -x +export ARCHIVE_AGE=${ARCHIVE_AGE:?"Please specify ARCHIVE_AGE variable."} +export PURGE_AGE=${PURGE_AGE:?"Please specify PURGE_AGE variable."} +archive_before=$(date --date="${ARCHIVE_AGE} day ago" +%Y-%m-%d) +purge_before=$(date --date="${PURGE_AGE} day ago" +%Y-%m-%d) + +nova-manage db archive_deleted_rows --verbose --until-complete --task-log --before "${archive_before}" +ret=$? +# 0 means no error and nothing is archived +# 1 means no error and someting is archived +if [ $ret -gt "1" ]; then + exit $ret +fi + +nova-manage db purge --verbose --before "${purge_before}" +ret=$? +# 0 means no error and something is deleted +# 3 means no error and nothing is deleted +if [[ $ret -eq 1 || $ret -eq 2 || $ret -gt 3 ]]; then + exit $ret +fi + +exit 0 diff --git a/templates/novaconductor/config/nova-conductor-dbpurge-config.json b/templates/novaconductor/config/nova-conductor-dbpurge-config.json new file mode 100644 index 000000000..7adf60f94 --- /dev/null +++ b/templates/novaconductor/config/nova-conductor-dbpurge-config.json @@ -0,0 +1,37 @@ +{ + "command": "/bin/dbpurge.sh", + "config_files": [ + { + "source": "/var/lib/openstack/config/nova-blank.conf", + "dest": "/etc/nova/nova.conf", + "owner": "nova", + "perm": "0600" + }, + { + "source": "/var/lib/openstack/config/01-nova.conf", + "dest": "/etc/nova/nova.conf.d/01-nova.conf", + "owner": "nova", + "perm": "0600" + }, + { + "source": "/var/lib/openstack/config/02-nova-override.conf", + "dest": "/etc/nova/nova.conf.d/02-nova-override.conf", + "owner": "nova", + "perm": "0600", + "optional": true + }, + { + "source": "/var/lib/openstack/bin/dbpurge.sh", + "dest": "/bin/", + "owner": "nova", + "perm": "0700" + } + ], + "permissions": [ + { + "path": "/var/log/nova", + "owner": "nova:nova", + "recurse": true + } + ] +} diff --git a/test/functional/base_test.go b/test/functional/base_test.go index 9bad73314..9f091e5ef 100644 --- a/test/functional/base_test.go +++ b/test/functional/base_test.go @@ -23,6 +23,7 @@ import ( "github.com/go-logr/logr" . "github.com/onsi/gomega" "golang.org/x/exp/maps" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" k8s_errors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" @@ -340,6 +341,9 @@ func CreateNovaWithCell0(name types.NamespacedName) client.Object { "cell0": map[string]interface{}{ "cellDatabaseUser": "nova_cell0", "hasAPIAccess": true, + "dbPurge": map[string]interface{}{ + "schedule": "1 0 * * *", + }, }, }, "apiMessageBusInstance": cell0.TransportURLName.Name, @@ -550,6 +554,7 @@ type CellNames struct { NovaComputeStatefulSetName types.NamespacedName NovaComputeConfigDataName types.NamespacedName HostDiscoveryJobName types.NamespacedName + DBPurgeCronJobName types.NamespacedName } func GetCellNames(novaName types.NamespacedName, cell string) CellNames { @@ -638,6 +643,10 @@ func GetCellNames(novaName types.NamespacedName, cell string) CellNames { Namespace: novaName.Namespace, Name: cellName.Name + "-compute-config", }, + DBPurgeCronJobName: types.NamespacedName{ + Namespace: novaName.Namespace, + Name: cellName.Name + "-db-purge", + }, } if cell == "cell0" { @@ -977,3 +986,12 @@ func AssertComputeDoesNotExist(name types.NamespacedName) { g.Expect(k8s_errors.IsNotFound(err)).To(BeTrue()) }, timeout, interval).Should(Succeed()) } + +func GetCronJob(name types.NamespacedName) *batchv1.CronJob { + cron := &batchv1.CronJob{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, name, cron)).Should(Succeed()) + }, timeout, interval).Should(Succeed()) + + return cron +} diff --git a/test/functional/nova_controller_test.go b/test/functional/nova_controller_test.go index afa0250e9..5416d52da 100644 --- a/test/functional/nova_controller_test.go +++ b/test/functional/nova_controller_test.go @@ -144,6 +144,14 @@ var _ = Describe("Nova controller", func() { Expect(instance.Status.RegisteredCells).To(BeEmpty()) }) + It("defaults Spec fields", func() { + nova := GetNova(novaNames.NovaName) + cell0Template := nova.Spec.CellTemplates["cell0"] + Expect(cell0Template.DBPurge.Schedule).To(Equal(ptr.To("1 0 * * *"))) + Expect(cell0Template.DBPurge.ArchiveAge).To(Equal(ptr.To(30))) + Expect(cell0Template.DBPurge.PurgeAge).To(Equal(ptr.To(90))) + }) + It("registers nova service to keystone", func() { // assert that the KeystoneService for nova is created keystone.GetKeystoneService(novaNames.KeystoneServiceName) @@ -232,10 +240,16 @@ var _ = Describe("Nova controller", func() { cell := GetNovaCell(cell0.CellCRName) Expect(cell.Spec.ServiceUser).To(Equal("nova")) Expect(cell.Spec.ServiceAccount).To(Equal(novaNames.ServiceAccountName.Name)) + Expect(cell.Spec.DBPurge.Schedule).To(Equal(ptr.To("1 0 * * *"))) + Expect(cell.Spec.DBPurge.ArchiveAge).To(Equal(ptr.To(30))) + Expect(cell.Spec.DBPurge.PurgeAge).To(Equal(ptr.To(90))) conductor := GetNovaConductor(cell0.ConductorName) Expect(conductor.Spec.ServiceUser).To(Equal("nova")) Expect(conductor.Spec.ServiceAccount).To(Equal(novaNames.ServiceAccountName.Name)) + Expect(conductor.Spec.DBPurge.Schedule).To(Equal(ptr.To("1 0 * * *"))) + Expect(conductor.Spec.DBPurge.ArchiveAge).To(Equal(ptr.To(30))) + Expect(conductor.Spec.DBPurge.PurgeAge).To(Equal(ptr.To(90))) // assert that a cell specific internal secret is created with the // proper content and the cell subCRs are configured to use the diff --git a/test/functional/nova_reconfiguration_test.go b/test/functional/nova_reconfiguration_test.go index 87ac554b9..b6ec70c6f 100644 --- a/test/functional/nova_reconfiguration_test.go +++ b/test/functional/nova_reconfiguration_test.go @@ -678,4 +678,27 @@ var _ = Describe("Nova reconfiguration", func() { g.Expect(configData).To(ContainSubstring("memcached_servers=inet_new")) }, timeout, interval).Should(Succeed()) }) + + It("reconfigures DB Pruge job", func() { + Eventually(func(g Gomega) { + nova := GetNova(novaNames.NovaName) + cell0 := nova.Spec.CellTemplates["cell0"] + (&cell0).DBPurge.Schedule = ptr.To("3 0 * * *") + (&cell0).DBPurge.ArchiveAge = ptr.To(33) + (&cell0).DBPurge.PurgeAge = ptr.To(99) + + nova.Spec.CellTemplates["cell0"] = cell0 + + g.Expect(k8sClient.Update(ctx, nova)).To(Succeed()) + }, timeout, interval).Should(Succeed()) + + Eventually(func(g Gomega) { + cron := GetCronJob(cell0.DBPurgeCronJobName) + + g.Expect(cron.Spec.Schedule).To(Equal("3 0 * * *")) + jobEnv := cron.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env + g.Expect(GetEnvVarValue(jobEnv, "ARCHIVE_AGE", "")).To(Equal("33")) + g.Expect(GetEnvVarValue(jobEnv, "PURGE_AGE", "")).To(Equal("99")) + }, timeout, interval).Should(Succeed()) + }) }) diff --git a/test/functional/novacell_controller_test.go b/test/functional/novacell_controller_test.go index fd87cb6fb..cce38f0ae 100644 --- a/test/functional/novacell_controller_test.go +++ b/test/functional/novacell_controller_test.go @@ -59,6 +59,13 @@ var _ = Describe("NovaCell controller", func() { ) }) + It("defaults Spec fields", func() { + cell0 := GetNovaCell(cell0.CellCRName) + Expect(cell0.Spec.DBPurge.Schedule).To(Equal(ptr.To("0 0 * * *"))) + Expect(cell0.Spec.DBPurge.ArchiveAge).To(Equal(ptr.To(30))) + Expect(cell0.Spec.DBPurge.PurgeAge).To(Equal(ptr.To(90))) + }) + It("is not Ready", func() { th.ExpectCondition( cell0.CellCRName, diff --git a/test/functional/novaconductor_controller_test.go b/test/functional/novaconductor_controller_test.go index df5977866..455ccabc1 100644 --- a/test/functional/novaconductor_controller_test.go +++ b/test/functional/novaconductor_controller_test.go @@ -72,6 +72,13 @@ var _ = Describe("NovaConductor controller", func() { Expect(instance.Status.ReadyCount).To(Equal(int32(0))) }) + It("defaults Spec fields", func() { + instance := GetNovaConductor(cell0.ConductorName) + Expect(instance.Spec.DBPurge.Schedule).To(Equal(ptr.To("0 0 * * *"))) + Expect(instance.Spec.DBPurge.ArchiveAge).To(Equal(ptr.To(30))) + Expect(instance.Spec.DBPurge.PurgeAge).To(Equal(ptr.To(90))) + }) + It("is missing the secret", func() { th.ExpectCondition( cell0.ConductorName, @@ -201,6 +208,10 @@ var _ = Describe("NovaConductor controller", func() { scriptData := string(scriptMap.Data["dbsync.sh"]) Expect(scriptData).Should(ContainSubstring("nova-manage db sync")) Expect(scriptData).Should(ContainSubstring("nova-manage api_db sync")) + Expect(scriptMap.Data).Should(HaveKey("dbpurge.sh")) + scriptData = string(scriptMap.Data["dbpurge.sh"]) + Expect(scriptData).Should(ContainSubstring("nova-manage db archive_deleted_rows")) + Expect(scriptData).Should(ContainSubstring("nova-manage db purge")) }) It("stored the input hash in the Status", func() { @@ -333,6 +344,27 @@ var _ = Describe("NovaConductor controller", func() { conductor := GetNovaConductor(cell0.ConductorName) Expect(conductor.Status.ReadyCount).To(BeNumerically(">", 0)) }) + + It("creates the DB purge CronJob", func() { + th.SimulateStatefulSetReplicaReady(cell0.ConductorStatefulSetName) + + conductor := GetNovaConductor(cell0.ConductorName) + cron := GetCronJob(cell0.DBPurgeCronJobName) + + Expect(cron.Spec.Schedule).To(Equal(*conductor.Spec.DBPurge.Schedule)) + jobEnv := cron.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env + Expect(GetEnvVarValue(jobEnv, "ARCHIVE_AGE", "")).To( + Equal(fmt.Sprintf("%d", *conductor.Spec.DBPurge.ArchiveAge))) + Expect(GetEnvVarValue(jobEnv, "PURGE_AGE", "")).To( + Equal(fmt.Sprintf("%d", *conductor.Spec.DBPurge.PurgeAge))) + + th.ExpectCondition( + cell0.ConductorName, + ConditionGetterFunc(NovaConductorConditionGetter), + condition.CronJobReadyCondition, + corev1.ConditionTrue, + ) + }) }) }) diff --git a/test/kuttl/test-suites/default/scale-tests/01-assert.yaml b/test/kuttl/test-suites/default/scale-tests/01-assert.yaml index 1551b8dda..49c9f5f24 100644 --- a/test/kuttl/test-suites/default/scale-tests/01-assert.yaml +++ b/test/kuttl/test-suites/default/scale-tests/01-assert.yaml @@ -513,3 +513,118 @@ status: readyReplicas: 1 replicas: 1 --- +apiVersion: batch/v1 +kind: CronJob +metadata: + labels: + service: nova-conductor + name: nova-kuttl-cell0-db-purge + namespace: nova-kuttl-default +spec: + concurrencyPolicy: Forbid + failedJobsHistoryLimit: 1 + jobTemplate: + spec: + completions: 1 + parallelism: 1 + template: + spec: + containers: + - args: + - -c + - /usr/local/bin/kolla_start + command: + - /bin/bash + env: + - name: ARCHIVE_AGE + value: "30" + - name: KOLLA_BOOTSTRAP + value: "true" + - name: KOLLA_CONFIG_STRATEGY + value: COPY_ALWAYS + - name: PURGE_AGE + value: "90" + image: quay.io/podified-antelope-centos9/openstack-nova-conductor:current-podified + name: nova-manage + securityContext: + runAsUser: 42436 + volumeMounts: + - mountPath: /var/lib/openstack/config + name: config-data + - mountPath: /var/lib/openstack/bin + name: scripts + - mountPath: /var/lib/kolla/config_files/config.json + name: config-data + subPath: nova-conductor-dbpurge-config.json + restartPolicy: OnFailure + serviceAccount: nova-nova-kuttl + serviceAccountName: nova-nova-kuttl + volumes: + - name: config-data + secret: + secretName: nova-kuttl-cell0-conductor-config-data + - name: scripts + secret: + secretName: nova-kuttl-cell0-conductor-scripts + schedule: 0 0 * * * + successfulJobsHistoryLimit: 3 + suspend: false +status: {} +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + labels: + service: nova-conductor + name: nova-kuttl-cell1-db-purge + namespace: nova-kuttl-default +spec: + concurrencyPolicy: Forbid + failedJobsHistoryLimit: 1 + jobTemplate: + spec: + completions: 1 + parallelism: 1 + template: + spec: + containers: + - args: + - -c + - /usr/local/bin/kolla_start + command: + - /bin/bash + env: + - name: ARCHIVE_AGE + value: "30" + - name: KOLLA_BOOTSTRAP + value: "true" + - name: KOLLA_CONFIG_STRATEGY + value: COPY_ALWAYS + - name: PURGE_AGE + value: "90" + image: quay.io/podified-antelope-centos9/openstack-nova-conductor:current-podified + name: nova-manage + securityContext: + runAsUser: 42436 + volumeMounts: + - mountPath: /var/lib/openstack/config + name: config-data + - mountPath: /var/lib/openstack/bin + name: scripts + - mountPath: /var/lib/kolla/config_files/config.json + name: config-data + subPath: nova-conductor-dbpurge-config.json + restartPolicy: OnFailure + serviceAccount: nova-nova-kuttl + serviceAccountName: nova-nova-kuttl + volumes: + - name: config-data + secret: + secretName: nova-kuttl-cell1-conductor-config-data + - name: scripts + secret: + secretName: nova-kuttl-cell1-conductor-scripts + schedule: 0 0 * * * + successfulJobsHistoryLimit: 3 + suspend: false +status: {}