Przeglądaj źródła

fix merge conflict

Alexander Belanger 5 lat temu
rodzic
commit
85968918b2

+ 46 - 0
.github/workflows/dev.yaml

@@ -0,0 +1,46 @@
+name: Deploy to production
+on:
+  push:
+    branches: 
+    - dev
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Set up Cloud SDK
+      uses: google-github-actions/setup-gcloud@master
+      with:
+        project_id: ${{ secrets.GCP_PROJECT_ID }}
+        service_account_key: ${{ secrets.GCP_SA_KEY }}
+        export_default_credentials: true
+    - name: Install kubectl
+      run: |
+        sudo apt-get install kubectl
+    - name: Log in to gcloud CLI
+      run: gcloud auth configure-docker
+    - name: Checkout
+      uses: actions/checkout@v2.3.4
+    - name: Write Dashboard Environment Variables
+      run: |
+        cat >./dashboard/.env <<EOL
+        NODE_ENV=production
+        API_SERVER=dashboard.dev.getporter.dev
+        FULLSTORY_ORG_ID=${{secrets.FULLSTORY_ORG_ID}}
+        DISCORD_KEY=${{secrets.DISCORD_KEY}}
+        DISCORD_CID=${{secrets.DISCORD_CID}}
+        FEEDBACK_ENDPOINT=${{secrets.FEEDBACK_ENDPOINT}}
+        POSTHOG_API_KEY=${{secrets.POSTHOG_API_KEY}}
+        POSTHOG_HOST=${{secrets.POSTHOG_HOST}}
+        EOL
+    - name: Build
+      run: |
+        DOCKER_BUILDKIT=1 docker build . -t gcr.io/porter-dev-273614/porter:dev -f ./docker/Dockerfile
+    - name: Push
+      run: |
+        docker push gcr.io/porter-dev-273614/porter:dev
+    - name: Deploy to cluster
+      run: |
+        gcloud container clusters get-credentials \
+          dev --region us-central1 --project ${{ secrets.GCP_PROJECT_ID }}
+          
+        kubectl rollout restart deployment/porter

+ 46 - 0
.github/workflows/production.yaml

@@ -0,0 +1,46 @@
+name: Deploy to production
+on:
+  push:
+    branches: 
+    - production
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Set up Cloud SDK
+      uses: google-github-actions/setup-gcloud@master
+      with:
+        project_id: ${{ secrets.GCP_PROJECT_ID }}
+        service_account_key: ${{ secrets.GCP_SA_KEY }}
+        export_default_credentials: true
+    - name: Install kubectl
+      run: |
+        sudo apt-get install kubectl
+    - name: Log in to gcloud CLI
+      run: gcloud auth configure-docker
+    - name: Checkout
+      uses: actions/checkout@v2.3.4
+    - name: Write Dashboard Environment Variables
+      run: |
+        cat >./dashboard/.env <<EOL
+        NODE_ENV=production
+        API_SERVER=dashboard.getporter.dev
+        FULLSTORY_ORG_ID=${{secrets.FULLSTORY_ORG_ID}}
+        DISCORD_KEY=${{secrets.DISCORD_KEY}}
+        DISCORD_CID=${{secrets.DISCORD_CID}}
+        FEEDBACK_ENDPOINT=${{secrets.FEEDBACK_ENDPOINT}}
+        POSTHOG_API_KEY=${{secrets.POSTHOG_API_KEY}}
+        POSTHOG_HOST=${{secrets.POSTHOG_HOST}}
+        EOL
+    - name: Build
+      run: |
+        DOCKER_BUILDKIT=1 docker build . -t gcr.io/porter-dev-273614/porter:latest -f ./docker/Dockerfile
+    - name: Push
+      run: |
+        docker push gcr.io/porter-dev-273614/porter:latest
+    - name: Deploy to cluster
+      run: |
+        gcloud container clusters get-credentials \
+          production-2 --region us-central1 --project ${{ secrets.GCP_PROJECT_ID }}
+          
+        kubectl rollout restart deployment/porter

+ 10 - 0
.github/workflows/staging.yaml

