diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index 70d7d780..4d6f58b6 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -88,6 +88,25 @@ jobs: # Expect the new column to be present. kubectl exec $POD_MYSQL -- mysql -uroot -h 127.0.0.1 -ppass -e "SHOW COLUMNS FROM myapp.users LIKE 'phone';" + + # Expect the devdb deployment is scaled to 1. + kubectl get deployment atlasschema-mysql-atlas-dev-db -o=jsonpath='{.spec.replicas}' | grep -q '1' + + # SET PREWARM_DEVDB to true + kubectl set env -n atlas-operator-system deployment/atlas-operator-controller-manager PREWARM_DEVDB=false + + # Reset database resources + kubectl delete pods -l app=mysql + kubectl wait --for condition=ready pods -l app=mysql --timeout=60s + + # Apply the desired schema and wait for it to be ready. + kubectl delete -f config/integration/schema + kubectl apply -f config/integration/schema + kubectl wait --for=condition=ready --timeout=120s atlasschemas --all + + # Expect the devdb deployment is scaled to 0. + kubectl get deployment atlasschema-mysql-atlas-dev-db -o=jsonpath='{.spec.replicas}' | grep -q '0' + - name: Reset database resources run: | kubectl delete pods -l app=mysql diff --git a/charts/atlas-operator/templates/deployment.yaml b/charts/atlas-operator/templates/deployment.yaml index 37e33d75..5f2bf5e0 100644 --- a/charts/atlas-operator/templates/deployment.yaml +++ b/charts/atlas-operator/templates/deployment.yaml @@ -45,6 +45,11 @@ spec: {{- toYaml .Values.resources | nindent 12 }} securityContext: {{- toYaml .Values.containerSecurityContext | nindent 12 }} + env: + {{- if .Values.prewarmDevDB }} + - name: PREWARM_DEVDB + value: "true" + {{- end }} {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} diff --git a/charts/atlas-operator/values.yaml b/charts/atlas-operator/values.yaml index b8fc5f26..38ae71bf 100644 --- a/charts/atlas-operator/values.yaml +++ b/charts/atlas-operator/values.yaml @@ -40,3 +40,7 @@ nodeSelector: {} tolerations: [] affinity: {} + +# By default, the operator will recreate devdb pods after migration +# Set this to true to keep the devdb pods around. +prewarmDevDB: true \ No newline at end of file diff --git a/controllers/atlasschema_controller.go b/controllers/atlasschema_controller.go index ff493142..cba3723e 100644 --- a/controllers/atlasschema_controller.go +++ b/controllers/atlasschema_controller.go @@ -58,6 +58,7 @@ type ( configMapWatcher *watch.ResourceWatcher secretWatcher *watch.ResourceWatcher recorder record.EventRecorder + prewarmDevDB bool } // managedData contains information about the managed database and its desired state. managedData struct { @@ -73,7 +74,7 @@ type ( } ) -func NewAtlasSchemaReconciler(mgr Manager, execPath string) *AtlasSchemaReconciler { +func NewAtlasSchemaReconciler(mgr Manager, execPath string, prewarmDevDB bool) *AtlasSchemaReconciler { return &AtlasSchemaReconciler{ Client: mgr.GetClient(), scheme: mgr.GetScheme(), @@ -81,6 +82,7 @@ func NewAtlasSchemaReconciler(mgr Manager, execPath string) *AtlasSchemaReconcil configMapWatcher: watch.New(), secretWatcher: watch.New(), recorder: mgr.GetEventRecorderFor("atlasschema-controller"), + prewarmDevDB: prewarmDevDB, } } diff --git a/controllers/atlasschema_controller_test.go b/controllers/atlasschema_controller_test.go index 2fe9f86a..dc0d5139 100644 --- a/controllers/atlasschema_controller_test.go +++ b/controllers/atlasschema_controller_test.go @@ -45,6 +45,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) const ( @@ -109,7 +110,9 @@ func TestReconcile_Reconcile(t *testing.T) { ObjectMeta: meta, Spec: dbv1alpha1.AtlasSchemaSpec{}, } - h, reconcile := newRunner(NewAtlasSchemaReconciler, func(cb *fake.ClientBuilder) { + h, reconcile := newRunner(func(mgr Manager, name string) reconcile.Reconciler { + return NewAtlasSchemaReconciler(mgr, name, true) + }, func(cb *fake.ClientBuilder) { cb.WithObjects(obj) }) assert := func(except ctrl.Result, ready bool, reason, msg string) { diff --git a/controllers/devdb.go b/controllers/devdb.go index e3a26403..b7266ca2 100644 --- a/controllers/devdb.go +++ b/controllers/devdb.go @@ -46,6 +46,23 @@ const ( // cleanUp clean up any resources created by the controller func (r *AtlasSchemaReconciler) cleanUp(ctx context.Context, sc *dbv1alpha1.AtlasSchema) { + // If prewarmDevDB is false, scale down the deployment to 0 + if !r.prewarmDevDB { + deploy := &appsv1.Deployment{} + key := nameDevDB(sc.ObjectMeta) + err := r.Get(ctx, key, deploy) + if err != nil { + r.recorder.Eventf(sc, corev1.EventTypeWarning, "CleanUpDevDB", "Error getting devDB deployment: %v", err) + return + } + deploy.Spec.Replicas = new(int32) + if err := r.Update(ctx, deploy); err != nil { + r.recorder.Eventf(sc, corev1.EventTypeWarning, "CleanUpDevDB", "Error scaling down devDB deployment: %v", err) + } + return + } + + // delete pods to clean up pods := &corev1.PodList{} err := r.List(ctx, pods, client.MatchingLabels(map[string]string{ labelInstance: nameDevDB(sc.ObjectMeta).Name, @@ -69,10 +86,20 @@ func (r *AtlasSchemaReconciler) devURL(ctx context.Context, sc *dbv1alpha1.Atlas } // make sure we have a dev db running key := nameDevDB(sc.ObjectMeta) - switch err := r.Get(ctx, key, &appsv1.Deployment{}); { + deploy := &appsv1.Deployment{} + switch err := r.Get(ctx, key, deploy); { case err == nil: // The dev database already exists, - // return the connection string. + // If it is scaled down, scale it up. + if deploy.Spec.Replicas == nil || *deploy.Spec.Replicas == 0 { + *deploy.Spec.Replicas = int32(1) + if err := r.Update(ctx, deploy); err != nil { + return "", transient(err) + } + r.recorder.Eventf(sc, corev1.EventTypeNormal, "ScaledUpDevDB", "Scaled up dev database deployment: %s", deploy.Name) + return "", transientAfter(errors.New("waiting for dev database to be ready"), 15*time.Second) + } + case apierrors.IsNotFound(err): // The dev database does not exist, create it. deploy, err := deploymentDevDB(key, drv, isSchemaBound(drv, &targetURL)) diff --git a/main.go b/main.go index d4e1dea2..83f9a181 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "bytes" "flag" "os" + "strconv" "time" "golang.org/x/mod/semver" @@ -53,6 +54,8 @@ const ( envNoUpdate = "SKIP_VERCHECK" vercheckURL = "https://vercheck.ariga.io" execPath = "/atlas" + // prewarmDevDB when disabled it deletes the devDB pods after the schema is created + prewarmDevDB = "PREWARM_DEVDB" ) func init() { @@ -91,7 +94,8 @@ func main() { setupLog.Error(err, "unable to start manager") os.Exit(1) } - if err = controllers.NewAtlasSchemaReconciler(mgr, execPath). + prewarmDevDB := getPrewarmDevDBEnv() + if err = controllers.NewAtlasSchemaReconciler(mgr, execPath, prewarmDevDB). SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "AtlasSchema") os.Exit(1) @@ -154,3 +158,18 @@ func checkForUpdate() { <-time.After(24 * time.Hour) } } + +// getPrewarmDevDBEnv returns the value of the env var PREWARM_DEVDB. +// if the env var is not set, it returns true. +func getPrewarmDevDBEnv() bool { + env := os.Getenv(prewarmDevDB) + if env == "" { + return true + } + prewarmDevDB, err := strconv.ParseBool(env) + if err != nil { + setupLog.Error(err, "invalid value for env var PREWARM_DEVDB, expected true or false") + os.Exit(1) + } + return prewarmDevDB +}