From ecbd12d3c0f02f7ca8889eb5e8d6c5222d26fae4 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo1991@outlook.com>
Date: Tue, 13 Aug 2024 18:16:27 +0800
Subject: [PATCH] feat(controller): support autorollback config

---
 .../0010_appsettings_autorollback.py           | 18 ++++++++++++++++++
 rootfs/api/models/app.py                       |  4 ++++
 rootfs/api/models/appsettings.py               | 12 +++++++++++-
 rootfs/api/models/release.py                   | 11 ++++++++++-
 rootfs/api/tests/test_app_settings.py          | 16 ++++++++++++++++
 5 files changed, 59 insertions(+), 2 deletions(-)
 create mode 100644 rootfs/api/migrations/0010_appsettings_autorollback.py

diff --git a/rootfs/api/migrations/0010_appsettings_autorollback.py b/rootfs/api/migrations/0010_appsettings_autorollback.py
new file mode 100644
index 000000000..aa4ab8f88
--- /dev/null
+++ b/rootfs/api/migrations/0010_appsettings_autorollback.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.15 on 2024-08-13 07:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('api', '0009_remove_appsettings_canaries_remove_release_canary_and_more'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='appsettings',
+            name='autorollback',
+            field=models.BooleanField(default=True),
+        ),
+    ]
diff --git a/rootfs/api/models/app.py b/rootfs/api/models/app.py
index 515ce2d33..a3d18fe37 100644
--- a/rootfs/api/models/app.py
+++ b/rootfs/api/models/app.py
@@ -240,6 +240,10 @@ def pipeline(self, release, force_deploy=False, rollback_on_failure=True):
                         raise DryccException(f'pipeline run state error: {state}')
             if procfile_types is None or len(procfile_types) > 0:
                 self.log(f"{prefix} starts running pipeline.deploy")
+                app_settings = self.appsettings_set.latest()
+                if app_settings.autorollback is False:
+                    self.log(f"{prefix} deploy do not rollback on failure")
+                    rollback_on_failure = False
                 self.deploy(release, procfile_types, force_deploy, rollback_on_failure)
             else:
                 self.log(f"{prefix} no changes, skip executing pipeline.deploy")
diff --git a/rootfs/api/models/appsettings.py b/rootfs/api/models/appsettings.py
index 94f658257..c68346beb 100644
--- a/rootfs/api/models/appsettings.py
+++ b/rootfs/api/models/appsettings.py
@@ -20,6 +20,7 @@ class AppSettings(UuidAuditedModel):
     owner = models.ForeignKey(User, on_delete=models.PROTECT)
     app = models.ForeignKey('App', on_delete=models.CASCADE)
     routable = models.BooleanField(default=True)
+    autorollback = models.BooleanField(default=True)
     autoscale = models.JSONField(default=dict, blank=True)
     label = models.JSONField(default=dict, blank=True)
 
@@ -71,6 +72,15 @@ def _update_routable(self, previous_settings):
         elif old != new:
             self.summary += ["{} changed routablity from {} to {}".format(self.owner, old, new)]
 
+    def _update_autorollback(self, previous_settings):
+        old = getattr(previous_settings, 'autorollback', None)
+        new = getattr(self, 'autorollback', None)
+        # if nothing changed copy the settings from previous
+        if new is None and old is not None:
+            setattr(self, 'autorollback', old)
+        elif old != new:
+            self.summary += ["{} changed autorollback from {} to {}".format(self.owner, old, new)]
+
     def _update_autoscale(self, previous_settings):
         data = getattr(previous_settings, 'autoscale', {}).copy()
         new = getattr(self, 'autoscale', {}).copy()
@@ -149,7 +159,7 @@ def _update_fields(self, ignore_update_fields=None):
             previous_settings = self.app.appsettings_set.latest()
         except AppSettings.DoesNotExist:
             pass
-        update_fields = ["routable", "autoscale", "label"]
+        update_fields = ["routable", "autorollback", "autoscale", "label"]
         try:
             for update_field in update_fields:
                 if ignore_update_fields is None or update_field not in ignore_update_fields:
diff --git a/rootfs/api/models/release.py b/rootfs/api/models/release.py
index 6c89222bc..baadb7ad0 100644
--- a/rootfs/api/models/release.py
+++ b/rootfs/api/models/release.py
@@ -2,6 +2,7 @@
 
 from django.conf import settings
 from django.db import models
+from django.db.models import Q
 from django.contrib.auth import get_user_model
 from api.utils import dict_diff
 from api.tasks import run_pipeline
@@ -9,6 +10,7 @@
 from scheduler import KubeHTTPException
 from scheduler.resources.pod import DEFAULT_CONTAINER_PORT
 from .base import UuidAuditedModel
+from .appsettings import AppSettings
 
 
 User = get_user_model()
@@ -197,9 +199,16 @@ def previous(self):
         if self.pk:
             releases = releases.exclude(pk=self.pk)
 
+        q = Q(failed=False, state="succeed")
+        try:
+            app_settings = self.app.appsettings_set.latest()
+            if app_settings.autorollback is False:
+                q = Q(state__in=["crashed", "succeed"])
+        except AppSettings.DoesNotExist:
+            pass
         try:
             # Get the Release previous to this one
-            prev_release = releases.filter(failed=False, state="succeed").latest()
+            prev_release = releases.filter(q).latest()
         except Release.DoesNotExist:
             prev_release = None
         return prev_release
diff --git a/rootfs/api/tests/test_app_settings.py b/rootfs/api/tests/test_app_settings.py
index 75cf0f2d3..d6db77e50 100644
--- a/rootfs/api/tests/test_app_settings.py
+++ b/rootfs/api/tests/test_app_settings.py
@@ -40,6 +40,22 @@ def test_settings_routable(self, mock_requests):
         self.assertEqual(response.status_code, 201, response.data)
         self.assertFalse(app.appsettings_set.latest().routable)
 
+    def test_settings_autorollback(self, mock_requests):
+        """
+        Create an application with the autorollback flag turned on or off
+        """
+        # create app, expecting autorollback to be true
+        app_id = self.create_app()
+        app = App.objects.get(id=app_id)
+        self.assertTrue(app.appsettings_set.latest().autorollback)
+        # Set autorollback to false
+        response = self.client.post(
+            f'/v2/apps/{app.id}/settings',
+            {'autorollback': False}
+        )
+        self.assertEqual(response.status_code, 201, response.data)
+        self.assertFalse(app.appsettings_set.latest().autorollback)
+
     def test_autoscale(self, mock_requests):
         """
         Test that autoscale can be applied