@@ -13,6 +13,9 @@ jobs:
         project_id: ${{ secrets.GCP_PROJECT_ID }}
         service_account_key: ${{ secrets.GCP_SA_KEY }}
         export_default_credentials: true
+    - name: Install kubectl
+      run: |
+        sudo apt-get install kubectl
     - name: Log in to gcloud CLI
       run: gcloud auth configure-docker
     - name: Checkout
@@ -35,3 +38,10 @@ jobs:
     - name: Push
       run: |
         docker push gcr.io/porter-dev-273614/porter:staging
+    - name: Deploy to cluster
+      run: |
+        gcloud container clusters get-credentials \
+          staging --region us-central1 --project ${{ secrets.GCP_PROJECT_ID }}
+          
+        kubectl rollout restart deployment/porter
+    

+ 132 - 32
cmd/migrate/keyrotate/rotate.go

@@ -3,6 +3,8 @@ package keyrotate
 import (
 	"fmt"
 
+	"encoding/hex"
+
 	"github.com/porter-dev/porter/internal/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	gorm "github.com/porter-dev/porter/internal/repository/gorm"
@@ -22,6 +24,10 @@ func Rotate(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 
 	fmt.Printf("beginning key rotation from %s to %s\n", string(oldKeyBytes), string(newKeyBytes))
 
+	for i, b := range oldKeyBytes {
+		fmt.Println(i, ":", string(b), string(newKeyBytes[i]))
+	}
+
 	err := rotateClusterModel(db, oldKey, newKey)
 
 	if err != nil {
@@ -127,7 +133,7 @@ func rotateClusterModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 	for i := 0; i < (int(count)/stepSize)+1; i++ {
 		clusters := []*models.Cluster{}
 
-		if err := db.Offset(i * stepSize).Limit(stepSize).Preload("TokenCache").Find(&clusters).Error; err != nil {
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Preload("TokenCache").Find(&clusters).Error; err != nil {
 			return err
 		}
 
@@ -138,6 +144,14 @@ func rotateClusterModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			if err != nil {
 				return err
 			}
+			if err != nil {
+				fmt.Printf("error decrypting cluster %d\n", cluster.ID)
+
+				// in these cases we'll wipe the data -- if it can't be decrypted, we can't
+				// recover it
+				cluster.CertificateAuthorityData = []byte{}
+				cluster.TokenCache.Token = []byte{}
+			}
 		}
 
 		// encrypt with the new key and re-insert
@@ -145,6 +159,8 @@ func rotateClusterModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.EncryptClusterData(cluster, newKey)
 
 			if err != nil {
+				fmt.Printf("error encrypting cluster %d\n", cluster.ID)
+
 				return err
 			}
 
@@ -154,7 +170,7 @@ func rotateClusterModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 		}
 	}
 
-	fmt.Printf("rotated %d clusters", count)
+	fmt.Printf("rotated %d clusters\n", count)
 
 	return nil
 }
@@ -174,7 +190,7 @@ func rotateClusterCandidateModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 	for i := 0; i < (int(count)/stepSize)+1; i++ {
 		ccs := []*models.ClusterCandidate{}
 
-		if err := db.Offset(i * stepSize).Limit(stepSize).Find(&ccs).Error; err != nil {
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&ccs).Error; err != nil {
 			return err
 		}
 
@@ -183,7 +199,12 @@ func rotateClusterCandidateModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.DecryptClusterCandidateData(cc, oldKey)
 
 			if err != nil {
-				return err
+				fmt.Printf("error decrypting cluster candidate %d\n", cc.ID)
+
+				// in these cases we'll wipe the data -- if it can't be decrypted, we can't
+				// recover it
+				cc.AWSClusterIDGuess = []byte{}
+				cc.Kubeconfig = []byte{}
 			}
 		}
 
@@ -192,6 +213,8 @@ func rotateClusterCandidateModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.EncryptClusterCandidateData(cc, newKey)
 
 			if err != nil {
+				fmt.Printf("error encrypting cluster candidate %d\n", cc.ID)
+
 				return err
 			}
 
@@ -201,7 +224,7 @@ func rotateClusterCandidateModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 		}
 	}
 
-	fmt.Printf("rotated %d cluster candidates", count)
+	fmt.Printf("rotated %d cluster candidates\n", count)
 
 	return nil
 }
@@ -221,7 +244,7 @@ func rotateRegistryModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 	for i := 0; i < (int(count)/stepSize)+1; i++ {
 		regs := []*models.Registry{}
 
-		if err := db.Offset(i * stepSize).Limit(stepSize).Preload("TokenCache").Find(&regs).Error; err != nil {
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Preload("TokenCache").Find(&regs).Error; err != nil {
 			return err
 		}
 
@@ -230,7 +253,12 @@ func rotateRegistryModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.DecryptRegistryData(reg, oldKey)
 
 			if err != nil {
-				return err
+				fmt.Printf("error decrypting registry %d\n", reg.ID)
+
+				// in these cases we'll wipe the data -- if it can't be decrypted, we can't
+				// recover it
+				reg.TokenCache.Token = []byte{}
+
 			}
 		}
 
@@ -239,6 +267,8 @@ func rotateRegistryModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.EncryptRegistryData(reg, newKey)
 
 			if err != nil {
+				fmt.Printf("error encrypting registry %d\n", reg.ID)
+
 				return err
 			}
 
@@ -248,7 +278,7 @@ func rotateRegistryModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 		}
 	}
 
-	fmt.Printf("rotated %d registries", count)
+	fmt.Printf("rotated %d registries\n", count)
 
 	return nil
 }
@@ -268,7 +298,7 @@ func rotateHelmRepoModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 	for i := 0; i < (int(count)/stepSize)+1; i++ {
 		hrs := []*models.HelmRepo{}
 
-		if err := db.Offset(i * stepSize).Limit(stepSize).Preload("TokenCache").Find(&hrs).Error; err != nil {
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Preload("TokenCache").Find(&hrs).Error; err != nil {
 			return err
 		}
 
@@ -277,7 +307,11 @@ func rotateHelmRepoModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.DecryptHelmRepoData(hr, oldKey)
 
 			if err != nil {
-				return err
+				fmt.Printf("error decrypting helm repo %d\n", hr.ID)
+
+				// in these cases we'll wipe the data -- if it can't be decrypted, we can't
+				// recover it
+				hr.TokenCache.Token = []byte{}
 			}
 		}
 
@@ -286,6 +320,8 @@ func rotateHelmRepoModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.EncryptHelmRepoData(hr, newKey)
 
 			if err != nil {
+				fmt.Printf("error encrypting helm repo %d\n", hr.ID)
+
 				return err
 			}
 
@@ -295,7 +331,7 @@ func rotateHelmRepoModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 		}
 	}
 
-	fmt.Printf("rotated %d helm repos", count)
+	fmt.Printf("rotated %d helm repos\n", count)
 
 	return nil
 }
@@ -315,7 +351,7 @@ func rotateInfraModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 	for i := 0; i < (int(count)/stepSize)+1; i++ {
 		infras := []*models.Infra{}
 
-		if err := db.Offset(i * stepSize).Limit(stepSize).Find(&infras).Error; err != nil {
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&infras).Error; err != nil {
 			return err
 		}
 
@@ -324,7 +360,17 @@ func rotateInfraModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.DecryptInfraData(infra, oldKey)
 
 			if err != nil {
-				return err
+				oldKeyBytes := make([]byte, 32)
+				newKeyBytes := make([]byte, 32)
+
+				copy(oldKeyBytes[:], oldKey[:])
+				copy(newKeyBytes[:], newKey[:])
+
+				fmt.Printf("error decrypting infra %d, %s, %s, %s\n", infra.ID, hex.EncodeToString(infra.LastApplied), string(oldKeyBytes), string(newKeyBytes))
+
+				// in these cases we'll wipe the data -- if it can't be decrypted, we can't
+				// recover it
+				infra.LastApplied = []byte{}
 			}
 		}
 
@@ -333,6 +379,8 @@ func rotateInfraModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.EncryptInfraData(infra, newKey)
 
 			if err != nil {
+				fmt.Printf("error encrypting infra %d\n", infra.ID)
+
 				return err
 			}
 
@@ -342,7 +390,7 @@ func rotateInfraModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 		}
 	}
 
-	fmt.Printf("rotated %d infras", count)
+	fmt.Printf("rotated %d infras\n", count)
 
 	return nil
 }
@@ -362,7 +410,7 @@ func rotateKubeIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 	for i := 0; i < (int(count)/stepSize)+1; i++ {
 		kis := []*ints.KubeIntegration{}
 
-		if err := db.Offset(i * stepSize).Limit(stepSize).Find(&kis).Error; err != nil {
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&kis).Error; err != nil {
 			return err
 		}
 
@@ -371,7 +419,16 @@ func rotateKubeIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.DecryptKubeIntegrationData(ki, oldKey)
 
 			if err != nil {
-				return err
+				fmt.Printf("error decrypting kube integration %d\n", ki.ID)
+
+				// in these cases we'll wipe the data -- if it can't be decrypted, we can't
+				// recover it
+				ki.ClientCertificateData = []byte{}
+				ki.ClientKeyData = []byte{}
+				ki.Token = []byte{}
+				ki.Username = []byte{}
+				ki.Password = []byte{}
+				ki.Kubeconfig = []byte{}
 			}
 		}
 
@@ -380,6 +437,8 @@ func rotateKubeIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.EncryptKubeIntegrationData(ki, newKey)
 
 			if err != nil {
+				fmt.Printf("error encrypting kube integration %d\n", ki.ID)
+
 				return err
 			}
 
@@ -389,7 +448,7 @@ func rotateKubeIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 		}
 	}
 
-	fmt.Printf("rotated %d kube integrations", count)
+	fmt.Printf("rotated %d kube integrations\n", count)
 
 	return nil
 }
@@ -409,7 +468,7 @@ func rotateBasicIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 	for i := 0; i < (int(count)/stepSize)+1; i++ {
 		basics := []*ints.BasicIntegration{}
 
-		if err := db.Offset(i * stepSize).Limit(stepSize).Find(&basics).Error; err != nil {
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&basics).Error; err != nil {
 			return err
 		}
 
@@ -418,7 +477,12 @@ func rotateBasicIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.DecryptBasicIntegrationData(basic, oldKey)
 
 			if err != nil {
-				return err
+				fmt.Printf("error decrypting basic integration %d\n", basic.ID)
+
+				// in these cases we'll wipe the data -- if it can't be decrypted, we can't
+				// recover it
+				basic.Username = []byte{}
+				basic.Password = []byte{}
 			}
 		}
 
@@ -427,6 +491,8 @@ func rotateBasicIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.EncryptBasicIntegrationData(basic, newKey)
 
 			if err != nil {
+				fmt.Printf("error encrypting basic integration %d\n", basic.ID)
+
 				return err
 			}
 
@@ -436,7 +502,7 @@ func rotateBasicIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 		}
 	}
 
-	fmt.Printf("rotated %d basic integrations", count)
+	fmt.Printf("rotated %d basic integrations\n", count)
 
 	return nil
 }
@@ -456,7 +522,7 @@ func rotateOIDCIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 	for i := 0; i < (int(count)/stepSize)+1; i++ {
 		oidcs := []*ints.OIDCIntegration{}
 
-		if err := db.Offset(i * stepSize).Limit(stepSize).Find(&oidcs).Error; err != nil {
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&oidcs).Error; err != nil {
 			return err
 		}
 
@@ -465,7 +531,16 @@ func rotateOIDCIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.DecryptOIDCIntegrationData(oidc, oldKey)
 
 			if err != nil {
-				return err
+				fmt.Printf("error decrypting oidc integration %d\n", oidc.ID)
+
+				// in these cases we'll wipe the data -- if it can't be decrypted, we can't
+				// recover it
+				oidc.IssuerURL = []byte{}
+				oidc.ClientID = []byte{}
+				oidc.ClientSecret = []byte{}
+				oidc.CertificateAuthorityData = []byte{}
+				oidc.IDToken = []byte{}
+				oidc.RefreshToken = []byte{}
 			}
 		}
 
@@ -474,6 +549,8 @@ func rotateOIDCIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.EncryptOIDCIntegrationData(oidc, newKey)
 
 			if err != nil {
+				fmt.Printf("error encrypting oidc integration %d\n", oidc.ID)
+
 				return err
 			}
 
@@ -483,7 +560,7 @@ func rotateOIDCIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 		}
 	}
 
-	fmt.Printf("rotated %d oidc integrations", count)
+	fmt.Printf("rotated %d oidc integrations\n", count)
 
 	return nil
 }
@@ -503,7 +580,7 @@ func rotateOAuthIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 	for i := 0; i < (int(count)/stepSize)+1; i++ {
 		oauths := []*ints.OAuthIntegration{}
 
-		if err := db.Offset(i * stepSize).Limit(stepSize).Find(&oauths).Error; err != nil {
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&oauths).Error; err != nil {
 			return err
 		}
 
@@ -512,7 +589,13 @@ func rotateOAuthIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.DecryptOAuthIntegrationData(oauth, oldKey)
 
 			if err != nil {
-				return err
+				fmt.Printf("error decrypting oauth integration %d\n", oauth.ID)
+
+				// in these cases we'll wipe the data -- if it can't be decrypted, we can't
+				// recover it
+				oauth.ClientID = []byte{}
+				oauth.AccessToken = []byte{}
+				oauth.RefreshToken = []byte{}
 			}
 		}
 
@@ -521,6 +604,8 @@ func rotateOAuthIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.EncryptOAuthIntegrationData(oauth, newKey)
 
 			if err != nil {
+				fmt.Printf("error encrypting oauth integration %d\n", oauth.ID)
+
 				return err
 			}
 
@@ -530,7 +615,7 @@ func rotateOAuthIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 		}
 	}
 
-	fmt.Printf("rotated %d oauth integrations", count)
+	fmt.Printf("rotated %d oauth integrations\n", count)
 
 	return nil
 }
@@ -550,7 +635,7 @@ func rotateGCPIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 	for i := 0; i < (int(count)/stepSize)+1; i++ {
 		gcps := []*ints.GCPIntegration{}
 
-		if err := db.Offset(i * stepSize).Limit(stepSize).Find(&gcps).Error; err != nil {
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&gcps).Error; err != nil {
 			return err
 		}
 
@@ -559,7 +644,11 @@ func rotateGCPIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.DecryptGCPIntegrationData(gcp, oldKey)
 
 			if err != nil {
-				return err
+				fmt.Printf("error decrypting gcp integration %d\n", gcp.ID)
+
+				// in these cases we'll wipe the data -- if it can't be decrypted, we can't
+				// recover it
+				gcp.GCPKeyData = []byte{}
 			}
 		}
 
@@ -568,6 +657,8 @@ func rotateGCPIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.EncryptGCPIntegrationData(gcp, newKey)
 
 			if err != nil {
+				fmt.Printf("error encrypting gcp integration %d\n", gcp.ID)
+
 				return err
 			}
 
@@ -577,7 +668,7 @@ func rotateGCPIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 		}
 	}
 
-	fmt.Printf("rotated %d gcp integrations", count)
+	fmt.Printf("rotated %d gcp integrations\n", count)
 
 	return nil
 }
@@ -597,7 +688,7 @@ func rotateAWSIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 	for i := 0; i < (int(count)/stepSize)+1; i++ {
 		awss := []*ints.AWSIntegration{}
 
-		if err := db.Offset(i * stepSize).Limit(stepSize).Find(&awss).Error; err != nil {
+		if err := db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&awss).Error; err != nil {
 			return err
 		}
 
@@ -606,7 +697,14 @@ func rotateAWSIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.DecryptAWSIntegrationData(aws, oldKey)
 
 			if err != nil {
-				return err
+				fmt.Printf("error encrypting aws integration %d\n", aws.ID)
+
+				// in these cases we'll wipe the data -- if it can't be decrypted, we can't
+				// recover it
+				aws.AWSAccessKeyID = []byte{}
+				aws.AWSClusterID = []byte{}
+				aws.AWSSecretAccessKey = []byte{}
+				aws.AWSSessionToken = []byte{}
 			}
 		}
 
@@ -615,6 +713,8 @@ func rotateAWSIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 			err := repo.EncryptAWSIntegrationData(aws, newKey)
 
 			if err != nil {
+				fmt.Printf("error decrypting aws integration %d\n", aws.ID)
+
 				return err
 			}
 
@@ -624,7 +724,7 @@ func rotateAWSIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 		}
 	}
 
-	fmt.Printf("rotated %d aws integrations", count)
+	fmt.Printf("rotated %d aws integrations\n", count)
 
 	return nil
 }

+ 2 - 0
cmd/migrate/main.go

@@ -77,6 +77,8 @@ func main() {
 }
 
 type RotateConf struct {
+	// we add a dummy field to avoid empty struct issue with envdecode
+	DummyField       string `env:"ASDF,default=asdf"`
 	OldEncryptionKey string `env:"OLD_ENCRYPTION_KEY"`
 	NewEncryptionKey string `env:"NEW_ENCRYPTION_KEY"`
 }

+ 3 - 2
dashboard/src/components/image-selector/ImageSelector.tsx

@@ -18,6 +18,7 @@ type PropsType = {
   selectedTag: string | null;
   setSelectedImageUrl: (x: string) => void;
   setSelectedTag: (x: string) => void;
+  noTagSelection?: boolean;
 };
 
 type StateType = {
@@ -175,7 +176,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
 
   renderExpanded = () => {
     let { selectedTag, selectedImageUrl, setSelectedTag } = this.props;
-    if (!this.state.clickedImage) {
+    if (!this.state.clickedImage || this.props.noTagSelection) {
       return (
         <div>
           <ExpandedWrapper>{this.renderImageList()}</ExpandedWrapper>
@@ -319,7 +320,7 @@ const ImageItem = styled.div`
   font-size: 13px;
   border-bottom: 1px solid
     ${(props: { lastItem: boolean; isSelected: boolean }) =>
-      props.lastItem ? "#00000000" : "#606166"};
+    props.lastItem ? "#00000000" : "#606166"};
   color: #ffffff;
   user-select: none;
   align-items: center;

+ 1 - 0
dashboard/src/components/repo-selector/ActionDetails.tsx

@@ -70,6 +70,7 @@ export default class ActionDetails extends Component<PropsType, StateType> {
           setSelectedImageUrl={this.setURL}
           setSelectedTag={() => null}
           forceExpanded={true}
+          noTagSelection={true}
         />
       </Holder>
     );

+ 7 - 0
internal/forms/git_action.go

@@ -24,3 +24,10 @@ func (ca *CreateGitAction) ToGitActionConfig() (*models.GitActionConfig, error)
 		GitRepoID:      ca.GitRepoID,
 	}, nil
 }
+
+type CreateGitActionOptional struct {
+	GitRepo        string `json:"git_repo"`
+	ImageRepoURI   string `json:"image_repo_uri"`
+	DockerfilePath string `json:"dockerfile_path"`
+	GitRepoID      uint   `json:"git_repo_id"`
+}

+ 3 - 0
internal/forms/release.go

@@ -129,4 +129,7 @@ type ChartTemplateForm struct {
 type InstallChartTemplateForm struct {
 	*ReleaseForm
 	*ChartTemplateForm
+
+	// optional git action config
+	GithubActionConfig *CreateGitActionOptional `json:"github_action,omitempty"`
 }

+ 2 - 0
internal/models/cluster.go

@@ -96,6 +96,8 @@ func (c *Cluster) Externalize() *ClusterExternal {
 		serv = integrations.EKS
 	} else if c.GCPIntegrationID != 0 {
 		serv = integrations.GKE
+	} else if c.DOIntegrationID != 0 {
+		serv = integrations.DOKS
 	}
 
 	return &ClusterExternal{

+ 1 - 0
internal/models/integrations/integration.go

@@ -6,6 +6,7 @@ type IntegrationService string
 // The list of supported third-party services
 const (
 	GKE      IntegrationService = "gke"
+	DOKS     IntegrationService = "doks"
 	GCS      IntegrationService = "gcs"
 	S3       IntegrationService = "s3"
 	HelmRepo IntegrationService = "helm"

+ 19 - 0
server/api/deploy_handler.go

@@ -138,6 +138,25 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 		}, w)
 	}
 
+	// if github action config is linked, call the github action config handler
+	if form.GithubActionConfig != nil {
+		gaForm := &forms.CreateGitAction{
+			ReleaseID:      release.ID,
+			GitRepo:        form.GithubActionConfig.GitRepo,
+			ImageRepoURI:   form.GithubActionConfig.ImageRepoURI,
+			DockerfilePath: form.GithubActionConfig.DockerfilePath,
+			GitRepoID:      form.GithubActionConfig.GitRepoID,
+		}
+
+		// validate the form
+		if err := app.validator.Struct(form); err != nil {
+			app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+			return
+		}
+
+		app.createGitActionFromForm(projID, release, name, gaForm, w, r)
+	}
+
 	w.WriteHeader(http.StatusOK)
 }
 

+ 28 - 18
server/api/git_action_handler.go

@@ -12,6 +12,7 @@ import (
 	"github.com/porter-dev/porter/internal/auth/token"
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/integrations/ci/actions"
+	"github.com/porter-dev/porter/internal/models"
 )
 
 // HandleCreateGitAction creates a new Github action in a repository for a given
@@ -56,10 +57,28 @@ func (app *App) HandleCreateGitAction(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	gaExt := app.createGitActionFromForm(projID, release, name, form, w, r)
+
+	w.WriteHeader(http.StatusCreated)
+
+	if err := json.NewEncoder(w).Encode(gaExt); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
+func (app *App) createGitActionFromForm(
+	projID uint64,
+	release *models.Release,
+	name string,
+	form *forms.CreateGitAction,
+	w http.ResponseWriter,
+	r *http.Request,
+) *models.GitActionConfigExternal {
 	// validate the form
 	if err := app.validator.Struct(form); err != nil {
 		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
-		return
+		return nil
 	}
 
 	// convert the form to a git action config
@@ -67,7 +86,7 @@ func (app *App) HandleCreateGitAction(w http.ResponseWriter, r *http.Request) {
 
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-		return
+		return nil
 	}
 
 	// read the git repo
@@ -75,21 +94,21 @@ func (app *App) HandleCreateGitAction(w http.ResponseWriter, r *http.Request) {
 
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-		return
+		return nil
 	}
 
 	repoSplit := strings.Split(gitAction.GitRepo, "/")
 
 	if len(repoSplit) != 2 {
 		app.handleErrorFormDecoding(fmt.Errorf("invalid formatting of repo name"), ErrProjectDecode, w)
-		return
+		return nil
 	}
 
 	session, err := app.Store.Get(r, app.ServerConf.CookieName)
 
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
+		return nil
 	}
 
 	userID, _ := session.Values["user_id"].(uint)
@@ -102,9 +121,8 @@ func (app *App) HandleCreateGitAction(w http.ResponseWriter, r *http.Request) {
 	})
 
 	if err != nil {
-		fmt.Println("ERROR GENERATING TOKEN", err)
 		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
+		return nil
 	}
 
 	// create the commit in the git repo
@@ -125,9 +143,8 @@ func (app *App) HandleCreateGitAction(w http.ResponseWriter, r *http.Request) {
 	_, err = gaRunner.Setup()
 
 	if err != nil {
-		fmt.Println("ERROR RUNNING SETUP", err)
 		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
+		return nil
 	}
 
 	// handle write to the database
@@ -135,17 +152,10 @@ func (app *App) HandleCreateGitAction(w http.ResponseWriter, r *http.Request) {
 
 	if err != nil {
 		app.handleErrorDataWrite(err, w)
-		return
+		return nil
 	}
 
 	app.Logger.Info().Msgf("New git action created: %d", ga.ID)
 
-	w.WriteHeader(http.StatusCreated)
-
-	gaExt := ga.Externalize()
-
-	if err := json.NewEncoder(w).Encode(gaExt); err != nil {
-		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-		return
-	}
+	return ga.Externalize()
 }