Kaynağa Gözat

Merge branch 'master' of https://github.com/porter-dev/porter into beta.3.metrics

jusrhee 5 yıl önce
ebeveyn
işleme
19206e8aba
59 değiştirilmiş dosya ile 13270 ekleme ve 1934 silme
  1. 17 15
      README.md
  2. 7 0
      cli/cmd/auth.go
  3. 34 15
      cmd/migrate/keyrotate/helpers_test.go
  4. 564 0
      cmd/migrate/keyrotate/rotate.go
  5. 550 4
      cmd/migrate/keyrotate/rotate_test.go
  6. 0 1
      cmd/migrate/keyrotate/temp.go
  7. 35 0
      cmd/migrate/main.go
  8. 10196 1
      dashboard/package-lock.json
  9. 131 121
      dashboard/src/components/image-selector/ImageList.tsx
  10. 65 72
      dashboard/src/components/image-selector/ImageSelector.tsx
  11. 19 19
      dashboard/src/components/image-selector/TagList.tsx
  12. 25 18
      dashboard/src/components/repo-selector/BranchList.tsx
  13. 62 75
      dashboard/src/components/repo-selector/ButtonTray.tsx
  14. 45 35
      dashboard/src/components/repo-selector/ContentsList.tsx
  15. 47 52
      dashboard/src/components/repo-selector/RepoList.tsx
  16. 15 14
      dashboard/src/main/Login.tsx
  17. 15 12
      dashboard/src/main/Main.tsx
  18. 13 11
      dashboard/src/main/Register.tsx
  19. 91 136
      dashboard/src/main/home/Home.tsx
  20. 16 13
      dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx
  21. 68 68
      dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx
  22. 123 139
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  23. 46 49
      dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx
  24. 79 66
      dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx
  25. 21 21
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ValuesYaml.tsx
  26. 17 16
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx
  27. 26 25
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx
  28. 8 12
      dashboard/src/main/home/dashboard/ClusterList.tsx
  29. 9 13
      dashboard/src/main/home/dashboard/Dashboard.tsx
  30. 99 91
      dashboard/src/main/home/integrations/Integrations.tsx
  31. 24 20
      dashboard/src/main/home/integrations/integration-form/ECRForm.tsx
  32. 76 56
      dashboard/src/main/home/integrations/integration-form/GCRForm.tsx
  33. 17 24
      dashboard/src/main/home/modals/IntegrationsModal.tsx
  34. 46 61
      dashboard/src/main/home/modals/UpdateClusterModal.tsx
  35. 4 3
      dashboard/src/main/home/navbar/Navbar.tsx
  36. 48 66
      dashboard/src/main/home/project-settings/InviteList.tsx
  37. 67 93
      dashboard/src/main/home/provisioner/AWSFormSection.tsx
  38. 25 34
      dashboard/src/main/home/provisioner/DOFormSection.tsx
  39. 23 32
      dashboard/src/main/home/provisioner/ExistingClusterSection.tsx
  40. 74 82
      dashboard/src/main/home/provisioner/GCPFormSection.tsx
  41. 54 53
      dashboard/src/main/home/provisioner/Provisioner.tsx
  42. 34 41
      dashboard/src/main/home/sidebar/ClusterSection.tsx
  43. 5 6
      dashboard/src/main/home/templates/Templates.tsx
  44. 21 23
      dashboard/src/main/home/templates/expanded-template/ExpandedTemplate.tsx
  45. 120 132
      dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx
  46. 18 51
      dashboard/src/shared/baseApi.tsx
  47. 10 3
      internal/auth/sessionstore/sessionstore.go
  48. 4 2
      internal/auth/sessionstore/sessionstore_test.go
  49. 2 2
      internal/config/config.go
  50. 24 15
      internal/integrations/ci/actions/actions.go
  51. 7 0
      internal/integrations/ci/actions/steps.go
  52. 0 4
      internal/repository/gorm/infra.go
  53. 24 0
      server/api/api.go
  54. 3 1
      server/api/git_repo_handler.go
  55. 8 7
      server/api/helpers_test.go
  56. 1 1
      server/api/oauth_github_handler.go
  57. 20 0
      server/api/user_handler.go
  58. 49 0
      server/api/user_handler_test.go
  59. 19 8
      server/router/middleware/auth.go

+ 17 - 15
README.md

@@ -6,29 +6,33 @@
 
 ![Provisioning View](https://user-images.githubusercontent.com/22849518/104234811-fe2dcb00-5421-11eb-9ce3-c0ebefc37476.png)
 
+## Community and Updates
+
+For help, questions, or if you just want a place to hang out, [join our Discord community.](https://discord.gg/MhYNuWwqum)
+
+To keep updated on our progress, please watch the repo for new releases (**Watch > Custom > Releases**) and [follow us on Twitter!](https://twitter.com/getporterdev)
 
 ## Why Porter?
 ### A PaaS that grows with your applications
 
 A traditional PaaS like Heroku is great for minimizing unnecessary DevOps work but doesn't offer enough flexibility as your applications grow. Custom network rules, resource constraints, and cost are common reasons developers move their applications off Heroku beyond a certain scale. 
 
-Porter brings the simplicity of a traditional PaaS to your own cloud provider while preserving the configurability of Kubernetes. Porter is built on top of a popular Kubernetes package manager called Helm and is compatible with standard Kubernetes management tools like `kubectl`, preparing your infra for mature DevOps work from day one.
+Porter brings the simplicity of a traditional PaaS to your own cloud provider while preserving the configurability of Kubernetes. Porter is built on top of a popular Kubernetes package manager `helm` and is compatible with standard Kubernetes management tools like `kubectl`, preparing your infra for mature DevOps work from day one.
 
 ![image](https://user-images.githubusercontent.com/65516095/103713478-71e75800-4f8a-11eb-915f-adee9d4f5bf7.png)
 
 ## Features
 ### Basics
 - One-click provisioning of a Kubernetes cluster in your own cloud console
-  - AWS ✅
-  - GCP ✅
-  - Digital Ocean ✅
-  
+  - ✅   AWS
+  - ✅   GCP
+  - ✅   Digital Ocean
 - Simple deploy of any public or private Docker image
-
 - Heroku-like GUI to monitor application status, logs, and history
 - Marketplace for one click add-ons (e.g. MongoDB, Redis, PostgreSQL)
 - Application rollback to previously deployed versions
-- Native CI/CD with buildpacks (Coming Soon)
+- Deploy webhooks that can be triggered from CI/CD pipelines
+- Native CI/CD with buildpacks for non-Dockerized apps (Coming Soon)
 
 ### DevOps Mode
 For those who are familiar with Kubernetes and Helm:
@@ -41,6 +45,10 @@ For those who are familiar with Kubernetes and Helm:
 
 ![Graph View](https://user-images.githubusercontent.com/22849518/101073320-43322800-356d-11eb-9b69-a68bd951992e.png)
 
+## Docs
+
+Below are instructions for a quickstart. For full documentation, visit our [official Docs page.](https://docs.getporter.dev)
+
 ## CLI Installation
 ### Mac 
 Run the following command to grab the latest binary:
@@ -67,20 +75,14 @@ For Linux and Windows installation, see our [Docs](https://docs.getporter.dev/do
 ## Getting Started
 1. Sign up and log into [Porter Dashboard](https://dashboard.getporter.dev).
 
-2. Create a Project and select a cloud provider you want to provision a Kubernetes cluster in.
+2. Create a Project and select a cloud provider you want to provision a Kubernetes cluster in (AWS, GCP, DO). It is also possible to [link up your own Kubernetes cluster.](https://docs.getporter.dev/docs/cli-documentation#linking-your-own-private-image-registry)
 
 3. [Put in your credentials](https://docs.getporter.dev/docs/getting-started-with-porter-on-aws), then Porter will automatically provision a cluster and an image registry in your own cloud account.
 
-4. [Build and push your Docker image to the provisioned registry with the CLI](https://docs.getporter.dev/docs/cli-documentation#porter-docker-configure).
+4. [Build and push your Docker image](https://docs.getporter.dev/docs/cli-documentation#porter-docker-configure), or connect your git repository if your application is not dockerized.
 
 5. From the Templates tab on the Dashboard, select the Docker template. Click on the image you have just pushed, configure the port, then hit deploy.
 
-## Keep Updated
-We are iterating fast and will be regularly announcing new releases in this repository. If you'd like to follow our progress, please watch the repo for releases (**Watch > Custom > Releases**) and leave us a star!
-
-## Want Help?
-We are always hanging out in our [Discord community](https://discord.gg/MhYNuWwqum). Join us there if you need help or have any questions!
-
 ## Want to Help?
 We welcome all contributions. Submit an issue or a pull request to help us improve Porter!
 ![porter](https://user-images.githubusercontent.com/65516095/103712859-def9ee00-4f88-11eb-804c-4b775d697ec4.jpeg)

+ 7 - 0
cli/cmd/auth.go

@@ -91,6 +91,13 @@ func login() error {
 		}
 
 		client = api.NewClientWithToken(getHost()+"/api", token)
+
+		user, _ := client.AuthCheck(context.Background())
+
+		if user == nil {
+			color.Red("Invalid token.")
+			return nil
+		}
 	} else {
 		client = api.NewClient(getHost()+"/api", "cookie.json")
 	}

+ 34 - 15
cmd/migrate/keyrotate/helpers_test.go

@@ -162,10 +162,15 @@ func initKubeIntegration(tester *tester, t *testing.T) {
 	}
 
 	ki := &ints.KubeIntegration{
-		Mechanism:  ints.KubeLocal,
-		ProjectID:  tester.initProjects[0].ID,
-		UserID:     tester.initUsers[0].ID,
-		Kubeconfig: []byte("current-context: testing\n"),
+		Mechanism:             ints.KubeLocal,
+		ProjectID:             tester.initProjects[0].ID,
+		UserID:                tester.initUsers[0].ID,
+		Kubeconfig:            []byte("current-context: testing\n"),
+		ClientCertificateData: []byte("clientcertdata"),
+		ClientKeyData:         []byte("clientkeydata"),
+		Token:                 []byte("token"),
+		Username:              []byte("username"),
+		Password:              []byte("password"),
 	}
 
 	ki, err := tester.repo.KubeIntegration.CreateKubeIntegration(ki)
@@ -216,14 +221,15 @@ func initOIDCIntegration(tester *tester, t *testing.T) {
 	}
 
 	oidc := &ints.OIDCIntegration{
-		Client:       ints.OIDCKube,
-		ProjectID:    tester.initProjects[0].ID,
-		UserID:       tester.initUsers[0].ID,
-		IssuerURL:    []byte("https://oidc.example.com"),
-		ClientID:     []byte("exampleclientid"),
-		ClientSecret: []byte("exampleclientsecret"),
-		IDToken:      []byte("idtoken"),
-		RefreshToken: []byte("refreshtoken"),
+		Client:                   ints.OIDCKube,
+		ProjectID:                tester.initProjects[0].ID,
+		UserID:                   tester.initUsers[0].ID,
+		IssuerURL:                []byte("https://oidc.example.com"),
+		ClientID:                 []byte("exampleclientid"),
+		ClientSecret:             []byte("exampleclientsecret"),
+		CertificateAuthorityData: []byte("cadata"),
+		IDToken:                  []byte("idtoken"),
+		RefreshToken:             []byte("refreshtoken"),
 	}
 
 	oidc, err := tester.repo.OIDCIntegration.CreateOIDCIntegration(oidc)
@@ -419,6 +425,12 @@ func initRegistry(tester *tester, t *testing.T) {
 	reg := &models.Registry{
 		ProjectID: tester.initProjects[0].ID,
 		Name:      "registry-test",
+		TokenCache: ints.RegTokenCache{
+			TokenCache: ints.TokenCache{
+				Token:  []byte("token-1"),
+				Expiry: time.Now().Add(-1 * time.Hour),
+			},
+		},
 	}
 
 	reg, err := tester.repo.Registry.CreateRegistry(reg)
@@ -441,6 +453,12 @@ func initHelmRepo(tester *tester, t *testing.T) {
 		Name:      "helm-repo-test",
 		RepoURL:   "https://example-repo.com",
 		ProjectID: tester.initProjects[0].Model.ID,
+		TokenCache: ints.HelmRepoTokenCache{
+			TokenCache: ints.TokenCache{
+				Token:  []byte("token-1"),
+				Expiry: time.Now().Add(-1 * time.Hour),
+			},
+		},
 	}
 
 	hr, err := tester.repo.HelmRepo.CreateHelmRepo(hr)
@@ -460,9 +478,10 @@ func initInfra(tester *tester, t *testing.T) {
 	}
 
 	infra := &models.Infra{
-		Kind:      models.InfraECR,
-		ProjectID: tester.initProjects[0].Model.ID,
-		Status:    models.StatusCreated,
+		Kind:        models.InfraECR,
+		ProjectID:   tester.initProjects[0].Model.ID,
+		Status:      models.StatusCreated,
+		LastApplied: []byte("testing"),
 	}
 
 	infra, err := tester.repo.Infra.CreateInfra(infra)

+ 564 - 0
cmd/migrate/keyrotate/rotate.go

@@ -1,7 +1,10 @@
 package keyrotate
 
 import (
+	"fmt"
+
 	"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"
 
 	_gorm "gorm.io/gorm"
@@ -11,9 +14,98 @@ import (
 const stepSize = 100
 
 func Rotate(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	oldKeyBytes := make([]byte, 32)
+	newKeyBytes := make([]byte, 32)
+
+	copy(oldKeyBytes[:], oldKey[:])
+	copy(newKeyBytes[:], newKey[:])
+
+	fmt.Printf("beginning key rotation from %s to %s\n", string(oldKeyBytes), string(newKeyBytes))
+
 	err := rotateClusterModel(db, oldKey, newKey)
 
 	if err != nil {
+		fmt.Printf("failed on cluster rotation: %v\n", err)
+		return err
+	}
+
+	err = rotateClusterCandidateModel(db, oldKey, newKey)
+
+	if err != nil {
+		fmt.Printf("failed on cc rotation: %v\n", err)
+
+		return err
+	}
+
+	err = rotateRegistryModel(db, oldKey, newKey)
+
+	if err != nil {
+		fmt.Printf("failed on registry rotation: %v\n", err)
+
+		return err
+	}
+
+	err = rotateHelmRepoModel(db, oldKey, newKey)
+
+	if err != nil {
+		fmt.Printf("failed on hr rotation: %v\n", err)
+
+		return err
+	}
+
+	err = rotateInfraModel(db, oldKey, newKey)
+
+	if err != nil {
+		fmt.Printf("failed on infra rotation: %v\n", err)
+
+		return err
+	}
+
+	err = rotateKubeIntegrationModel(db, oldKey, newKey)
+
+	if err != nil {
+		fmt.Printf("failed on ki rotation: %v\n", err)
+
+		return err
+	}
+
+	err = rotateBasicIntegrationModel(db, oldKey, newKey)
+
+	if err != nil {
+		fmt.Printf("failed on basic rotation: %v\n", err)
+
+		return err
+	}
+
+	err = rotateOIDCIntegrationModel(db, oldKey, newKey)
+
+	if err != nil {
+		fmt.Printf("failed on oidc rotation: %v\n", err)
+
+		return err
+	}
+
+	err = rotateOAuthIntegrationModel(db, oldKey, newKey)
+
+	if err != nil {
+		fmt.Printf("failed on oauth rotation: %v\n", err)
+
+		return err
+	}
+
+	err = rotateGCPIntegrationModel(db, oldKey, newKey)
+
+	if err != nil {
+		fmt.Printf("failed on gcp rotation: %v\n", err)
+
+		return err
+	}
+
+	err = rotateAWSIntegrationModel(db, oldKey, newKey)
+
+	if err != nil {
+		fmt.Printf("failed on aws rotation: %v\n", err)
+
 		return err
 	}
 
@@ -62,5 +154,477 @@ func rotateClusterModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
 		}
 	}
 
+	fmt.Printf("rotated %d clusters", count)
+
+	return nil
+}
+
+func rotateClusterCandidateModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&models.ClusterCandidate{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// cluster-scoped repository
+	repo := gorm.NewClusterRepository(db, oldKey).(*gorm.ClusterRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	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 {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, cc := range ccs {
+			err := repo.DecryptClusterCandidateData(cc, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, cc := range ccs {
+			err := repo.EncryptClusterCandidateData(cc, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(cc).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d cluster candidates", count)
+
+	return nil
+}
+
+func rotateRegistryModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&models.Registry{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// registry-scoped repository
+	repo := gorm.NewRegistryRepository(db, oldKey).(*gorm.RegistryRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	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 {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, reg := range regs {
+			err := repo.DecryptRegistryData(reg, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, reg := range regs {
+			err := repo.EncryptRegistryData(reg, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(reg).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d registries", count)
+
+	return nil
+}
+
+func rotateHelmRepoModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&models.HelmRepo{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// helm repo-scoped repository
+	repo := gorm.NewHelmRepoRepository(db, oldKey).(*gorm.HelmRepoRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	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 {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, hr := range hrs {
+			err := repo.DecryptHelmRepoData(hr, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, hr := range hrs {
+			err := repo.EncryptHelmRepoData(hr, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(hr).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d helm repos", count)
+
+	return nil
+}
+
+func rotateInfraModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&models.Infra{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// helm repo-scoped repository
+	repo := gorm.NewInfraRepository(db, oldKey).(*gorm.InfraRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	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 {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, infra := range infras {
+			err := repo.DecryptInfraData(infra, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, infra := range infras {
+			err := repo.EncryptInfraData(infra, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(infra).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d infras", count)
+
+	return nil
+}
+
+func rotateKubeIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&ints.KubeIntegration{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// cluster-scoped repository
+	repo := gorm.NewKubeIntegrationRepository(db, oldKey).(*gorm.KubeIntegrationRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	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 {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, ki := range kis {
+			err := repo.DecryptKubeIntegrationData(ki, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, ki := range kis {
+			err := repo.EncryptKubeIntegrationData(ki, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(ki).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d kube integrations", count)
+
+	return nil
+}
+
+func rotateBasicIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&ints.BasicIntegration{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// cluster-scoped repository
+	repo := gorm.NewBasicIntegrationRepository(db, oldKey).(*gorm.BasicIntegrationRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	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 {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, basic := range basics {
+			err := repo.DecryptBasicIntegrationData(basic, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, basic := range basics {
+			err := repo.EncryptBasicIntegrationData(basic, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(basic).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d basic integrations", count)
+
+	return nil
+}
+
+func rotateOIDCIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&ints.OIDCIntegration{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// cluster-scoped repository
+	repo := gorm.NewOIDCIntegrationRepository(db, oldKey).(*gorm.OIDCIntegrationRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	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 {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, oidc := range oidcs {
+			err := repo.DecryptOIDCIntegrationData(oidc, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, oidc := range oidcs {
+			err := repo.EncryptOIDCIntegrationData(oidc, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(oidc).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d oidc integrations", count)
+
+	return nil
+}
+
+func rotateOAuthIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&ints.OAuthIntegration{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// cluster-scoped repository
+	repo := gorm.NewOAuthIntegrationRepository(db, oldKey).(*gorm.OAuthIntegrationRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	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 {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, oauth := range oauths {
+			err := repo.DecryptOAuthIntegrationData(oauth, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, oauth := range oauths {
+			err := repo.EncryptOAuthIntegrationData(oauth, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(oauth).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d oauth integrations", count)
+
+	return nil
+}
+
+func rotateGCPIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&ints.GCPIntegration{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// cluster-scoped repository
+	repo := gorm.NewGCPIntegrationRepository(db, oldKey).(*gorm.GCPIntegrationRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	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 {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, gcp := range gcps {
+			err := repo.DecryptGCPIntegrationData(gcp, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, gcp := range gcps {
+			err := repo.EncryptGCPIntegrationData(gcp, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(gcp).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d gcp integrations", count)
+
+	return nil
+}
+
+func rotateAWSIntegrationModel(db *_gorm.DB, oldKey, newKey *[32]byte) error {
+	// get count of model
+	var count int64
+
+	if err := db.Model(&ints.AWSIntegration{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// cluster-scoped repository
+	repo := gorm.NewAWSIntegrationRepository(db, oldKey).(*gorm.AWSIntegrationRepository)
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	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 {
+			return err
+		}
+
+		// decrypt with the old key
+		for _, aws := range awss {
+			err := repo.DecryptAWSIntegrationData(aws, oldKey)
+
+			if err != nil {
+				return err
+			}
+		}
+
+		// encrypt with the new key and re-insert
+		for _, aws := range awss {
+			err := repo.EncryptAWSIntegrationData(aws, newKey)
+
+			if err != nil {
+				return err
+			}
+
+			if err := db.Save(aws).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+	fmt.Printf("rotated %d aws integrations", count)
+
 	return nil
 }

+ 550 - 4
cmd/migrate/keyrotate/rotate_test.go

@@ -1,11 +1,11 @@
 package keyrotate_test
 
 import (
-	"fmt"
 	"testing"
 
 	"github.com/porter-dev/porter/cmd/migrate/keyrotate"
 	"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,7 +22,7 @@ func TestClusterModelRotation(t *testing.T) {
 
 	setupTestEnv(tester, t)
 
-	for i := 0; i < 1; i++ {
+	for i := 0; i < 128; i++ {
 		initCluster(tester, t)
 	}
 
@@ -45,8 +45,6 @@ func TestClusterModelRotation(t *testing.T) {
 
 	// decrypt with the old key
 	for _, c := range clusters {
-		fmt.Println("GOT TOKEN", string(c.TokenCache.Token))
-
 		cluster, err := repo.ReadCluster(c.ID)
 
 		if err != nil {
@@ -62,3 +60,551 @@ func TestClusterModelRotation(t *testing.T) {
 		}
 	}
 }
+
+func TestClusterCandidateModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_cluster_candidate_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 256; i++ {
+		initClusterCandidate(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all clusters decoded properly
+	repo := gorm.NewClusterRepository(tester.DB, &newKey).(*gorm.ClusterRepository)
+
+	ccs := []*models.ClusterCandidate{}
+
+	if err := tester.DB.Find(&ccs).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, c := range ccs {
+		cc, err := repo.ReadClusterCandidate(c.ID)
+
+		if err != nil {
+			t.Fatalf("error reading cluster: %v\n", err)
+		}
+
+		if string(cc.AWSClusterIDGuess) != "example-cluster-0" {
+			t.Errorf("%s\n", string(cc.AWSClusterIDGuess))
+		}
+
+		if string(cc.Kubeconfig) != "current-context: testing\n" {
+			t.Errorf("%s\n", string(cc.Kubeconfig))
+		}
+	}
+}
+
+func TestRegistryModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_registry_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 144; i++ {
+		initRegistry(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all registries decoded properly
+	repo := gorm.NewRegistryRepository(tester.DB, &newKey).(*gorm.RegistryRepository)
+
+	regs := []*models.Registry{}
+
+	if err := tester.DB.Preload("TokenCache").Find(&regs).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, r := range regs {
+		registry, err := repo.ReadRegistry(r.ID)
+
+		if err != nil {
+			t.Fatalf("error reading registry: %v\n", err)
+		}
+
+		if string(registry.TokenCache.Token) != "token-1" {
+			t.Errorf("%s\n", string(registry.TokenCache.Token))
+		}
+	}
+}
+
+func TestHelmRepoModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_hr_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 169; i++ {
+		initHelmRepo(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all helm repos decoded properly
+	repo := gorm.NewHelmRepoRepository(tester.DB, &newKey).(*gorm.HelmRepoRepository)
+
+	hrs := []*models.HelmRepo{}
+
+	if err := tester.DB.Preload("TokenCache").Find(&hrs).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, h := range hrs {
+		hr, err := repo.ReadHelmRepo(h.ID)
+
+		if err != nil {
+			t.Fatalf("error reading helm repo: %v\n", err)
+		}
+
+		if string(hr.TokenCache.Token) != "token-1" {
+			t.Errorf("%s\n", string(hr.TokenCache.Token))
+		}
+	}
+}
+
+func TestInfraModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_infra_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 128; i++ {
+		initInfra(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all infras decoded properly
+	repo := gorm.NewInfraRepository(tester.DB, &newKey).(*gorm.InfraRepository)
+
+	infras := []*models.Infra{}
+
+	if err := tester.DB.Find(&infras).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, i := range infras {
+		infra, err := repo.ReadInfra(i.ID)
+
+		if err != nil {
+			t.Fatalf("error reading infra: %v\n", err)
+		}
+
+		if string(infra.LastApplied) != "testing" {
+			t.Errorf("%s\n", string(infra.LastApplied))
+		}
+	}
+}
+
+func TestKubeIntegrationModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_ki_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 128; i++ {
+		initKubeIntegration(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all kis decoded properly
+	repo := gorm.NewKubeIntegrationRepository(tester.DB, &newKey).(*gorm.KubeIntegrationRepository)
+
+	kis := []*ints.KubeIntegration{}
+
+	if err := tester.DB.Find(&kis).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, k := range kis {
+		ki, err := repo.ReadKubeIntegration(k.ID)
+
+		if err != nil {
+			t.Fatalf("error reading infra: %v\n", err)
+		}
+
+		if string(ki.Kubeconfig) != "current-context: testing\n" {
+			t.Errorf("%s\n", string(ki.Kubeconfig))
+		}
+
+		if string(ki.ClientCertificateData) != "clientcertdata" {
+			t.Errorf("%s\n", string(ki.ClientCertificateData))
+		}
+
+		if string(ki.ClientKeyData) != "clientkeydata" {
+			t.Errorf("%s\n", string(ki.ClientKeyData))
+		}
+
+		if string(ki.Token) != "token" {
+			t.Errorf("%s\n", string(ki.Token))
+		}
+
+		if string(ki.Username) != "username" {
+			t.Errorf("%s\n", string(ki.Username))
+		}
+
+		if string(ki.Password) != "password" {
+			t.Errorf("%s\n", string(ki.Password))
+		}
+	}
+}
+
+func TestBasicIntegrationModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_basic_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 128; i++ {
+		initBasicIntegration(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all basics decoded properly
+	repo := gorm.NewBasicIntegrationRepository(tester.DB, &newKey).(*gorm.BasicIntegrationRepository)
+
+	basics := []*ints.BasicIntegration{}
+
+	if err := tester.DB.Find(&basics).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, k := range basics {
+		basic, err := repo.ReadBasicIntegration(k.ID)
+
+		if err != nil {
+			t.Fatalf("error reading infra: %v\n", err)
+		}
+
+		if string(basic.Username) != "username" {
+			t.Errorf("%s\n", string(basic.Username))
+		}
+
+		if string(basic.Password) != "password" {
+			t.Errorf("%s\n", string(basic.Password))
+		}
+	}
+}
+
+func TestOIDCIntegrationModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_oidc_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 128; i++ {
+		initOIDCIntegration(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all oidcs decoded properly
+	repo := gorm.NewOIDCIntegrationRepository(tester.DB, &newKey).(*gorm.OIDCIntegrationRepository)
+
+	oidcs := []*ints.OIDCIntegration{}
+
+	if err := tester.DB.Find(&oidcs).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, k := range oidcs {
+		oidc, err := repo.ReadOIDCIntegration(k.ID)
+
+		if err != nil {
+			t.Fatalf("error reading infra: %v\n", err)
+		}
+
+		if string(oidc.IssuerURL) != "https://oidc.example.com" {
+			t.Errorf("%s\n", string(oidc.IssuerURL))
+		}
+
+		if string(oidc.ClientID) != "exampleclientid" {
+			t.Errorf("%s\n", string(oidc.ClientID))
+		}
+
+		if string(oidc.ClientSecret) != "exampleclientsecret" {
+			t.Errorf("%s\n", string(oidc.ClientSecret))
+		}
+
+		if string(oidc.CertificateAuthorityData) != "cadata" {
+			t.Errorf("%s\n", string(oidc.CertificateAuthorityData))
+		}
+
+		if string(oidc.IDToken) != "idtoken" {
+			t.Errorf("%s\n", string(oidc.IDToken))
+		}
+
+		if string(oidc.RefreshToken) != "refreshtoken" {
+			t.Errorf("%s\n", string(oidc.RefreshToken))
+		}
+	}
+}
+
+func TestOAuthIntegrationModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_oauth_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 128; i++ {
+		initOAuthIntegration(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all oauths decoded properly
+	repo := gorm.NewOAuthIntegrationRepository(tester.DB, &newKey).(*gorm.OAuthIntegrationRepository)
+
+	oauths := []*ints.OAuthIntegration{}
+
+	if err := tester.DB.Find(&oauths).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, k := range oauths {
+		oauth, err := repo.ReadOAuthIntegration(k.ID)
+
+		if err != nil {
+			t.Fatalf("error reading infra: %v\n", err)
+		}
+
+		if string(oauth.ClientID) != "exampleclientid" {
+			t.Errorf("%s\n", string(oauth.ClientID))
+		}
+
+		if string(oauth.AccessToken) != "idtoken" {
+			t.Errorf("%s\n", string(oauth.AccessToken))
+		}
+
+		if string(oauth.RefreshToken) != "refreshtoken" {
+			t.Errorf("%s\n", string(oauth.RefreshToken))
+		}
+	}
+}
+
+func TestGCPIntegrationModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_gcp_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 128; i++ {
+		initGCPIntegration(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all gcps decoded properly
+	repo := gorm.NewGCPIntegrationRepository(tester.DB, &newKey).(*gorm.GCPIntegrationRepository)
+
+	gcps := []*ints.GCPIntegration{}
+
+	if err := tester.DB.Find(&gcps).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, k := range gcps {
+		gcp, err := repo.ReadGCPIntegration(k.ID)
+
+		if err != nil {
+			t.Fatalf("error reading infra: %v\n", err)
+		}
+
+		if string(gcp.GCPKeyData) != "{\"test\":\"key\"}" {
+			t.Errorf("%s\n", string(gcp.GCPKeyData))
+		}
+	}
+}
+
+func TestAWSIntegrationModelRotation(t *testing.T) {
+	var newKey [32]byte
+
+	for i, b := range []byte("__r3n3o3_s3r3n3_3n3r3p3i3n_k3y__") {
+		newKey[i] = b
+	}
+
+	tester := &tester{
+		dbFileName: "./porter_aws_rotate.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	for i := 0; i < 128; i++ {
+		initAWSIntegration(tester, t)
+	}
+
+	defer cleanup(tester, t)
+
+	err := keyrotate.Rotate(tester.DB, tester.Key, &newKey)
+
+	if err != nil {
+		t.Fatalf("error rotating: %v\n", err)
+	}
+
+	// very all awss decoded properly
+	repo := gorm.NewAWSIntegrationRepository(tester.DB, &newKey).(*gorm.AWSIntegrationRepository)
+
+	awss := []*ints.AWSIntegration{}
+
+	if err := tester.DB.Find(&awss).Error; err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// decrypt with the old key
+	for _, k := range awss {
+		aws, err := repo.ReadAWSIntegration(k.ID)
+
+		if err != nil {
+			t.Fatalf("error reading infra: %v\n", err)
+		}
+
+		if string(aws.AWSClusterID) != "example-cluster-0" {
+			t.Errorf("%s\n", string(aws.AWSClusterID))
+		}
+
+		if string(aws.AWSAccessKeyID) != "accesskey" {
+			t.Errorf("%s\n", string(aws.AWSAccessKeyID))
+		}
+
+		if string(aws.AWSSecretAccessKey) != "secret" {
+			t.Errorf("%s\n", string(aws.AWSSecretAccessKey))
+		}
+
+		if string(aws.AWSSessionToken) != "optional" {
+			t.Errorf("%s\n", string(aws.AWSSessionToken))
+		}
+	}
+}

+ 0 - 1
cmd/migrate/keyrotate/temp.go

@@ -1 +0,0 @@
-package keyrotate

+ 35 - 0
cmd/migrate/main.go

@@ -2,6 +2,9 @@ package main
 
 import (
 	"fmt"
+	"log"
+
+	"github.com/porter-dev/porter/cmd/migrate/keyrotate"
 
 	adapter "github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/config"
@@ -9,6 +12,8 @@ import (
 	"github.com/porter-dev/porter/internal/models"
 
 	ints "github.com/porter-dev/porter/internal/models/integrations"
+
+	"github.com/joeshaw/envdecode"
 )
 
 func main() {
@@ -54,4 +59,34 @@ func main() {
 	if err != nil {
 		panic(err)
 	}
+
+	if shouldRotate, oldKeyStr, newKeyStr := shouldKeyRotate(); shouldRotate {
+		oldKey := [32]byte{}
+		newKey := [32]byte{}
+
+		copy(oldKey[:], []byte(oldKeyStr))
+		copy(newKey[:], []byte(newKeyStr))
+
+		err := keyrotate.Rotate(db, &oldKey, &newKey)
+
+		if err != nil {
+			panic(err)
+		}
+	}
+}
+
+type RotateConf struct {
+	OldEncryptionKey string `env:"OLD_ENCRYPTION_KEY"`
+	NewEncryptionKey string `env:"NEW_ENCRYPTION_KEY"`
+}
+
+func shouldKeyRotate() (bool, string, string) {
+	var c RotateConf
+
+	if err := envdecode.StrictDecode(&c); err != nil {
+		log.Fatalf("Failed to decode migration conf: %s", err)
+		return false, "", ""
+	}
+
+	return c.OldEncryptionKey != "" && c.NewEncryptionKey != "", c.OldEncryptionKey, c.NewEncryptionKey
 }

Dosya farkı çok büyük olduğundan ihmal edildi
+ 10196 - 1
dashboard/package-lock.json


+ 131 - 121
dashboard/src/components/image-selector/ImageList.tsx

@@ -1,28 +1,28 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
-import api from '../../shared/api';
-import { integrationList } from '../../shared/common';
-import { Context } from '../../shared/Context';
-import { ImageType } from '../../shared/types';
+import api from "shared/api";
+import { integrationList } from "shared/common";
+import { Context } from "shared/Context";
+import { ImageType } from "shared/types";
 
-import Loading from '../Loading';
-import TagList from './TagList';
+import Loading from "../Loading";
+import TagList from "./TagList";
 
 type PropsType = {
-  selectedImageUrl: string | null,
-  selectedTag: string | null,
-  clickedImage: ImageType | null,
-  registry?: any,
-  setSelectedImageUrl: (x: string) => void,
-  setSelectedTag: (x: string) => void,
-  setClickedImage: (x: ImageType) => void,
+  selectedImageUrl: string | null;
+  selectedTag: string | null;
+  clickedImage: ImageType | null;
+  registry?: any;
+  setSelectedImageUrl: (x: string) => void;
+  setSelectedTag: (x: string) => void;
+  setClickedImage: (x: ImageType) => void;
 };
 
 type StateType = {
-  loading: boolean,
-  error: boolean,
-  images: ImageType[],
+  loading: boolean;
+  error: boolean;
+  images: ImageType[];
 };
 
 export default class ImageSelector extends Component<PropsType, StateType> {
@@ -30,116 +30,127 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     loading: true,
     error: false,
     images: [] as ImageType[],
-  }
+  };
 
   componentDidMount() {
     const { currentProject, setCurrentError } = this.context;
-    let images = [] as ImageType[]
-    let errors = [] as number[]
+    let images = [] as ImageType[];
+    let errors = [] as number[];
     if (!this.props.registry) {
-      api.getProjectRegistries('<token>', {}, { id: currentProject.id }, async (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          this.setState({ loading: false, error: true });
-        } else {
+      api
+        .getProjectRegistries("<token>", {}, { id: currentProject.id })
+        .then((res) => {
           let registries = res.data;
           if (registries.length === 0) {
             this.setState({ loading: false });
           }
           // Loop over connected image registries
+          // TODO: promise.map the whole thing
           registries.forEach(async (registry: any, i: number) => {
-            await new Promise((nextController: (res?: any) => void) => {           
-              api.getImageRepos('<token>', {}, 
-                { 
-                  project_id: currentProject.id,
-                  registry_id: registry.id,
-                }, (err: any, res: any) => {
-                if (err) {
-                  errors.push(1);
-                } else {
-                  res.data.sort((a: any, b: any) => (a.name > b.name) ? 1 : -1);
-                  // Loop over found image repositories
-                  let newImg = res.data.map((img: any) => {
-                    if (this.props.selectedImageUrl === img.uri) {
-                      this.props.setClickedImage(
-                        {
+            await new Promise(
+              (resolveToNextController: (res?: any) => void) => {
+                api
+                  .getImageRepos(
+                    "<token>",
+                    {},
+                    {
+                      project_id: currentProject.id,
+                      registry_id: registry.id,
+                    }
+                  )
+                  .then((res) => {
+                    res.data.sort((a: any, b: any) =>
+                      a.name > b.name ? 1 : -1
+                    );
+                    // Loop over found image repositories
+                    let newImg = res.data.map((img: any) => {
+                      if (this.props.selectedImageUrl === img.uri) {
+                        this.props.setClickedImage({
                           kind: registry.service,
                           source: img.uri,
                           name: img.name,
                           registryId: registry.id,
-                        }
-                      );
-                    }
-                    return {
-                      kind: registry.service, 
-                      source: img.uri,
-                      name: img.name,
-                      registryId: registry.id,
-                    }
+                        });
+                      }
+                      return {
+                        kind: registry.service,
+                        source: img.uri,
+                        name: img.name,
+                        registryId: registry.id,
+                      };
+                    });
+                    images.push(...newImg);
+                    errors.push(0);
                   })
-                  images.push(...newImg)
-                  errors.push(0);
-                }
-                
-                if (i == registries.length - 1) {
-                  let error = errors.reduce((a, b) => {
-                    return a + b;
-                  }) == registries.length ? true : false; 
-  
-                  this.setState({
-                    images,
-                    loading: false,
-                    error,
+                  .catch((err) => errors.push(1))
+                  .finally(() => {
+                    if (i == registries.length - 1) {
+                      let error =
+                        errors.reduce((a, b) => {
+                          return a + b;
+                        }) == registries.length
+                          ? true
+                          : false;
+
+                      this.setState({
+                        images,
+                        loading: false,
+                        error,
+                      });
+                    }
+                    resolveToNextController();
                   });
-                }
-  
-                nextController()
-              });
-            })
+              }
+            );
           });
-        }
-      });
+        })
+        .catch((err) => {
+          console.log(err);
+          this.setState({ loading: false, error: true });
+        });
     } else {
-      api.getImageRepos('<token>', {}, 
-      { 
-        project_id: currentProject.id,
-        registry_id: this.props.registry.id,
-      }, (err: any, res: any) => {
-        if (err) {
-          this.setState({
-            loading: false,
-            error: true,
-          });
-        } else {
-          res.data.sort((a: any, b: any) => (a.name > b.name) ? 1 : -1);
+      api
+        .getImageRepos(
+          "<token>",
+          {},
+          {
+            project_id: currentProject.id,
+            registry_id: this.props.registry.id,
+          }
+        )
+        .then((res) => {
+          res.data.sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
           // Loop over found image repositories
           let newImg = res.data.map((img: any) => {
             if (this.props.selectedImageUrl === img.uri) {
-              this.props.setClickedImage(
-                {
-                  kind: this.props.registry.service,
-                  source: img.uri,
-                  name: img.name,
-                  registryId: this.props.registry.id,
-                }
-              );
+              this.props.setClickedImage({
+                kind: this.props.registry.service,
+                source: img.uri,
+                name: img.name,
+                registryId: this.props.registry.id,
+              });
             }
             return {
-              kind: this.props.registry.service, 
+              kind: this.props.registry.service,
               source: img.uri,
               name: img.name,
               registryId: this.props.registry.id,
-            }
-          })
-          images.push(...newImg)
+            };
+          });
+          images.push(...newImg);
 
           this.setState({
             images,
             loading: false,
             error: false,
           });
-        }
-      });
+        })
+        .catch((err) =>
+          this.setState({
+            loading: false,
+            error: true,
+          })
+        );
     }
   }
 
@@ -151,46 +162,48 @@ export default class ImageSelector extends Component<PropsType, StateType> {
   renderImageList = () => {
     let { images, loading, error } = this.state;
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
-    } else if (error || !images) {
-      return <LoadingWrapper>Error loading repos</LoadingWrapper>
-    } else if (images.length === 0) {
       return (
         <LoadingWrapper>
-          No registries found. 
+          <Loading />
         </LoadingWrapper>
       );
+    } else if (error || !images) {
+      return <LoadingWrapper>Error loading repos</LoadingWrapper>;
+    } else if (images.length === 0) {
+      return <LoadingWrapper>No registries found.</LoadingWrapper>;
     }
 
     return images.map((image: ImageType, i: number) => {
-      let icon = integrationList[image.kind] && integrationList[image.kind].icon;
+      let icon =
+        integrationList[image.kind] && integrationList[image.kind].icon;
       if (!icon) {
-        icon = integrationList['docker'].icon;
+        icon = integrationList["docker"].icon;
       }
       return (
         <ImageItem
           key={i}
           isSelected={image.source === this.props.selectedImageUrl}
           lastItem={i === images.length - 1}
-          onClick={() => { 
+          onClick={() => {
             this.props.setSelectedImageUrl(image.source);
             this.props.setClickedImage(image);
           }}
         >
-          <img src={icon && icon} />{image.source}
+          <img src={icon && icon} />
+          {image.source}
         </ImageItem>
       );
     });
-  }
+  };
 
   renderBackButton = () => {
     let { setSelectedImageUrl } = this.props;
     if (this.props.clickedImage) {
       return (
         <BackButton
-          width='175px'
+          width="175px"
           onClick={() => {
-            setSelectedImageUrl('');
+            setSelectedImageUrl("");
             this.props.setClickedImage(null);
           }}
         >
@@ -199,16 +212,14 @@ export default class ImageSelector extends Component<PropsType, StateType> {
         </BackButton>
       );
     }
-  }
+  };
 
   renderExpanded = () => {
     let { selectedTag, selectedImageUrl, setSelectedTag } = this.props;
     if (!this.props.clickedImage) {
       return (
         <div>
-          <ExpandedWrapper>
-            {this.renderImageList()}
-          </ExpandedWrapper>
+          <ExpandedWrapper>{this.renderImageList()}</ExpandedWrapper>
           {this.renderBackButton()}
         </div>
       );
@@ -227,14 +238,10 @@ export default class ImageSelector extends Component<PropsType, StateType> {
         </div>
       );
     }
-  }
+  };
 
   render() {
-    return (
-      <>
-        {this.renderExpanded()}
-      </>
-    );
+    return <>{this.renderExpanded()}</>;
   }
 }
 
@@ -269,13 +276,16 @@ const ImageItem = styled.div`
   display: flex;
   width: 100%;
   font-size: 13px;
-  border-bottom: 1px solid ${(props: { lastItem: boolean, isSelected: boolean }) => props.lastItem ? '#00000000' : '#606166'};
+  border-bottom: 1px solid
+    ${(props: { lastItem: boolean; isSelected: boolean }) =>
+      props.lastItem ? "#00000000" : "#606166"};
   color: #ffffff;
   user-select: none;
   align-items: center;
   padding: 10px 0px;
   cursor: pointer;
-  background: ${(props: { isSelected: boolean, lastItem: boolean }) => props.isSelected ? '#ffffff11' : ''};
+  background: ${(props: { isSelected: boolean; lastItem: boolean }) =>
+    props.isSelected ? "#ffffff11" : ""};
   :hover {
     background: #ffffff22;
 
@@ -310,4 +320,4 @@ const ExpandedWrapper = styled.div`
   max-height: 275px;
   background: #ffffff11;
   overflow-y: auto;
-`;
+`;

+ 65 - 72
dashboard/src/components/image-selector/ImageSelector.tsx

@@ -8,9 +8,9 @@ import { integrationList } from "shared/common";
 import { Context } from "shared/Context";
 import { ImageType } from "shared/types";
 
-import Loading from '../Loading';
-import TagList from './TagList';
-import ImageList from './ImageList';
+import Loading from "../Loading";
+import TagList from "./TagList";
+import ImageList from "./ImageList";
 
 type PropsType = {
   forceExpanded?: boolean;
@@ -41,83 +41,76 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     const { currentProject, setCurrentError } = this.context;
     let images = [] as ImageType[];
     let errors = [] as number[];
-    api.getProjectRegistries(
-      "<token>",
-      {},
-      { id: currentProject.id },
-      async (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          this.setState({ error: true });
-        } else {
-          let registries = res.data;
-          if (registries.length === 0) {
-            this.setState({ loading: false });
-          }
-
-          // Loop over connected image registries
-          registries.forEach(async (registry: any, i: number) => {
-            await new Promise((nextController: (res?: any) => void) => {
-              api.getImageRepos(
+    api
+      .getProjectRegistries("<token>", {}, { id: currentProject.id })
+      .then(async (res) => {
+        let registries = res.data;
+        if (registries.length === 0) {
+          this.setState({ loading: false });
+        }
+
+        // Loop over connected image registries
+        registries.forEach(async (registry: any, i: number) => {
+          await new Promise((nextController: (res?: any) => void) => {
+            api
+              .getImageRepos(
                 "<token>",
                 {},
                 {
                   project_id: currentProject.id,
                   registry_id: registry.id,
-                },
-                (err: any, res: any) => {
-                  if (err) {
-                    errors.push(1);
-                  } else {
-                    res.data.sort((a: any, b: any) =>
-                      a.name > b.name ? 1 : -1
-                    );
-                    // Loop over found image repositories
-                    let newImg = res.data.map((img: any) => {
-                      if (this.props.selectedImageUrl === img.uri) {
-                        this.setState({
-                          clickedImage: {
-                            kind: registry.service,
-                            source: img.uri,
-                            name: img.name,
-                            registryId: registry.id,
-                          },
-                        });
-                      }
-                      return {
+                }
+              )
+              .then((res) => {
+                res.data.sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
+                // Loop over found image repositories
+                let newImg = res.data.map((img: any) => {
+                  if (this.props.selectedImageUrl === img.uri) {
+                    this.setState({
+                      clickedImage: {
                         kind: registry.service,
                         source: img.uri,
                         name: img.name,
                         registryId: registry.id,
-                      };
+                      },
                     });
-                    images.push(...newImg);
-                    errors.push(0);
                   }
-
-                  if (i == registries.length - 1) {
-                    let error =
-                      errors.reduce((a, b) => {
-                        return a + b;
-                      }) == registries.length
-                        ? true
-                        : false;
-
-                    this.setState({
-                      images,
-                      loading: false,
-                      error,
-                    });
-                  }
-
-                  nextController();
+                  return {
+                    kind: registry.service,
+                    source: img.uri,
+                    name: img.name,
+                    registryId: registry.id,
+                  };
+                });
+                images.push(...newImg);
+                errors.push(0);
+              })
+              .catch(() => errors.push(1))
+              .finally(() => {
+                if (i == registries.length - 1) {
+                  let error =
+                    errors.reduce((a, b) => {
+                      return a + b;
+                    }) == registries.length
+                      ? true
+                      : false;
+
+                  this.setState({
+                    images,
+                    loading: false,
+                    error,
+                  });
                 }
-              );
-            });
+
+                nextController();
+              });
           });
-        }
-      }
-    );
+        });
+      })
+      .catch((err) => {
+        console.log(err);
+        this.setState({ error: true });
+      });
   }
 
   /*
@@ -259,18 +252,18 @@ export default class ImageSelector extends Component<PropsType, StateType> {
           )}
         </StyledImageSelector>
 
-        {this.state.isExpanded
-          ?
+        {this.state.isExpanded ? (
           <ImageList
             selectedImageUrl={this.props.selectedImageUrl}
             selectedTag={this.props.selectedTag}
             clickedImage={this.state.clickedImage}
             setSelectedImageUrl={this.props.setSelectedImageUrl}
             setSelectedTag={this.props.setSelectedTag}
-            setClickedImage={(x: ImageType) => this.setState({ clickedImage: x })}
+            setClickedImage={(x: ImageType) =>
+              this.setState({ clickedImage: x })
+            }
           />
-          : null
-        }
+        ) : null}
       </div>
     );
   }

+ 19 - 19
dashboard/src/components/image-selector/TagList.tsx

@@ -34,26 +34,26 @@ export default class TagList extends Component<PropsType, StateType> {
     const { currentProject } = this.context;
     let splits = this.props.selectedImageUrl.split("/");
     let repoName = splits[splits.length - 1];
-    api.getImageTags(
-      "<token>",
-      {},
-      {
-        project_id: currentProject.id,
-        registry_id: this.props.registryId,
-        repo_name: repoName,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          this.setState({ loading: false, error: true });
-        } else {
-          let tags = res.data.map((tag: any, i: number) => {
-            return tag.tag;
-          });
-          this.setState({ tags, loading: false });
+    api
+      .getImageTags(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          registry_id: this.props.registryId,
+          repo_name: repoName,
         }
-      }
-    );
+      )
+      .then((res) => {
+        let tags = res.data.map((tag: any, i: number) => {
+          return tag.tag;
+        });
+        this.setState({ tags, loading: false });
+      })
+      .catch((err) => {
+        console.log(err);
+        this.setState({ loading: false, error: true });
+      });
   }
 
   setTag = (tag: string) => {

+ 25 - 18
dashboard/src/components/repo-selector/BranchList.tsx

@@ -2,15 +2,15 @@ import React, { Component } from "react";
 import styled from "styled-components";
 import branch_icon from "assets/branch.png";
 
-import api from '../../shared/api';
-import { Context } from '../../shared/Context';
-import { ActionConfigType } from '../..//shared/types';
+import api from "../../shared/api";
+import { Context } from "../../shared/Context";
+import { ActionConfigType } from "../..//shared/types";
 
 import Loading from "../Loading";
 
 type PropsType = {
-  actionConfig: ActionConfigType,
-  setBranch: (x: string) => void,
+  actionConfig: ActionConfigType;
+  setBranch: (x: string) => void;
 };
 
 type StateType = {
@@ -31,20 +31,25 @@ export default class BranchList extends Component<PropsType, StateType> {
     let { currentProject } = this.context;
 
     // Get branches
-    api.getBranches('<token>', {}, {
-      project_id: currentProject.id,
-      git_repo_id: actionConfig.git_repo_id,
-      kind: 'github',
-      owner: actionConfig.git_repo.split('/')[0],
-      name: actionConfig.git_repo.split('/')[1],
-    }, (err: any, res: any) => {
-      if (err) {
+    api
+      .getBranches(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          git_repo_id: actionConfig.git_repo_id,
+          kind: "github",
+          owner: actionConfig.git_repo.split("/")[0],
+          name: actionConfig.git_repo.split("/")[1],
+        }
+      )
+      .then((res) =>
+        this.setState({ branches: res.data, loading: false, error: false })
+      )
+      .catch((err) => {
         console.log(err);
         this.setState({ loading: false, error: true });
-      } else {
-        this.setState({ branches: res.data, loading: false, error: false });
-      }
-    });
+      });
   }
 
   renderBranchList = () => {
@@ -84,7 +89,9 @@ const BranchName = styled.div`
   display: flex;
   width: 100%;
   font-size: 13px;
-  border-bottom: 1px solid ${(props: { lastItem: boolean }) => props.lastItem ? '#00000000' : '#606166'};
+  border-bottom: 1px solid
+    ${(props: { lastItem: boolean }) =>
+      props.lastItem ? "#00000000" : "#606166"};
   color: #ffffff;
   user-select: none;
   align-items: center;

+ 62 - 75
dashboard/src/components/repo-selector/ButtonTray.tsx

@@ -1,88 +1,82 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
-import api from '../../shared/api';
-import { ActionConfigType } from '../../shared/types';
-import { Context } from '../../shared/Context';
+import api from "../../shared/api";
+import { ActionConfigType } from "../../shared/types";
+import { Context } from "../../shared/Context";
 
 type PropsType = {
-  chartName: string | null,
-  chartNamespace: string | null,
-  pathIsSet: boolean,
-  branch: string,
-  actionConfig: ActionConfigType | null,
-  setBranch: (x: string) => void,
-  setActionConfig: (x: ActionConfigType) => void,
-  setPath: (x: boolean) => void,
+  chartName: string | null;
+  chartNamespace: string | null;
+  pathIsSet: boolean;
+  branch: string;
+  actionConfig: ActionConfigType | null;
+  setBranch: (x: string) => void;
+  setActionConfig: (x: ActionConfigType) => void;
+  setPath: (x: boolean) => void;
 };
 
-type StateType = {
-};
+type StateType = {};
 
 export default class RepoSelector extends Component<PropsType, StateType> {
   createGHAction = () => {
     let { currentProject, currentCluster } = this.context;
     let { actionConfig, chartName, chartNamespace } = this.props;
 
-    api.createGHAction('<token>', {
-      git_repo: actionConfig.git_repo,
-      image_repo_uri: actionConfig.image_repo_uri,
-      dockerfile_path: actionConfig.dockerfile_path,
-      git_repo_id: actionConfig.git_repo_id,
-    }, {
-      project_id: currentProject.id,
-      CLUSTER_ID: currentCluster.id,
-      RELEASE_NAME: chartName,
-      RELEASE_NAMESPACE: chartNamespace,
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else {
-        // Exit to initial settings tab
-        console.log(res.data);
-      }
-    });
-  }
+    api
+      .createGHAction(
+        "<token>",
+        {
+          git_repo: actionConfig.git_repo,
+          image_repo_uri: actionConfig.image_repo_uri,
+          dockerfile_path: actionConfig.dockerfile_path,
+          git_repo_id: actionConfig.git_repo_id,
+        },
+        {
+          project_id: currentProject.id,
+          CLUSTER_ID: currentCluster.id,
+          RELEASE_NAME: chartName,
+          RELEASE_NAMESPACE: chartNamespace,
+        }
+      )
+      .then((res) => console.log(res.data))
+      .catch(console.log);
+  };
 
   setSelectedRepo = () => {
     let { actionConfig, setActionConfig } = this.props;
     let updatedConfig = actionConfig;
-    updatedConfig.git_repo = '';
+    updatedConfig.git_repo = "";
     updatedConfig.git_repo_id = null as number;
     setActionConfig(updatedConfig);
-  }
+  };
 
   goToBranchSelect = () => {
     let { actionConfig, setActionConfig, setBranch } = this.props;
     let updatedConfig = actionConfig;
-    updatedConfig.dockerfile_path = '';
-    setBranch('');
+    updatedConfig.dockerfile_path = "";
+    setBranch("");
     setActionConfig(updatedConfig);
-  }
+  };
 
   goToPathSelect = () => {
     let { actionConfig, setActionConfig, setPath } = this.props;
     let updatedConfig = actionConfig;
-    updatedConfig.image_repo_uri = '';
+    updatedConfig.image_repo_uri = "";
     updatedConfig.dockerfile_path = updatedConfig.dockerfile_path.slice(0, -11);
     setPath(false);
     setActionConfig(updatedConfig);
-  }
+  };
 
   renderExpanded = () => {
     let { actionConfig, pathIsSet, branch } = this.props;
 
     if (!actionConfig.git_repo) {
-      return (
-        <></>
-      );
+      return <></>;
     } else if (!branch) {
       return (
         <ButtonTray>
-          <BackButton
-            width='130px'
-            onClick={() => this.setSelectedRepo()}
-          >
+          <BackButton width="130px" onClick={() => this.setSelectedRepo()}>
             <i className="material-icons">keyboard_backspace</i>
             Select Repo
           </BackButton>
@@ -91,47 +85,37 @@ export default class RepoSelector extends Component<PropsType, StateType> {
     } else if (!pathIsSet) {
       return (
         <ButtonTray>
-          <BackButton
-            onClick={() => this.goToBranchSelect()}
-            width='140px'
-          >
+          <BackButton onClick={() => this.goToBranchSelect()} width="140px">
             <i className="material-icons">keyboard_backspace</i>
             Select Branch
           </BackButton>
-        </ButtonTray>  
-      )
+        </ButtonTray>
+      );
     }
     return (
       <ButtonTray>
-        <BackButton
-          width='130px'
-          onClick={() => this.goToPathSelect()}
-        >
-          <i className='material-icons'>keyboard_backspace</i>
+        <BackButton width="130px" onClick={() => this.goToPathSelect()}>
+          <i className="material-icons">keyboard_backspace</i>
           Select Dockerfile
         </BackButton>
         <BackButton
           disabled={
-            (!actionConfig.git_repo) ||
-            (!actionConfig.dockerfile_path) ||
-            (!actionConfig.image_repo_uri)
+            !actionConfig.git_repo ||
+            !actionConfig.dockerfile_path ||
+            !actionConfig.image_repo_uri
           }
-          width='146px'
+          width="146px"
           onClick={() => this.createGHAction()}
         >
-          <i className='material-icons'>local_shipping</i>
+          <i className="material-icons">local_shipping</i>
           Create Github Action
         </BackButton>
       </ButtonTray>
     );
-  }
+  };
 
   render() {
-    return (
-      <>
-        {this.renderExpanded()}
-      </>
-    );
+    return <>{this.renderExpanded()}</>;
   }
 }
 
@@ -147,16 +131,19 @@ const BackButton = styled.div`
   padding: 5px 10px;
   border: 1px solid #ffffff55;
   border-radius: 3px;
-  width: ${(props: { width: string, disabled?: boolean }) => props.width};
-  color: ${(props: { width: string, disabled?: boolean }) => props.disabled ? '#ffffff55' : 'white'};
-  pointer-events: ${(props: { width: string, disabled?: boolean }) => props.disabled ? 'none' : 'auto'};
+  width: ${(props: { width: string; disabled?: boolean }) => props.width};
+  color: ${(props: { width: string; disabled?: boolean }) =>
+    props.disabled ? "#ffffff55" : "white"};
+  pointer-events: ${(props: { width: string; disabled?: boolean }) =>
+    props.disabled ? "none" : "auto"};
 
   :hover {
     background: #ffffff11;
   }
 
   > i {
-    color: ${(props: { width: string, disabled?: boolean }) => props.disabled ? '#ffffff55' : 'white'};
+    color: ${(props: { width: string; disabled?: boolean }) =>
+      props.disabled ? "#ffffff55" : "white"};
     font-size: 18px;
     margin-right: 10px;
   }
@@ -168,4 +155,4 @@ const ButtonTray = styled.div`
   flex-direction: row;
   justify-content: space-between;
   align-items: center;
-`;
+`;

+ 45 - 35
dashboard/src/components/repo-selector/ContentsList.tsx

@@ -4,17 +4,17 @@ import file from "assets/file.svg";
 import folder from "assets/folder.svg";
 import info from "assets/info.svg";
 
-import api from '../../shared/api';
-import { Context } from '../../shared/Context';
-import { FileType, ActionConfigType } from '../../shared/types';
+import api from "../../shared/api";
+import { Context } from "../../shared/Context";
+import { FileType, ActionConfigType } from "../../shared/types";
 
 import Loading from "../Loading";
 
 type PropsType = {
-  actionConfig: ActionConfigType | null,
-  branch: string,
-  setActionConfig: (x: ActionConfigType) => void,
-  setPath: () => void,
+  actionConfig: ActionConfigType | null;
+  branch: string;
+  setActionConfig: (x: ActionConfigType) => void;
+  setPath: () => void;
 };
 
 type StateType = {
@@ -36,38 +36,48 @@ export default class ContentsList extends Component<PropsType, StateType> {
     updatedConfig.dockerfile_path = x;
     setActionConfig(updatedConfig);
     this.updateContents();
-  }
+  };
 
   updateContents = () => {
     let { actionConfig, branch } = this.props;
     let { currentProject } = this.context;
 
     // Get branch contents
-    api.getBranchContents('<token>', { dir: actionConfig.dockerfile_path }, {
-      project_id: currentProject.id,
-      git_repo_id: actionConfig.git_repo_id,
-      kind: 'github',
-      owner: actionConfig.git_repo.split('/')[0],
-      name: actionConfig.git_repo.split('/')[1],
-      branch: branch,
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        this.setState({ loading: false, error: true });
-      } else {
+    api
+      .getBranchContents(
+        "<token>",
+        { dir: actionConfig.dockerfile_path },
+        {
+          project_id: currentProject.id,
+          git_repo_id: actionConfig.git_repo_id,
+          kind: "github",
+          owner: actionConfig.git_repo.split("/")[0],
+          name: actionConfig.git_repo.split("/")[1],
+          branch: branch,
+        }
+      )
+      .then((res) => {
         let files = [] as FileType[];
         let folders = [] as FileType[];
         res.data.map((x: FileType, i: number) => {
-          x.Type === 'dir' ? folders.push(x) : files.push(x);
+          x.Type === "dir" ? folders.push(x) : files.push(x);
         });
 
-        folders.sort((a: FileType, b: FileType) => { return a.Path < b.Path ? 1 : 0 });
-        files.sort((a: FileType, b: FileType) => { return a.Path < b.Path ? 1 : 0 });
+        folders.sort((a: FileType, b: FileType) => {
+          return a.Path < b.Path ? 1 : 0;
+        });
+        files.sort((a: FileType, b: FileType) => {
+          return a.Path < b.Path ? 1 : 0;
+        });
         let contents = folders.concat(files);
-        
+
         this.setState({ contents, loading: false, error: false });
-      }
-    });
+      })
+      .catch((err) => {
+        console.log(err);
+
+        this.setState({ loading: false, error: true });
+      });
   };
 
   componentDidMount() {
@@ -127,21 +137,21 @@ export default class ContentsList extends Component<PropsType, StateType> {
 
   renderJumpToParent = () => {
     let { actionConfig } = this.props;
-    if (actionConfig.dockerfile_path !== '') {
-      let splits = actionConfig.dockerfile_path.split('/');
-      let subdir = '';
+    if (actionConfig.dockerfile_path !== "") {
+      let splits = actionConfig.dockerfile_path.split("/");
+      let subdir = "";
       if (splits.length !== 1) {
-        subdir = actionConfig.dockerfile_path.replace(splits[splits.length - 1], '');
-        if (subdir.charAt(subdir.length - 1) === '/') {
+        subdir = actionConfig.dockerfile_path.replace(
+          splits[splits.length - 1],
+          ""
+        );
+        if (subdir.charAt(subdir.length - 1) === "/") {
           subdir = subdir.slice(0, subdir.length - 1);
         }
       }
 
       return (
-        <Item
-          lastItem={false}
-          onClick={() => this.setSubdirectory(subdir)}
-        >
+        <Item lastItem={false} onClick={() => this.setSubdirectory(subdir)}>
           <BackLabel>..</BackLabel>
         </Item>
       );

+ 47 - 52
dashboard/src/components/repo-selector/RepoList.tsx

@@ -33,63 +33,58 @@ export default class ActionConfEditor extends Component<PropsType, StateType> {
 
     // Get repos
     if (!this.props.userId && this.props.userId !== 0) {
-      api.getGitRepos(
-        "<token>",
-        {},
-        { project_id: currentProject.id },
-        (err: any, res: any) => {
-          if (err) {
-            this.setState({ loading: false, error: true });
-          } else {
-            var allRepos: any = [];
-            for (let i = 0; i < res.data.length; i++) {
-              var grid = res.data[i].id;
-              api.getGitRepoList(
+      api
+        .getGitRepos("<token>", {}, { project_id: currentProject.id })
+        .then((res) => {
+          var allRepos: any = [];
+          // TODO: make into promise.all
+          for (let i = 0; i < res.data.length; i++) {
+            var grid = res.data[i].id;
+            api
+              .getGitRepoList(
                 "<token>",
                 {},
-                { project_id: currentProject.id, git_repo_id: grid },
-                (err: any, res: any) => {
-                  if (err) {
-                    console.log(err);
-                    this.setState({ loading: false, error: true });
-                  } else {
-                    res.data.forEach((repo: any, id: number) => {
-                      repo.GHRepoID = grid;
-                    });
-                    allRepos = allRepos.concat(res.data);
-                    this.setState({
-                      repos: allRepos,
-                      loading: false,
-                      error: false,
-                    });
-                  }
-                }
-              );
-            }
-            if (res.data.length < 1) {
-              this.setState({ loading: false, error: false });
-            }
+                { project_id: currentProject.id, git_repo_id: grid }
+              )
+              .then((res) => {
+                res.data.forEach((repo: any, id: number) => {
+                  repo.GHRepoID = grid;
+                });
+                allRepos = allRepos.concat(res.data);
+                this.setState({
+                  repos: allRepos,
+                  loading: false,
+                  error: false,
+                });
+              })
+              .catch((err) => {
+                console.log(err);
+                this.setState({ loading: false, error: true });
+              });
           }
-        }
-      );
+          if (res.data.length < 1) {
+            this.setState({ loading: false, error: false });
+          }
+        })
+        .catch((err) => this.setState({ loading: false, error: true }));
     } else {
       let grid = this.props.userId;
-      api.getGitRepoList(
-        "<token>",
-        {},
-        { project_id: currentProject.id, git_repo_id: grid },
-        (err: any, res: any) => {
-          if (err) {
-            console.log(err);
-            this.setState({ loading: false, error: true });
-          } else {
-            res.data.forEach((repo: any, id: number) => {
-              repo.GHRepoID = grid;
-            });
-            this.setState({ repos: res.data, loading: false, error: false });
-          }
-        }
-      );
+      api
+        .getGitRepoList(
+          "<token>",
+          {},
+          { project_id: currentProject.id, git_repo_id: grid }
+        )
+        .then((res) => {
+          res.data.forEach((repo: any, id: number) => {
+            repo.GHRepoID = grid;
+          });
+          this.setState({ repos: res.data, loading: false, error: false });
+        })
+        .catch((err) => {
+          console.log(err);
+          this.setState({ loading: false, error: true });
+        });
     }
   }
 

+ 15 - 14
dashboard/src/main/Login.tsx

@@ -52,26 +52,27 @@ export default class Login extends Component<PropsType, StateType> {
       this.setState({ emailError: true });
     } else {
       // Attempt user login
-      api.logInUser(
-        "",
-        {
-          email: email,
-          password: password,
-        },
-        {},
-        (err: any, res: any) => {
+      api
+        .logInUser(
+          "",
+          {
+            email: email,
+            password: password,
+          },
+          {}
+        )
+        .then((res) => {
           // TODO: case and set credential error
-          if (err) {
-            this.context.setCurrentError(err.response.data.errors[0]);
-          }
           if (res?.data?.redirect) {
             window.location.href = res.data.redirect;
           } else {
             setUser(res?.data?.id, res?.data?.email);
-            err ? console.log(err.response.data) : authenticate();
+            authenticate();
           }
-        }
-      );
+        })
+        .catch((err) =>
+          this.context.setCurrentError(err.response.data.errors[0])
+        );
     }
   };
 

+ 15 - 12
dashboard/src/main/Main.tsx

@@ -32,18 +32,21 @@ export default class Main extends Component<PropsType, StateType> {
     let urlParams = new URLSearchParams(window.location.search);
     let error = urlParams.get("error");
     error && setCurrentError(error);
-    api.checkAuth("", {}, {}, (err: any, res: any) => {
-      if (err && err.response?.status == 403) {
-        this.setState({ isLoggedIn: false, loading: false });
-      }
-
-      if (res && res.data) {
-        setUser(res?.data?.id, res?.data?.email);
-        this.setState({ isLoggedIn: true, initialized: true, loading: false });
-      } else {
-        this.setState({ isLoggedIn: false, loading: false });
-      }
-    });
+    api
+      .checkAuth("", {}, {})
+      .then((res) => {
+        if (res && res.data) {
+          setUser(res?.data?.id, res?.data?.email);
+          this.setState({
+            isLoggedIn: true,
+            initialized: true,
+            loading: false,
+          });
+        } else {
+          this.setState({ isLoggedIn: false, loading: false });
+        }
+      })
+      .catch((err) => this.setState({ isLoggedIn: false, loading: false }));
   }
 
   initialize = () => {

+ 13 - 11
dashboard/src/main/Register.tsx

@@ -61,22 +61,24 @@ export default class Register extends Component<PropsType, StateType> {
     // Check for valid input
     if (emailRegex.test(email) && confirmPassword === password) {
       // Attempt user registration
-      api.registerUser(
-        "",
-        {
-          email: email,
-          password: password,
-        },
-        {},
-        (err: any, res: any) => {
+      api
+        .registerUser(
+          "",
+          {
+            email: email,
+            password: password,
+          },
+          {}
+        )
+        .then((res: any) => {
           if (res?.data?.redirect) {
             window.location.href = res.data.redirect;
           } else {
             setUser(res?.data?.id, res?.data?.email);
-            err ? setCurrentError(err.response.data.errors[0]) : authenticate();
+            authenticate();
           }
-        }
-      );
+        })
+        .catch((err) => setCurrentError(err.response.data.errors[0]));
     }
   };
 

+ 91 - 136
dashboard/src/main/home/Home.tsx

@@ -61,14 +61,15 @@ class Home extends Component<PropsType, StateType> {
 
     if (!currentProject) return;
 
-    api.getInfra(
-      "<token>",
-      {},
-      {
-        project_id: currentProject.id,
-      },
-      (err: any, res: any) => {
-        if (err) return;
+    api
+      .getInfra(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+        }
+      )
+      .then((res) => {
         let creating = false;
 
         for (var i = 0; i < res.data.length; i++) {
@@ -80,21 +81,16 @@ class Home extends Component<PropsType, StateType> {
           this.props.history.push("integrations");
           this.setState({ ghRedirect: false });
         }
-      }
-    );
+      });
   };
 
   getProjects = (id?: number) => {
     let { user, setProjects } = this.context;
     let { currentProject } = this.props;
-    api.getProjects(
-      "<token>",
-      {},
-      { id: user.userId },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else if (res.data) {
+    api
+      .getProjects("<token>", {}, { id: user.userId })
+      .then((res) => {
+        if (res.data) {
           if (res.data.length === 0) {
             this.props.history.push("new-project");
           } else if (res.data.length > 0 && !currentProject) {
@@ -125,13 +121,13 @@ class Home extends Component<PropsType, StateType> {
             }
           }
         }
-      }
-    );
+      })
+      .catch(console.log);
   };
 
   provisionDOCR = (integrationId: number, tier: string, callback?: any) => {
     console.log("Provisioning DOCR...");
-    api.createDOCR(
+    return api.createDOCR(
       "<token>",
       {
         do_integration_id: integrationId,
@@ -140,53 +136,39 @@ class Home extends Component<PropsType, StateType> {
       },
       {
         project_id: this.props.currentProject.id,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          return;
-        }
-        callback && callback();
       }
     );
   };
 
   provisionDOKS = (integrationId: number, region: string) => {
     console.log("Provisioning DOKS...");
-    api.createDOKS(
-      "<token>",
-      {
-        do_integration_id: integrationId,
-        doks_name: this.props.currentProject.name,
-        do_region: region,
-      },
-      {
-        project_id: this.props.currentProject.id,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          return;
+    return api
+      .createDOKS(
+        "<token>",
+        {
+          do_integration_id: integrationId,
+          doks_name: this.props.currentProject.name,
+          do_region: region,
+        },
+        {
+          project_id: this.props.currentProject.id,
         }
-        this.props.history.push("dashboard?tab=provisioner");
-      }
-    );
+      )
+      .then(() => this.props.history.push("dashboard?tab=provisioner"));
   };
 
   checkDO = () => {
     let { currentProject } = this.props;
     if (this.state.handleDO && currentProject?.id) {
-      api.getOAuthIds(
-        "<token>",
-        {},
-        {
-          project_id: currentProject.id,
-        },
-        (err: any, res: any) => {
-          if (err) {
-            console.log(err);
-            return;
+      api
+        .getOAuthIds(
+          "<token>",
+          {},
+          {
+            project_id: currentProject.id,
           }
+        )
+        .then((res) => {
           let tgtIntegration = res.data.find((integration: any) => {
             return integration.client === "do";
           });
@@ -206,8 +188,8 @@ class Home extends Component<PropsType, StateType> {
           } else {
             this.provisionDOKS(tgtIntegration.id, region);
           }
-        }
-      );
+        })
+        .catch(console.log);
       this.setState({ handleDO: false });
     }
   };
@@ -358,14 +340,10 @@ class Home extends Component<PropsType, StateType> {
 
   projectOverlayCall = () => {
     let { user, setProjects } = this.context;
-    api.getProjects(
-      "<token>",
-      {},
-      { id: user.userId },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else if (res.data) {
+    api
+      .getProjects("<token>", {}, { id: user.userId })
+      .then((res) => {
+        if (res.data) {
           setProjects(res.data);
           if (res.data.length > 0) {
             this.context.setCurrentProject(res.data[0]);
@@ -375,37 +353,23 @@ class Home extends Component<PropsType, StateType> {
           }
           this.context.setCurrentModal(null, null);
         }
-      }
-    );
+      })
+      .catch(console.log);
   };
 
   handleDelete = () => {
     let { setCurrentModal, currentProject } = this.context;
     localStorage.removeItem(currentProject.id + "-cluster");
-    api.deleteProject(
-      "<token>",
-      {},
-      { id: currentProject.id },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else {
-          this.projectOverlayCall();
-        }
-      }
-    );
+    api
+      .deleteProject("<token>", {}, { id: currentProject.id })
+      .then(this.projectOverlayCall)
+      .catch(console.log);
 
     // Loop through and delete infra of all clusters we've provisioned
-    api.getClusters(
-      "<token>",
-      {},
-      { id: currentProject.id },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          return;
-        }
-
+    api
+      .getClusters("<token>", {}, { id: currentProject.id })
+      .then((res) => {
+        // TODO: promise.map
         for (var i = 0; i < res.data.length; i++) {
           let cluster = res.data[i];
           if (!cluster.infra_id) continue;
@@ -413,65 +377,56 @@ class Home extends Component<PropsType, StateType> {
           // Handle destroying infra we've provisioned
           switch (cluster.service) {
             case "eks":
-              api.destroyEKS(
-                "<token>",
-                { eks_name: cluster.name },
-                {
-                  project_id: currentProject.id,
-                  infra_id: cluster.infra_id,
-                },
-                (err: any, res: any) => {
-                  if (err) {
-                    console.log(err);
-                  } else {
-                    console.log(
-                      "destroyed provisioned infra:",
-                      cluster.infra_id
-                    );
+              api
+                .destroyEKS(
+                  "<token>",
+                  { eks_name: cluster.name },
+                  {
+                    project_id: currentProject.id,
+                    infra_id: cluster.infra_id,
                   }
-                }
-              );
+                )
+                .then(() =>
+                  console.log("destroyed provisioned infra:", cluster.infra_id)
+                )
+                .catch(console.log);
               break;
 
             case "gke":
-              api.destroyGKE(
-                "<token>",
-                { gke_name: cluster.name },
-                {
-                  project_id: currentProject.id,
-                  infra_id: cluster.infra_id,
-                },
-                (err: any, res: any) => {
-                  if (err) {
-                    console.log(err);
-                  } else {
-                    console.log("destroyed provisioned infra.");
+              api
+                .destroyGKE(
+                  "<token>",
+                  { gke_name: cluster.name },
+                  {
+                    project_id: currentProject.id,
+                    infra_id: cluster.infra_id,
                   }
-                }
-              );
+                )
+                .then(() =>
+                  console.log("destroyed provisioned infra:", cluster.infra_id)
+                )
+                .catch(console.log);
               break;
 
             case "doks":
-              api.destroyDOKS(
-                "<token>",
-                { doks_name: cluster.name },
-                {
-                  project_id: currentProject.id,
-                  infra_id: cluster.infra_id,
-                },
-                (err: any, res: any) => {
-                  if (err) {
-                    console.log(err);
-                  } else {
-                    console.log("destroyed provisioned infra.");
+              api
+                .destroyDOKS(
+                  "<token>",
+                  { doks_name: cluster.name },
+                  {
+                    project_id: currentProject.id,
+                    infra_id: cluster.infra_id,
                   }
-                }
-              );
+                )
+                .then(() =>
+                  console.log("destroyed provisioned infra:", cluster.infra_id)
+                )
+                .catch(console.log);
               break;
           }
         }
-      }
-    );
+      })
+      .catch(console.log);
     setCurrentModal(null, null);
     this.props.history.push("dashboard?tab=overview");
   };

+ 16 - 13
dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx

@@ -26,17 +26,16 @@ export default class NamespaceSelector extends Component<PropsType, StateType> {
   updateOptions = () => {
     let { currentCluster, currentProject } = this.context;
 
-    api.getNamespaces(
-      "<token>",
-      {
-        cluster_id: currentCluster.id,
-      },
-      { id: currentProject.id },
-      (err: any, res: any) => {
-        if (err && this._isMounted) {
-          // setCurrentError('Could not read clusters: ' + JSON.stringify(err));
-          this.setState({ namespaceOptions: [{ label: "All", value: "" }] });
-        } else if (this._isMounted) {
+    api
+      .getNamespaces(
+        "<token>",
+        {
+          cluster_id: currentCluster.id,
+        },
+        { id: currentProject.id }
+      )
+      .then((res) => {
+        if (this._isMounted) {
           let namespaceOptions: { label: string; value: string }[] = [
             { label: "All", value: "" },
           ];
@@ -50,8 +49,12 @@ export default class NamespaceSelector extends Component<PropsType, StateType> {
           );
           this.setState({ namespaceOptions });
         }
-      }
-    );
+      })
+      .catch((err) => {
+        if (this._isMounted) {
+          this.setState({ namespaceOptions: [{ label: "All", value: "" }] });
+        }
+      });
   };
 
   componentDidMount() {

+ 68 - 68
dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx

@@ -34,61 +34,60 @@ export default class ChartList extends Component<PropsType, StateType> {
     websockets: {} as Record<string, any>,
   };
 
+  // TODO: promisify
   updateCharts = (callback: Function) => {
     let { currentCluster, currentProject, setCurrentError } = this.context;
     this.setState({ loading: true });
 
-    api.getCharts(
-      "<token>",
-      {
-        namespace: this.props.namespace,
-        cluster_id: currentCluster.id,
-        storage: StorageType.Secret,
-        limit: 20,
-        skip: 0,
-        byDate: false,
-        statusFilter: [
-          "deployed",
-          "uninstalled",
-          "pending",
-          "pending_upgrade",
-          "pending_rollback",
-          "superseded",
-          "failed",
-        ],
-      },
-      { id: currentProject.id },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          setCurrentError(JSON.stringify(err));
-          this.setState({ loading: false, error: true });
-        } else {
-          let charts = res.data || [];
-          if (this.props.sortType == "Newest") {
-            charts.sort((a: any, b: any) =>
-              Date.parse(a.info.last_deployed) >
-              Date.parse(b.info.last_deployed)
-                ? -1
-                : 1
-            );
-          } else if (this.props.sortType == "Oldest") {
-            charts.sort((a: any, b: any) =>
-              Date.parse(a.info.last_deployed) >
-              Date.parse(b.info.last_deployed)
-                ? 1
-                : -1
-            );
-          } else if (this.props.sortType == "Alphabetical") {
-            charts.sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
-          }
-          this.setState({ charts }, () => {
-            this.setState({ loading: false, error: false });
-          });
-          callback(charts);
+    api
+      .getCharts(
+        "<token>",
+        {
+          namespace: this.props.namespace,
+          cluster_id: currentCluster.id,
+          storage: StorageType.Secret,
+          limit: 20,
+          skip: 0,
+          byDate: false,
+          statusFilter: [
+            "deployed",
+            "uninstalled",
+            "pending",
+            "pending_upgrade",
+            "pending_rollback",
+            "superseded",
+            "failed",
+          ],
+        },
+        { id: currentProject.id }
+      )
+      .then((res) => {
+        let charts = res.data || [];
+        if (this.props.sortType == "Newest") {
+          charts.sort((a: any, b: any) =>
+            Date.parse(a.info.last_deployed) > Date.parse(b.info.last_deployed)
+              ? -1
+              : 1
+          );
+        } else if (this.props.sortType == "Oldest") {
+          charts.sort((a: any, b: any) =>
+            Date.parse(a.info.last_deployed) > Date.parse(b.info.last_deployed)
+              ? 1
+              : -1
+          );
+        } else if (this.props.sortType == "Alphabetical") {
+          charts.sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
         }
-      }
-    );
+        this.setState({ charts }, () => {
+          this.setState({ loading: false, error: false });
+        });
+        callback(charts);
+      })
+      .catch((err) => {
+        console.log(err);
+        setCurrentError(JSON.stringify(err));
+        this.setState({ loading: false, error: true });
+      });
   };
 
   setupWebsocket = (kind: string) => {
@@ -150,23 +149,21 @@ export default class ChartList extends Component<PropsType, StateType> {
       if (chart.info.status == "failed") return;
 
       await new Promise((next: (res?: any) => void) => {
-        api.getChartControllers(
-          "<token>",
-          {
-            namespace: chart.namespace,
-            cluster_id: currentCluster.id,
-            storage: StorageType.Secret,
-          },
-          {
-            id: currentProject.id,
-            name: chart.name,
-            revision: chart.version,
-          },
-          (err: any, res: any) => {
-            if (err) {
-              setCurrentError(JSON.stringify(err));
-              return;
+        api
+          .getChartControllers(
+            "<token>",
+            {
+              namespace: chart.namespace,
+              cluster_id: currentCluster.id,
+              storage: StorageType.Secret,
+            },
+            {
+              id: currentProject.id,
+              name: chart.name,
+              revision: chart.version,
             }
+          )
+          .then((res) => {
             // transform controller array into hash table for easy lookup during updates.
             let chartControllers = {} as Record<string, Record<string, any>>;
             res.data.forEach((c: any) => {
@@ -194,8 +191,11 @@ export default class ChartList extends Component<PropsType, StateType> {
               });
             });
             next();
-          }
-        );
+          })
+          .catch((err) => {
+            setCurrentError(JSON.stringify(err));
+            return;
+          });
       });
     });
   };

+ 123 - 139
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -80,27 +80,25 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     let { currentCluster, currentChart, setCurrentChart } = this.props;
 
     this.setState({ loading: true });
-    api.getChart(
-      "<token>",
-      {
-        namespace: currentChart.namespace,
-        cluster_id: currentCluster.id,
-        storage: StorageType.Secret,
-      },
-      {
-        name: chart.name,
-        revision: chart.version,
-        id: currentProject.id,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else {
-          setCurrentChart(res.data);
-          this.setState({ loading: false });
+    api
+      .getChart(
+        "<token>",
+        {
+          namespace: currentChart.namespace,
+          cluster_id: currentCluster.id,
+          storage: StorageType.Secret,
+        },
+        {
+          name: chart.name,
+          revision: chart.version,
+          id: currentProject.id,
         }
-      }
-    );
+      )
+      .then((res) => {
+        setCurrentChart(res.data);
+        this.setState({ loading: false });
+      })
+      .catch(console.log);
   };
 
   getControllers = async (chart: ChartType) => {
@@ -108,26 +106,23 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
 
     // don't retrieve controllers for chart that failed to even deploy.
     if (chart.info.status == "failed") return;
-
+    // TODO: properly promisify
     await new Promise((next: (res?: any) => void) => {
-      api.getChartControllers(
-        "<token>",
-        {
-          namespace: chart.namespace,
-          cluster_id: currentCluster.id,
-          storage: StorageType.Secret,
-        },
-        {
-          id: currentProject.id,
-          name: chart.name,
-          revision: chart.version,
-        },
-        (err: any, res: any) => {
-          if (err) {
-            setCurrentError(JSON.stringify(err));
-            return;
+      api
+        .getChartControllers(
+          "<token>",
+          {
+            namespace: chart.namespace,
+            cluster_id: currentCluster.id,
+            storage: StorageType.Secret,
+          },
+          {
+            id: currentProject.id,
+            name: chart.name,
+            revision: chart.version,
           }
-
+        )
+        .then((res) => {
           res.data.forEach(async (c: any) => {
             await new Promise((nextController: (res?: any) => void) => {
               c.metadata.kind = c.kind;
@@ -145,8 +140,8 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
             });
           });
           next();
-        }
-      );
+        })
+        .catch((err) => setCurrentError(JSON.stringify(err)));
     });
   };
 
@@ -198,29 +193,27 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     let { currentCluster, currentProject } = this.context;
     let { currentChart } = this.props;
 
-    api.getChartComponents(
-      "<token>",
-      {
-        namespace: currentChart.namespace,
-        cluster_id: currentCluster.id,
-        storage: StorageType.Secret,
-      },
-      {
-        id: currentProject.id,
-        name: currentChart.name,
-        revision: currentChart.version,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else {
-          this.setState({
-            components: res.data.Objects,
-            podSelectors: res.data.PodSelectors,
-          });
+    api
+      .getChartComponents(
+        "<token>",
+        {
+          namespace: currentChart.namespace,
+          cluster_id: currentCluster.id,
+          storage: StorageType.Secret,
+        },
+        {
+          id: currentProject.id,
+          name: currentChart.name,
+          revision: currentChart.version,
         }
-      }
-    );
+      )
+      .then((res) => {
+        this.setState({
+          components: res.data.Objects,
+          podSelectors: res.data.PodSelectors,
+        });
+      })
+      .catch(console.log);
   };
 
   refreshChart = () => this.getChartData(this.props.currentChart);
@@ -242,30 +235,30 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
 
     this.setState({ saveValuesStatus: "loading" });
     this.refreshChart();
-    api.upgradeChartValues(
-      "<token>",
-      {
-        namespace: this.props.currentChart.namespace,
-        storage: StorageType.Secret,
-        values: valuesYaml,
-      },
-      {
-        id: currentProject.id,
-        name: this.props.currentChart.name,
-        cluster_id: currentCluster.id,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          this.setState({ saveValuesStatus: "error" });
-          console.log(err);
-        } else {
-          this.setState({
-            saveValuesStatus: "successful",
-            forceRefreshRevisions: true,
-          });
+    api
+      .upgradeChartValues(
+        "<token>",
+        {
+          namespace: this.props.currentChart.namespace,
+          storage: StorageType.Secret,
+          values: valuesYaml,
+        },
+        {
+          id: currentProject.id,
+          name: this.props.currentChart.name,
+          cluster_id: currentCluster.id,
         }
-      }
-    );
+      )
+      .then((res) => {
+        this.setState({
+          saveValuesStatus: "successful",
+          forceRefreshRevisions: true,
+        });
+      })
+      .catch((err) => {
+        this.setState({ saveValuesStatus: "error" });
+        console.log(err);
+      });
   };
 
   renderTabContents = () => {
@@ -489,43 +482,36 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
       this.props.currentChart
     );
 
-    api.getChartComponents(
-      "<token>",
-      {
-        namespace: currentChart.namespace,
-        cluster_id: currentCluster.id,
-        storage: StorageType.Secret,
-      },
-      {
-        id: currentProject.id,
-        name: currentChart.name,
-        revision: currentChart.version,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else {
-          this.setState({ components: res.data.Objects });
+    api
+      .getChartComponents(
+        "<token>",
+        {
+          namespace: currentChart.namespace,
+          cluster_id: currentCluster.id,
+          storage: StorageType.Secret,
+        },
+        {
+          id: currentProject.id,
+          name: currentChart.name,
+          revision: currentChart.version,
         }
-      }
-    );
+      )
+      .then((res) => this.setState({ components: res.data.Objects }))
+      .catch(console.log);
 
-    api.getIngress(
-      "<token>",
-      {
-        cluster_id: currentCluster.id,
-      },
-      {
-        id: currentProject.id,
-        name: `${this.props.currentChart.name}-docker`,
-        namespace: `${this.props.currentChart.namespace}`,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          return;
+    api
+      .getIngress(
+        "<token>",
+        {
+          cluster_id: currentCluster.id,
+        },
+        {
+          id: currentProject.id,
+          name: `${this.props.currentChart.name}-docker`,
+          namespace: `${this.props.currentChart.namespace}`,
         }
-
+      )
+      .then((res) => {
         if (res.data?.spec?.rules && res.data?.spec?.rules[0]?.host) {
           this.setState({ url: `https://${res.data?.spec?.rules[0]?.host}` });
           return;
@@ -537,8 +523,8 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
           });
           return;
         }
-      }
-    );
+      })
+      .catch(console.log);
 
     this.updateTabs();
   }
@@ -590,25 +576,23 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     let { currentProject, currentCluster } = this.context;
     let { currentChart } = this.props;
     this.setState({ deleting: true });
-    api.uninstallTemplate(
-      "<token>",
-      {},
-      {
-        namespace: currentChart.namespace,
-        storage: StorageType.Secret,
-        name: currentChart.name,
-        id: currentProject.id,
-        cluster_id: currentCluster.id,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else {
-          this.setState({ showDeleteOverlay: false });
-          this.props.setCurrentChart(null);
+    api
+      .uninstallTemplate(
+        "<token>",
+        {},
+        {
+          namespace: currentChart.namespace,
+          storage: StorageType.Secret,
+          name: currentChart.name,
+          id: currentProject.id,
+          cluster_id: currentCluster.id,
         }
-      }
-    );
+      )
+      .then((res) => {
+        this.setState({ showDeleteOverlay: false });
+        this.props.setCurrentChart(null);
+      })
+      .catch(console.log);
   };
 
   renderDeleteOverlay = () => {

+ 46 - 49
dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx

@@ -35,32 +35,29 @@ export default class RevisionSection extends Component<PropsType, StateType> {
     maxVersion: 0, // Track most recent version even when previewing old revisions
   };
 
-  refreshHistory = (callback?: () => void) => {
+  refreshHistory = () => {
     let { chart } = this.props;
     let { currentCluster, currentProject } = this.context;
-    api.getRevisions(
-      "<token>",
-      {
-        namespace: chart.namespace,
-        cluster_id: currentCluster.id,
-        storage: StorageType.Secret,
-      },
-      { id: currentProject.id, name: chart.name },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else {
-          res.data.sort((a: ChartType, b: ChartType) => {
-            return -(a.version - b.version);
-          });
-          this.setState({
-            revisions: res.data,
-            maxVersion: res.data[0].version,
-          });
-          callback && callback();
-        }
-      }
-    );
+    return api
+      .getRevisions(
+        "<token>",
+        {
+          namespace: chart.namespace,
+          cluster_id: currentCluster.id,
+          storage: StorageType.Secret,
+        },
+        { id: currentProject.id, name: chart.name }
+      )
+      .then((res) => {
+        res.data.sort((a: ChartType, b: ChartType) => {
+          return -(a.version - b.version);
+        });
+        this.setState({
+          revisions: res.data,
+          maxVersion: res.data[0].version,
+        });
+      })
+      .catch(console.log);
   };
 
   componentDidMount() {
@@ -73,7 +70,7 @@ export default class RevisionSection extends Component<PropsType, StateType> {
       this.props.refreshRevisionsOff();
 
       // Force refresh occurs on submit -> set current to newest
-      this.refreshHistory(() => {
+      this.refreshHistory().then(() => {
         this.props.setRevision(this.state.revisions[0], true);
       });
     } else if (this.props.chart !== prevProps.chart) {
@@ -97,31 +94,31 @@ export default class RevisionSection extends Component<PropsType, StateType> {
     let revisionNumber = this.state.rollbackRevision;
     this.setState({ loading: true, rollbackRevision: null });
 
-    api.rollbackChart(
-      "<token>",
-      {
-        namespace: this.props.chart.namespace,
-        storage: StorageType.Secret,
-        revision: revisionNumber,
-      },
-      {
-        id: currentProject.id,
-        name: this.props.chart.name,
-        cluster_id: currentCluster.id,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          setCurrentError(err.response.data);
-          this.setState({ loading: false });
-        } else {
-          this.setState({ loading: false });
-          this.refreshHistory(() => {
-            this.props.setRevision(this.state.revisions[0], true);
-          });
+    api
+      .rollbackChart(
+        "<token>",
+        {
+          namespace: this.props.chart.namespace,
+          storage: StorageType.Secret,
+          revision: revisionNumber,
+        },
+        {
+          id: currentProject.id,
+          name: this.props.chart.name,
+          cluster_id: currentCluster.id,
         }
-      }
-    );
+      )
+      .then((res) => {
+        this.setState({ loading: false });
+        this.refreshHistory().then(() => {
+          this.props.setRevision(this.state.revisions[0], true);
+        });
+      })
+      .catch((err) => {
+        console.log(err);
+        setCurrentError(err.response.data);
+        this.setState({ loading: false });
+      });
   };
 
   handleClickRevision = (revision: ChartType) => {

+ 79 - 66
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -25,31 +25,31 @@ type PropsType = {
 };
 
 type StateType = {
-  actionConfig: ActionConfigType,
-  sourceType: string,
-  selectedImageUrl: string | null,
-  selectedTag: string | null,
-  saveValuesStatus: string | null,
-  values: string,
-  webhookToken: string,
-  highlightCopyButton: boolean,
+  actionConfig: ActionConfigType;
+  sourceType: string;
+  selectedImageUrl: string | null;
+  selectedTag: string | null;
+  saveValuesStatus: string | null;
+  values: string;
+  webhookToken: string;
+  highlightCopyButton: boolean;
   action: ActionConfigType;
 };
 
 export default class SettingsSection extends Component<PropsType, StateType> {
   state = {
     actionConfig: {
-      git_repo: '',
-      image_repo_uri: '',
+      git_repo: "",
+      image_repo_uri: "",
       git_repo_id: 0,
-      dockerfile_path: '',
+      dockerfile_path: "",
     } as ActionConfigType,
-    sourceType: '',
-    selectedImageUrl: '',
-    selectedTag: '',
-    values: '',
-    saveValuesStatus: null as (string | null),
-    webhookToken: '',
+    sourceType: "",
+    selectedImageUrl: "",
+    selectedTag: "",
+    values: "",
+    saveValuesStatus: null as string | null,
+    webhookToken: "",
     highlightCopyButton: false,
     action: {
       git_repo: "",
@@ -69,18 +69,24 @@ export default class SettingsSection extends Component<PropsType, StateType> {
       selectedTag: image?.tag,
     });
 
-    api.getReleaseToken('<token>', {
-      namespace: this.props.currentChart.namespace,
-      cluster_id: currentCluster.id,
-      storage: StorageType.Secret
-    }, { id: currentProject.id, name: this.props.currentChart.name }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else {
+    api
+      .getReleaseToken(
+        "<token>",
+        {
+          namespace: this.props.currentChart.namespace,
+          cluster_id: currentCluster.id,
+          storage: StorageType.Secret,
+        },
+        { id: currentProject.id, name: this.props.currentChart.name }
+      )
+      .then((res) => {
         console.log(res.data);
-        this.setState({ action: res.data.git_action_config, webhookToken: res.data.webhook_token });
-      }
-    });
+        this.setState({
+          action: res.data.git_action_config,
+          webhookToken: res.data.webhook_token,
+        });
+      })
+      .catch(console.log);
   }
 
   redeployWithNewImage = (img: string, tag: string) => {
@@ -103,28 +109,28 @@ export default class SettingsSection extends Component<PropsType, StateType> {
     };
 
     let values = yaml.dump(image);
-    api.upgradeChartValues(
-      "<token>",
-      {
-        namespace: this.props.currentChart.namespace,
-        storage: StorageType.Secret,
-        values,
-      },
-      {
-        id: currentProject.id,
-        name: this.props.currentChart.name,
-        cluster_id: currentCluster.id,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          this.setState({ saveValuesStatus: "error" });
-        } else {
-          this.setState({ saveValuesStatus: "successful" });
-          this.props.refreshChart();
+    api
+      .upgradeChartValues(
+        "<token>",
+        {
+          namespace: this.props.currentChart.namespace,
+          storage: StorageType.Secret,
+          values,
+        },
+        {
+          id: currentProject.id,
+          name: this.props.currentChart.name,
+          cluster_id: currentCluster.id,
         }
-      }
-    );
+      )
+      .then((res) => {
+        this.setState({ saveValuesStatus: "successful" });
+        this.props.refreshChart();
+      })
+      .catch((err) => {
+        console.log(err);
+        this.setState({ saveValuesStatus: "error" });
+      });
   };
 
   /*
@@ -143,34 +149,34 @@ export default class SettingsSection extends Component<PropsType, StateType> {
           <Holder>
             <InputRow
               disabled={true}
-              label='Git Repository'
-              type='text'
-              width='100%'
+              label="Git Repository"
+              type="text"
+              width="100%"
               value={this.state.action.git_repo}
               setValue={(x: string) => console.log(x)}
             />
             <InputRow
               disabled={true}
-              label='Dockerfile Path'
-              type='text'
-              width='100%'
+              label="Dockerfile Path"
+              type="text"
+              width="100%"
               value={this.state.action.dockerfile_path}
               setValue={(x: string) => console.log(x)}
             />
             <InputRow
               disabled={true}
-              label='Docker Image Repository'
-              type='text'
-              width='100%'
+              label="Docker Image Repository"
+              type="text"
+              width="100%"
               value={this.state.action.image_repo_uri}
               setValue={(x: string) => console.log(x)}
             />
           </Holder>
         </>
-      )
+      );
     }
 
-    if (this.state.sourceType === 'registry') {
+    if (this.state.sourceType === "registry") {
       return (
         <>
           <Heading>Connected Source</Heading>
@@ -193,19 +199,26 @@ export default class SettingsSection extends Component<PropsType, StateType> {
       <>
         <Heading>Connect a Source</Heading>
         <Helper>
-          Select a repo to connect to. You can 
-          <A padRight={true} href={`/api/oauth/projects/${currentProject.id}/github?redirected=true`}>
+          Select a repo to connect to. You can
+          <A
+            padRight={true}
+            href={`/api/oauth/projects/${currentProject.id}/github?redirected=true`}
+          >
             log in with GitHub
-          </A> or
-          <Highlight onClick={() => this.setState({ sourceType: 'registry' })}>
+          </A>{" "}
+          or
+          <Highlight onClick={() => this.setState({ sourceType: "registry" })}>
             link an image registry
-          </Highlight>.
+          </Highlight>
+          .
         </Helper>
         <RepoSelector
           chart={this.props.currentChart}
           forceExpanded={true}
           actionConfig={this.state.actionConfig}
-          setActionConfig={(actionConfig: ActionConfigType) => this.setState({ actionConfig })}
+          setActionConfig={(actionConfig: ActionConfigType) =>
+            this.setState({ actionConfig })
+          }
         />
       </>
     );

+ 21 - 21
dashboard/src/main/home/cluster-dashboard/expanded-chart/ValuesYaml.tsx

@@ -48,28 +48,28 @@ export default class ValuesYaml extends Component<PropsType, StateType> {
     let { currentCluster, setCurrentError, currentProject } = this.context;
     this.setState({ saveValuesStatus: "loading" });
 
-    api.upgradeChartValues(
-      "<token>",
-      {
-        namespace: this.props.currentChart.namespace,
-        storage: StorageType.Secret,
-        values: this.state.values,
-      },
-      {
-        id: currentProject.id,
-        name: this.props.currentChart.name,
-        cluster_id: currentCluster.id,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          this.setState({ saveValuesStatus: "error" });
-        } else {
-          this.setState({ saveValuesStatus: "successful" });
-          this.props.refreshChart();
+    api
+      .upgradeChartValues(
+        "<token>",
+        {
+          namespace: this.props.currentChart.namespace,
+          storage: StorageType.Secret,
+          values: this.state.values,
+        },
+        {
+          id: currentProject.id,
+          name: this.props.currentChart.name,
+          cluster_id: currentCluster.id,
         }
-      }
-    );
+      )
+      .then((res) => {
+        this.setState({ saveValuesStatus: "successful" });
+        this.props.refreshChart();
+      })
+      .catch((err) => {
+        console.log(err);
+        this.setState({ saveValuesStatus: "error" });
+      });
   };
 
   render() {

+ 17 - 16
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx

@@ -46,21 +46,18 @@ export default class ControllerTab extends Component<PropsType, StateType> {
     }
     selectors.push(selector);
 
-    api.getMatchingPods(
-      "<token>",
-      {
-        cluster_id: currentCluster.id,
-        selectors,
-      },
-      {
-        id: currentProject.id,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          setCurrentError(JSON.stringify(err));
-          return;
+    api
+      .getMatchingPods(
+        "<token>",
+        {
+          cluster_id: currentCluster.id,
+          selectors,
+        },
+        {
+          id: currentProject.id,
         }
+      )
+      .then((res) => {
         let pods = res?.data?.map((pod: any) => {
           return {
             namespace: pod?.metadata?.namespace,
@@ -78,8 +75,12 @@ export default class ControllerTab extends Component<PropsType, StateType> {
         if (isFirst) {
           selectPod(res.data[0]);
         }
-      }
-    );
+      })
+      .catch((err) => {
+        console.log(err);
+        setCurrentError(JSON.stringify(err));
+        return;
+      });
   }
 
   getAvailability = (kind: string, c: any) => {

+ 26 - 25
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx

@@ -79,42 +79,43 @@ export default class StatusSection extends Component<PropsType, StateType> {
           <TabWrapper>{this.renderTabs()}</TabWrapper>
           {this.renderLogs()}
         </Wrapper>
-      )
+      );
     }
 
     return (
-      <NoControllers> 
-        <i className="material-icons">category</i> 
-        No objects to display. This might happen while your app is still deploying.
+      <NoControllers>
+        <i className="material-icons">category</i>
+        No objects to display. This might happen while your app is still
+        deploying.
       </NoControllers>
-    )
-  }
+    );
+  };
 
   componentDidMount() {
     const { selectors, currentChart } = this.props;
     let { currentCluster, currentProject, setCurrentError } = this.context;
 
-    api.getChartControllers(
-      "<token>",
-      {
-        namespace: currentChart.namespace,
-        cluster_id: currentCluster.id,
-        storage: StorageType.Secret,
-      },
-      {
-        id: currentProject.id,
-        name: currentChart.name,
-        revision: currentChart.version,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          setCurrentError(JSON.stringify(err));
-          this.setState({ controllers: [], loading: false });
-          return;
+    api
+      .getChartControllers(
+        "<token>",
+        {
+          namespace: currentChart.namespace,
+          cluster_id: currentCluster.id,
+          storage: StorageType.Secret,
+        },
+        {
+          id: currentProject.id,
+          name: currentChart.name,
+          revision: currentChart.version,
         }
+      )
+      .then((res) => {
         this.setState({ controllers: res.data, loading: false });
-      }
-    );
+      })
+      .catch((err) => {
+        setCurrentError(JSON.stringify(err));
+        this.setState({ controllers: [], loading: false });
+      });
   }
 
   render() {

+ 8 - 12
dashboard/src/main/home/dashboard/ClusterList.tsx

@@ -25,18 +25,16 @@ class Templates extends Component<PropsType, StateType> {
   };
 
   componentDidMount() {
-    api.getClusters(
-      "<token>",
-      {},
-      { id: this.context.currentProject.id },
-      (err: any, res: any) => {
-        if (res && res.data) {
+    api
+      .getClusters("<token>", {}, { id: this.context.currentProject.id })
+      .then((res) => {
+        if (res.data) {
           this.setState({ clusters: res.data, loading: false, error: "" });
         } else {
-          this.setState({ loading: false, error: err });
+          this.setState({ loading: false, error: "Response data missing" });
         }
-      }
-    );
+      })
+      .catch((err) => this.setState(err));
   }
 
   renderIcon = () => {
@@ -68,9 +66,7 @@ class Templates extends Component<PropsType, StateType> {
     return (
       <StyledClusterList>
         <Helper>Clusters connected to this project:</Helper>
-        <TemplateList>
-          {this.renderClusters()}
-        </TemplateList>
+        <TemplateList>{this.renderClusters()}</TemplateList>
       </StyledClusterList>
     );
   }

+ 9 - 13
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -36,20 +36,16 @@ class Dashboard extends Component<PropsType, StateType> {
 
   refreshInfras = () => {
     if (this.props.projectId) {
-      api.getInfra(
-        "<token>",
-        {},
-        {
-          project_id: this.props.projectId,
-        },
-        (err: any, res: any) => {
-          if (err) {
-            console.log(err);
-            return;
+      api
+        .getInfra(
+          "<token>",
+          {},
+          {
+            project_id: this.props.projectId,
           }
-          this.setState({ infras: res.data });
-        }
-      );
+        )
+        .then((res) => this.setState({ infras: res.data }))
+        .catch(console.log);
     }
   };
 

+ 99 - 91
dashboard/src/main/home/integrations/Integrations.tsx

@@ -1,26 +1,24 @@
 import React, { Component } from "react";
 import styled from "styled-components";
 
-import { Context } from 'shared/Context';
-import api from 'shared/api';
-import { integrationList } from 'shared/common';
-import { ActionConfigType, ChoiceType } from 'shared/types';
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { integrationList } from "shared/common";
 
 import IntegrationList from "./IntegrationList";
 import IntegrationForm from "./integration-form/IntegrationForm";
 
-import GHIcon from 'assets/GithubIcon';
+import GHIcon from "assets/GithubIcon";
 
-type PropsType = {
-};
+type PropsType = {};
 
 type StateType = {
-  currentCategory: string | null,
-  currentIntegration: string | null,
-  currentOptions: any[],
-  currentTitles: any[],
-  currentIds: any[],
-  currentIntegrationData: any[],
+  currentCategory: string | null;
+  currentIntegration: string | null;
+  currentOptions: any[];
+  currentTitles: any[];
+  currentIds: any[];
+  currentIntegrationData: any[];
 };
 
 export default class Integrations extends Component<PropsType, StateType> {
@@ -43,65 +41,49 @@ export default class Integrations extends Component<PropsType, StateType> {
     });
     switch (categoryType) {
       case "kubernetes":
-        api.getProjectClusters(
-          "<token>",
-          {},
-          { id: currentProject.id },
-          (err: any, res: any) => {
-            if (err) {
-              console.log(err);
-            } else {
-              // console.log(res.data)
-            }
-          }
-        );
+        api
+          .getProjectClusters("<token>", {}, { id: currentProject.id })
+          .then()
+          .catch(console.log);
         break;
       case "registry":
-        api.getProjectRegistries(
-          "<token>",
-          {},
-          { id: currentProject.id },
-          (err: any, res: any) => {
-            if (err) {
-              console.log(err);
-            } else {
-              // Sort res.data into service type and sort each service's registry alphabetically
-              let grouped: any = {};
-              let final: any = [];
-              for (let i = 0; i < res.data.length; i++) {
-                let p = res.data[i].service;
-                if (!grouped[p]) {
-                  grouped[p] = [];
-                }
-                grouped[p].push(res.data[i]);
+        api
+          .getProjectRegistries("<token>", {}, { id: currentProject.id })
+          .then((res) => {
+            // Sort res.data into service type and sort each service's registry alphabetically
+            let grouped: any = {};
+            let final: any = [];
+            for (let i = 0; i < res.data.length; i++) {
+              let p = res.data[i].service;
+              if (!grouped[p]) {
+                grouped[p] = [];
               }
-              Object.values(grouped).forEach((val: any) => {
-                final = final.concat(
-                  val.sort((a: any, b: any) => (a.name > b.name ? 1 : -1))
-                );
-              });
-
-              let currentOptions = [] as string[];
-              let currentTitles = [] as string[];
-              final.forEach((integration: any, i: number) => {
-                currentOptions.push(integration.service);
-                currentTitles.push(integration.name);
-              });
-              this.setState({
-                currentOptions,
-                currentTitles,
-                currentIntegrationData: res.data,
-              });
+              grouped[p].push(res.data[i]);
             }
-          }
-        );
+            Object.values(grouped).forEach((val: any) => {
+              final = final.concat(
+                val.sort((a: any, b: any) => (a.name > b.name ? 1 : -1))
+              );
+            });
+
+            let currentOptions = [] as string[];
+            let currentTitles = [] as string[];
+            final.forEach((integration: any, i: number) => {
+              currentOptions.push(integration.service);
+              currentTitles.push(integration.name);
+            });
+            this.setState({
+              currentOptions,
+              currentTitles,
+              currentIntegrationData: res.data,
+            });
+          })
+          .catch(console.log);
         break;
-      case 'repo':
-        api.getGitRepos('<token>', {
-        }, { project_id: currentProject.id }, (err: any, res: any) => {
-          if (err) {
-            console.log(err);
-          } else {
+      case "repo":
+        api
+          .getGitRepos("<token>", {}, { project_id: currentProject.id })
+          .then((res) => {
             let currentOptions = [] as string[];
             let currentTitles = [] as string[];
             let currentIds = [] as any[];
@@ -109,10 +91,15 @@ export default class Integrations extends Component<PropsType, StateType> {
               currentOptions.push(item.service);
               currentTitles.push(item.repo_entity);
               currentIds.push(item.id);
-            })
-            this.setState({ currentOptions, currentTitles, currentIds, currentIntegrationData: res.data })
-          }
-        });
+            });
+            this.setState({
+              currentOptions,
+              currentTitles,
+              currentIds,
+              currentIntegrationData: res.data,
+            });
+          })
+          .catch(console.log);
         break;
       default:
         console.log("Unknown integration category.");
@@ -187,38 +174,52 @@ export default class Integrations extends Component<PropsType, StateType> {
         </div>
       );
     } else if (currentCategory) {
-      let icon = integrationList[currentCategory] && integrationList[currentCategory].icon;
-      let label = integrationList[currentCategory] && integrationList[currentCategory].label;
-      let buttonText = integrationList[currentCategory] && integrationList[currentCategory].buttonText;
-      if (currentCategory !== 'repo') {
+      let icon =
+        integrationList[currentCategory] &&
+        integrationList[currentCategory].icon;
+      let label =
+        integrationList[currentCategory] &&
+        integrationList[currentCategory].label;
+      let buttonText =
+        integrationList[currentCategory] &&
+        integrationList[currentCategory].buttonText;
+      if (currentCategory !== "repo") {
         return (
           <div>
             <TitleSectionAlt>
               <Flex>
-                <i className="material-icons" onClick={() => this.setState({ currentCategory: null })}>
+                <i
+                  className="material-icons"
+                  onClick={() => this.setState({ currentCategory: null })}
+                >
                   keyboard_backspace
                 </i>
                 <Icon src={icon && icon} />
                 <Title>{label}</Title>
               </Flex>
-              <Button 
-                onClick={() => this.context.setCurrentModal('IntegrationsModal', { 
-                  category: currentCategory,
-                  setCurrentIntegration: (x: string) => this.setState({ currentIntegration: x })
-                })}
+              <Button
+                onClick={() =>
+                  this.context.setCurrentModal("IntegrationsModal", {
+                    category: currentCategory,
+                    setCurrentIntegration: (x: string) =>
+                      this.setState({ currentIntegration: x }),
+                  })
+                }
               >
                 <i className="material-icons">add</i>
                 {buttonText}
               </Button>
             </TitleSectionAlt>
-  
+
             <LineBreak />
-  
+
             <IntegrationList
               currentCategory={currentCategory}
               integrations={this.state.currentOptions}
               titles={this.state.currentTitles}
-              setCurrent={(x: string) => this.setState({ currentIntegration: x })}
+              setCurrent={(x: string) =>
+                this.setState({ currentIntegration: x })
+              }
               itemIdentifier={this.state.currentIntegrationData}
             />
           </div>
@@ -228,27 +229,34 @@ export default class Integrations extends Component<PropsType, StateType> {
           <div>
             <TitleSectionAlt>
               <Flex>
-                <i className="material-icons" onClick={() => this.setState({ currentCategory: null })}>
+                <i
+                  className="material-icons"
+                  onClick={() => this.setState({ currentCategory: null })}
+                >
                   keyboard_backspace
                 </i>
                 <Icon src={icon && icon} />
                 <Title>{label}</Title>
               </Flex>
-              <Button 
-                onClick={() => window.open(`/api/oauth/projects/${currentProject.id}/github`)}
+              <Button
+                onClick={() =>
+                  window.open(`/api/oauth/projects/${currentProject.id}/github`)
+                }
               >
                 <GHIcon />
                 {buttonText}
               </Button>
             </TitleSectionAlt>
-  
+
             <LineBreak />
 
             <IntegrationList
               currentCategory={currentCategory}
               integrations={this.state.currentOptions}
               titles={this.state.currentTitles}
-              setCurrent={(x: string) => this.setState({ currentIntegration: x })}
+              setCurrent={(x: string) =>
+                this.setState({ currentIntegration: x })
+              }
               itemIdentifier={this.state.currentIds}
             />
           </div>
@@ -262,8 +270,8 @@ export default class Integrations extends Component<PropsType, StateType> {
         </TitleSection>
 
         <IntegrationList
-          currentCategory={''}
-          integrations={['kubernetes', 'registry', 'repo']}
+          currentCategory={""}
+          integrations={["kubernetes", "registry", "repo"]}
           setCurrent={(x: any) => this.setState({ currentCategory: x })}
           isCategory={true}
         />

+ 24 - 20
dashboard/src/main/home/integrations/integration-form/ECRForm.tsx

@@ -41,30 +41,34 @@ export default class ECRForm extends Component<PropsType, StateType> {
     return false;
   };
 
+  catchErr = (err: any) => console.log(err);
+
   handleSubmit = () => {
     let { awsRegion, awsAccessId, awsSecretKey, credentialsName } = this.state;
     let { currentProject } = this.context;
 
-    api.createAWSIntegration('<token>', {
-      aws_region: awsRegion,
-      aws_access_key_id: awsAccessId,
-      aws_secret_access_key: awsSecretKey,
-    }, { id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else {
-        api.connectECRRegistry('<token>', {
-          name: credentialsName,
-          aws_integration_id: res.data.id,
-        }, { id: currentProject.id }, (err: any, res: any) => {
-          if (err) {
-            console.log(err);
-          } else {
-            this.props.closeForm();
-          }
-        });
-      }
-    });
+    api
+      .createAWSIntegration(
+        "<token>",
+        {
+          aws_region: awsRegion,
+          aws_access_key_id: awsAccessId,
+          aws_secret_access_key: awsSecretKey,
+        },
+        { id: currentProject.id }
+      )
+      .then((res) =>
+        api.connectECRRegistry(
+          "<token>",
+          {
+            name: credentialsName,
+            aws_integration_id: res.data.id,
+          },
+          { id: currentProject.id }
+        )
+      )
+      .then(() => this.props.closeForm())
+      .catch(this.catchErr);
   };
 
   render() {

+ 76 - 56
dashboard/src/main/home/integrations/integration-form/GCRForm.tsx

@@ -15,59 +15,75 @@ type PropsType = {
 };
 
 type StateType = {
-  credentialsName: string,
-  gcpRegion: string,
-  serviceAccountKey: string,
-  gcpProjectID: string,
-  url: string,
+  credentialsName: string;
+  gcpRegion: string;
+  serviceAccountKey: string;
+  gcpProjectID: string;
+  url: string;
 };
 
 export default class GCRForm extends Component<PropsType, StateType> {
   state = {
-    credentialsName: '',
-    gcpRegion: '',
-    serviceAccountKey: '',
-    gcpProjectID: '',
-    url: '',
-  }
+    credentialsName: "",
+    gcpRegion: "",
+    serviceAccountKey: "",
+    gcpProjectID: "",
+    url: "",
+  };
 
   isDisabled = (): boolean => {
-    let { credentialsName, gcpRegion, gcpProjectID, serviceAccountKey } = this.state;
-    if (credentialsName === '' || gcpRegion  === '' || serviceAccountKey === '' || gcpProjectID === '') {
+    let {
+      credentialsName,
+      gcpRegion,
+      gcpProjectID,
+      serviceAccountKey,
+    } = this.state;
+    if (
+      credentialsName === "" ||
+      gcpRegion === "" ||
+      serviceAccountKey === "" ||
+      gcpProjectID === ""
+    ) {
       return true;
     }
     return false;
   };
 
+  catchError = (err: any) => console.log(err);
+
   handleSubmit = () => {
     let { currentProject } = this.context;
 
-    api.createGCPIntegration('<token>', {
-      gcp_region: this.state.gcpRegion,
-      gcp_key_data: this.state.serviceAccountKey,
-      gcp_project_id: this.state.gcpProjectID,
-    }, {
-      project_id: currentProject.id,
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else {
-        api.connectGCRRegistry('<token>', {
-          name: this.state.credentialsName,
-          gcp_integration_id: res.data.id,
-          url: this.state.url,
-        }, {
-          id: currentProject.id,
-        }, (err: any, res: any) => {
-          if (err) {
-            console.log(err);
-          } else {
-            console.log(res.data);
-            this.props.closeForm();
+    api
+      .createGCPIntegration(
+        "<token>",
+        {
+          gcp_region: this.state.gcpRegion,
+          gcp_key_data: this.state.serviceAccountKey,
+          gcp_project_id: this.state.gcpProjectID,
+        },
+        {
+          project_id: currentProject.id,
+        }
+      )
+      .then((res) =>
+        api.connectGCRRegistry(
+          "<token>",
+          {
+            name: this.state.credentialsName,
+            gcp_integration_id: res.data.id,
+            url: this.state.url,
+          },
+          {
+            id: currentProject.id,
           }
-        })
-      }
-    });
+        )
+      )
+      .then((res) => {
+        console.log(res.data);
+        this.props.closeForm();
+      })
+      .catch(this.catchError);
   };
 
   render() {
@@ -81,10 +97,12 @@ export default class GCRForm extends Component<PropsType, StateType> {
           <InputRow
             type="text"
             value={this.state.credentialsName}
-            setValue={(credentialsName: string) => this.setState({ credentialsName })}
-            label='🏷️ Registry Name'
-            placeholder='ex: paper-straw'
-            width='100%'
+            setValue={(credentialsName: string) =>
+              this.setState({ credentialsName })
+            }
+            label="🏷️ Registry Name"
+            placeholder="ex: paper-straw"
+            width="100%"
           />
           <Heading>GCP Settings</Heading>
           <Helper>Service account credentials for GCP permissions.</Helper>
@@ -92,32 +110,34 @@ export default class GCRForm extends Component<PropsType, StateType> {
             type="text"
             value={this.state.gcpRegion}
             setValue={(gcpRegion: string) => this.setState({ gcpRegion })}
-            label='📍 GCP Region'
-            placeholder='ex: uranus-north3'
-            width='100%'
+            label="📍 GCP Region"
+            placeholder="ex: uranus-north3"
+            width="100%"
           />
           <TextArea
             value={this.state.serviceAccountKey}
-            setValue={(serviceAccountKey: string) => this.setState({ serviceAccountKey })}
-            label='🔑 Service Account Key (JSON)'
-            placeholder='(Paste your JSON service account key here)'
-            width='100%'
+            setValue={(serviceAccountKey: string) =>
+              this.setState({ serviceAccountKey })
+            }
+            label="🔑 Service Account Key (JSON)"
+            placeholder="(Paste your JSON service account key here)"
+            width="100%"
           />
           <InputRow
             type="text"
             value={this.state.gcpProjectID}
             setValue={(gcpProjectID: string) => this.setState({ gcpProjectID })}
-            label='📝 GCP Project ID'
-            placeholder='ex: skynet-dev-172969'
-            width='100%'
+            label="📝 GCP Project ID"
+            placeholder="ex: skynet-dev-172969"
+            width="100%"
           />
           <InputRow
-            type='text'
+            type="text"
             value={this.state.url}
             setValue={(url: string) => this.setState({ url })}
-            label='🔗 GCR URL'
-            placeholder='ex: gcr.io/skynet-dev-172969'
-            width='100%'
+            label="🔗 GCR URL"
+            placeholder="ex: gcr.io/skynet-dev-172969"
+            width="100%"
           />
         </CredentialWrapper>
         <SaveButton

+ 17 - 24
dashboard/src/main/home/modals/IntegrationsModal.tsx

@@ -20,30 +20,20 @@ export default class IntegrationsModal extends Component<PropsType, StateType> {
   componentDidMount() {
     let { category } = this.context.currentModalData;
     if (category === "kubernetes") {
-      api.getClusterIntegrations("<token>", {}, {}, (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else {
-          this.setState({ integrations: res.data });
-        }
-      });
+      api
+        .getClusterIntegrations("<token>", {}, {})
+        .then((res) => this.setState({ integrations: res.data }))
+        .catch((err) => console.log(err));
     } else if (category === "registry") {
-      api.getRegistryIntegrations("<token>", {}, {}, (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else {
-          // console.log(res.data)
-          this.setState({ integrations: res.data });
-        }
-      });
+      api
+        .getRegistryIntegrations("<token>", {}, {})
+        .then((res) => this.setState({ integrations: res.data }))
+        .catch((err) => console.log(err));
     } else {
-      api.getRepoIntegrations("<token>", {}, {}, (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else {
-          this.setState({ integrations: res.data });
-        }
-      });
+      api
+        .getRepoIntegrations("<token>", {}, {})
+        .then((res) => this.setState({ integrations: res.data }))
+        .catch((err) => console.log(err));
     }
   }
 
@@ -51,8 +41,11 @@ export default class IntegrationsModal extends Component<PropsType, StateType> {
     if (this.context.currentModalData) {
       let { setCurrentIntegration } = this.context.currentModalData;
       return this.state.integrations.map((integration: any, i: number) => {
-        let icon = integrationList[integration.service] && integrationList[integration.service].icon;
-        let disabled = integration.service === 'kube' || integration.service === 'docker';
+        let icon =
+          integrationList[integration.service] &&
+          integrationList[integration.service].icon;
+        let disabled =
+          integration.service === "kube" || integration.service === "docker";
         return (
           <IntegrationOption
             key={i}

+ 46 - 61
dashboard/src/main/home/modals/UpdateClusterModal.tsx

@@ -28,24 +28,25 @@ class UpdateClusterModal extends Component<PropsType, StateType> {
     showDeleteOverlay: false,
   };
 
+  catchErr = (err: any) => {
+    this.setState({ status: "error" });
+    console.log(err);
+  };
+
   handleDelete = () => {
     let { currentProject, currentCluster } = this.context;
     this.setState({ status: "loading" });
 
-    api.deleteCluster(
-      "<token>",
-      {},
-      {
-        project_id: currentProject.id,
-        cluster_id: currentCluster.id,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          this.setState({ status: "error" });
-          console.log(err);
-          return;
+    api
+      .deleteCluster(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
         }
-
+      )
+      .then((_) => {
         if (!currentCluster?.infra_id) {
           // TODO: make this more declarative from the Home component
           this.props.setRefreshClusters(true);
@@ -58,68 +59,52 @@ class UpdateClusterModal extends Component<PropsType, StateType> {
         // Handle destroying infra we've provisioned
         switch (currentCluster.service) {
           case "eks":
-            api.destroyEKS(
-              "<token>",
-              { eks_name: currentCluster.name },
-              {
-                project_id: currentProject.id,
-                infra_id: currentCluster.infra_id,
-              },
-              (err: any, res: any) => {
-                if (err) {
-                  this.setState({ status: "error" });
-                  console.log(err);
-                } else {
-                  console.log("destroyed provisioned infra.");
+            api
+              .destroyEKS(
+                "<token>",
+                { eks_name: currentCluster.name },
+                {
+                  project_id: currentProject.id,
+                  infra_id: currentCluster.infra_id,
                 }
-              }
-            );
+              )
+              .then(() => console.log("destroyed provisioned infra."))
+              .catch(this.catchErr);
             break;
-
           case "gke":
-            api.destroyGKE(
-              "<token>",
-              { gke_name: currentCluster.name },
-              {
-                project_id: currentProject.id,
-                infra_id: currentCluster.infra_id,
-              },
-              (err: any, res: any) => {
-                if (err) {
-                  this.setState({ status: "error" });
-                  console.log(err);
-                } else {
-                  console.log("destroyed provisioned infra.");
+            api
+              .destroyGKE(
+                "<token>",
+                { gke_name: currentCluster.name },
+                {
+                  project_id: currentProject.id,
+                  infra_id: currentCluster.infra_id,
                 }
-              }
-            );
+              )
+              .then(() => console.log("destroyed provisioned infra."))
+              .catch(this.catchErr);
             break;
 
           case "doks":
-            api.destroyDOKS(
-              "<token>",
-              { doks_name: currentCluster.name },
-              {
-                project_id: currentProject.id,
-                infra_id: currentCluster.infra_id,
-              },
-              (err: any, res: any) => {
-                if (err) {
-                  this.setState({ status: "error" });
-                  console.log(err);
-                } else {
-                  console.log("destroyed provisioned infra.");
+            api
+              .destroyDOKS(
+                "<token>",
+                { doks_name: currentCluster.name },
+                {
+                  project_id: currentProject.id,
+                  infra_id: currentCluster.infra_id,
                 }
-              }
-            );
+              )
+              .then(() => console.log("destroyed provisioned infra."))
+              .catch(this.catchErr);
             break;
         }
 
         this.props.setRefreshClusters(true);
         this.setState({ status: "successful", showDeleteOverlay: false });
         this.context.setCurrentModal(null, null);
-      }
-    );
+      })
+      .catch(this.catchErr);
   };
 
   renderWarning = () => {

+ 4 - 3
dashboard/src/main/home/navbar/Navbar.tsx

@@ -25,9 +25,10 @@ export default class Navbar extends Component<PropsType, StateType> {
     let { setCurrentError } = this.context;
 
     // Attempt user logout
-    api.logOutUser("<token>", {}, {}, (err: any, res: any) => {
-      err ? setCurrentError(err.response.data.errors[0]) : logOut();
-    });
+    api
+      .logOutUser("<token>", {}, {})
+      .then(logOut)
+      .catch((err) => setCurrentError(err.response.data.errors[0]));
   };
 
   renderSettingsDropdown = () => {

+ 48 - 66
dashboard/src/main/home/project-settings/InviteList.tsx

@@ -39,20 +39,16 @@ export default class InviteList extends Component<PropsType, StateType> {
     let { currentProject } = this.context;
 
     this.setState({ loading: true });
-    api.getInvites(
-      "<token>",
-      {},
-      {
-        id: currentProject.id,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else {
-          this.setState({ invites: res.data, loading: false });
+    api
+      .getInvites(
+        "<token>",
+        {},
+        {
+          id: currentProject.id,
         }
-      }
-    );
+      )
+      .then((res) => this.setState({ invites: res.data, loading: false }))
+      .catch((err) => console.log(err));
   };
 
   validateEmail = () => {
@@ -67,68 +63,54 @@ export default class InviteList extends Component<PropsType, StateType> {
 
   createInvite = () => {
     let { currentProject } = this.context;
-    api.createInvite(
-      "<token>",
-      { email: this.state.email },
-      { id: currentProject.id },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else {
-          this.getInviteData();
-          this.setState({ email: "" });
-        }
-      }
-    );
+    api
+      .createInvite(
+        "<token>",
+        { email: this.state.email },
+        { id: currentProject.id }
+      )
+      .then((_) => {
+        this.getInviteData();
+        this.setState({ email: "" });
+      })
+      .catch((err) => console.log(err));
   };
 
   deleteInvite = (index: number) => {
     let { currentProject } = this.context;
-    api.deleteInvite(
-      "<token>",
-      {},
-      {
-        id: currentProject.id,
-        invId: this.state.invites[index].id,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else {
-          this.getInviteData();
+    api
+      .deleteInvite(
+        "<token>",
+        {},
+        {
+          id: currentProject.id,
+          invId: this.state.invites[index].id,
         }
-      }
-    );
+      )
+      .then(this.getInviteData)
+      .catch((err) => console.log(err));
   };
 
   replaceInvite = (index: number) => {
     let { currentProject } = this.context;
-    api.createInvite(
-      "<token>",
-      { email: this.state.invites[index].email },
-      { id: currentProject.id },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else {
-          api.deleteInvite(
-            "<token>",
-            {},
-            {
-              id: currentProject.id,
-              invId: this.state.invites[index].id,
-            },
-            (err: any, res: any) => {
-              if (err) {
-                console.log(err);
-              } else {
-                this.getInviteData();
-              }
-            }
-          );
-        }
-      }
-    );
+    api
+      .createInvite(
+        "<token>",
+        { email: this.state.invites[index].email },
+        { id: currentProject.id }
+      )
+      .then((_) =>
+        api.deleteInvite(
+          "<token>",
+          {},
+          {
+            id: currentProject.id,
+            invId: this.state.invites[index].id,
+          }
+        )
+      )
+      .then(this.getInviteData)
+      .catch((err) => console.log(err));
   };
 
   copyToClip = (index: number) => {

+ 67 - 93
dashboard/src/main/home/provisioner/AWSFormSection.tsx

@@ -104,130 +104,100 @@ class AWSFormSection extends Component<PropsType, StateType> {
     }
   };
 
+  catchError = (err: any) => {
+    console.log(err);
+    this.props.handleError();
+  };
+
   // Step 1: Create a project
+  // TODO: promisify this function
   createProject = (callback?: any) => {
     console.log("Creating project");
     let { projectName, handleError } = this.props;
     let { user, setProjects, setCurrentProject, currentProject } = this.context;
 
-    api.createProject(
-      "<token>",
-      { name: projectName },
-      {},
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          handleError();
-          return;
-        } else {
-          let proj = res.data;
-
-          // Need to set project list for dropdown
-          // TODO: consolidate into ProjectSection (case on exists in list on set)
-          api.getProjects(
+    api
+      .createProject("<token>", { name: projectName }, {})
+      .then((res) => {
+        let proj = res.data;
+        // Need to set project list for dropdown
+        // TODO: consolidate into ProjectSection (case on exists in list on set)
+        api
+          .getProjects(
             "<token>",
             {},
             {
               id: user.userId,
-            },
-            (err: any, res: any) => {
-              if (err) {
-                console.log(err);
-                handleError();
-                return;
-              }
-              setProjects(res.data);
-              setCurrentProject(proj, () => {
-                callback && callback();
-              });
             }
-          );
-        }
-      }
-    );
+          )
+          .then((res) => {
+            setProjects(res.data);
+            setCurrentProject(proj, () => {
+              callback && callback();
+            });
+          })
+          .catch(this.catchError);
+      })
+      .catch(this.catchError);
   };
 
-  provisionECR = (callback?: any) => {
+  provisionECR = () => {
     console.log("Provisioning ECR");
     let { awsAccessId, awsSecretKey, awsRegion } = this.state;
     let { currentProject } = this.context;
-    let { handleError } = this.props;
-
-    api.createAWSIntegration(
-      "<token>",
-      {
-        aws_region: awsRegion,
-        aws_access_key_id: awsAccessId,
-        aws_secret_access_key: awsSecretKey,
-      },
-      { id: currentProject.id },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          handleError();
-          return;
-        }
 
+    return api
+      .createAWSIntegration(
+        "<token>",
+        {
+          aws_region: awsRegion,
+          aws_access_key_id: awsAccessId,
+          aws_secret_access_key: awsSecretKey,
+        },
+        { id: currentProject.id }
+      )
+      .then((res) =>
         api.provisionECR(
           "<token>",
           {
             aws_integration_id: res.data.id,
             ecr_name: `${currentProject.name}-registry`,
           },
-          { id: currentProject.id },
-          (err: any, res: any) => {
-            if (err) {
-              console.log(err);
-              handleError();
-              return;
-            }
-            callback && callback();
-          }
-        );
-      }
-    );
+          { id: currentProject.id }
+        )
+      )
+      .catch(this.catchError);
   };
 
   provisionEKS = () => {
     console.log("Provisioning EKS");
-    let { handleError } = this.props;
     let { awsAccessId, awsSecretKey, awsRegion } = this.state;
     let { currentProject } = this.context;
 
     let clusterName = `${currentProject.name}-cluster`;
-    api.createAWSIntegration(
-      "<token>",
-      {
-        aws_region: awsRegion,
-        aws_access_key_id: awsAccessId,
-        aws_secret_access_key: awsSecretKey,
-        aws_cluster_id: clusterName,
-      },
-      { id: currentProject.id },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          handleError();
-          return;
-        }
+    api
+      .createAWSIntegration(
+        "<token>",
+        {
+          aws_region: awsRegion,
+          aws_access_key_id: awsAccessId,
+          aws_secret_access_key: awsSecretKey,
+          aws_cluster_id: clusterName,
+        },
+        { id: currentProject.id }
+      )
+      .then((res) =>
         api.provisionEKS(
           "<token>",
           {
             aws_integration_id: res.data.id,
             eks_name: clusterName,
           },
-          { id: currentProject.id },
-          (err: any, eks: any) => {
-            if (err) {
-              console.log(err);
-              handleError();
-              return;
-            }
-            this.props.history.push("dashboard?tab=provisioner");
-          }
-        );
-      }
-    );
+          { id: currentProject.id }
+        )
+      )
+      .then(() => this.props.history.push("dashboard?tab=provisioner"))
+      .catch(this.catchError);
   };
 
   // TODO: handle generically (with > 2 steps)
@@ -238,10 +208,12 @@ class AWSFormSection extends Component<PropsType, StateType> {
     if (!projectName) {
       if (selectedInfras.length === 2) {
         // Case: project exists, provision ECR + EKS
-        this.provisionECR(this.provisionEKS);
+        this.provisionECR().then(this.provisionEKS);
       } else if (selectedInfras[0].value === "ecr") {
         // Case: project exists, only provision ECR
-        this.provisionECR(() => this.props.history.push("dashboard?tab=provisioner"));
+        this.provisionECR().then(() =>
+          this.props.history.push("dashboard?tab=provisioner")
+        );
       } else {
         // Case: project exists, only provision EKS
         this.provisionEKS();
@@ -249,12 +221,14 @@ class AWSFormSection extends Component<PropsType, StateType> {
     } else {
       if (selectedInfras.length === 2) {
         // Case: project DNE, provision ECR + EKS
-        this.createProject(() => this.provisionECR(this.provisionEKS));
+        this.createProject(() => this.provisionECR().then(this.provisionEKS));
       } else if (selectedInfras[0].value === "ecr") {
         // Case: project DNE, only provision ECR
-        this.createProject(() => this.provisionECR(() => {
-          this.props.history.push("dashboard?tab=provisioner");
-        }));
+        this.createProject(() =>
+          this.provisionECR().then(() => {
+            this.props.history.push("dashboard?tab=provisioner");
+          })
+        );
       } else {
         // Case: project DNE, only provision EKS
         this.createProject(this.provisionEKS);

+ 25 - 34
dashboard/src/main/home/provisioner/DOFormSection.tsx

@@ -87,46 +87,37 @@ export default class DOFormSection extends Component<PropsType, StateType> {
     }
   };
 
+  catchError = (err: any) => {
+    console.log(err);
+    this.props.handleError();
+    return;
+  };
+
   // Step 1: Create a project
   createProject = (callback?: any) => {
     console.log("Creating project");
-    let { projectName, handleError } = this.props;
+    let { projectName } = this.props;
     let { user, setProjects, setCurrentProject } = this.context;
 
-    api.createProject(
-      "<token>",
-      { name: projectName },
-      {},
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          handleError();
-          return;
-        } else {
-          let proj = res.data;
+    api
+      .createProject("<token>", { name: projectName }, {})
+      .then(async (res) => {
+        let proj = res.data;
 
-          // Need to set project list for dropdown
-          // TODO: consolidate into ProjectSection (case on exists in list on set)
-          api.getProjects(
-            "<token>",
-            {},
-            {
-              id: user.userId,
-            },
-            (err: any, res: any) => {
-              if (err) {
-                console.log(err);
-                handleError();
-                return;
-              }
-              setProjects(res.data);
-              setCurrentProject(proj);
-              callback && callback(proj.id);
-            }
-          );
-        }
-      }
-    );
+        // Need to set project list for dropdown
+        // TODO: consolidate into ProjectSection (case on exists in list on set)
+        const res_1 = await api.getProjects(
+          "<token>",
+          {},
+          {
+            id: user.userId,
+          }
+        );
+        setProjects(res_1.data);
+        setCurrentProject(proj);
+        callback && callback(proj.id);
+      })
+      .catch(this.catchError);
   };
 
   doRedirect = (projectId: number) => {

+ 23 - 32
dashboard/src/main/home/provisioner/ExistingClusterSection.tsx

@@ -27,39 +27,30 @@ class ExistingClusterSection extends Component<PropsType, StateType> {
     let { user, setProjects, setCurrentProject } = this.context;
 
     this.setState({ buttonStatus: "loading" });
-    api.createProject(
-      "<token>",
-      { name: projectName },
-      {},
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else {
-          api.getProjects(
-            "<token>",
-            {},
-            {
-              id: user.userId,
-            },
-            (err: any, res: any) => {
-              if (err) {
-                console.log(err);
-              } else if (res.data) {
-                setProjects(res.data);
-                if (res.data.length > 0) {
-                  let proj = res.data.find((el: ProjectType) => {
-                    return el.name === projectName;
-                  });
-                  setCurrentProject(proj);
-
-                  this.props.history.push("dashboard?tab=overview");
-                }
-              }
-            }
-          );
+    api
+      .createProject("<token>", { name: projectName }, {})
+      .then((res) =>
+        api.getProjects(
+          "<token>",
+          {},
+          {
+            id: user.userId,
+          }
+        )
+      )
+      .then((res) => {
+        if (res.data) {
+          setProjects(res.data);
+          if (res.data.length > 0) {
+            let proj = res.data.find((el: ProjectType) => {
+              return el.name === projectName;
+            });
+            setCurrentProject(proj);
+            this.props.history.push("dashboard?tab=overview");
+          }
         }
-      }
-    );
+      })
+      .catch(console.log);
   };
 
   render() {

+ 74 - 82
dashboard/src/main/home/provisioner/GCPFormSection.tsx

@@ -107,68 +107,55 @@ class GCPFormSection extends Component<PropsType, StateType> {
     }
   };
 
+  catchError = (err: any) => {
+    console.log(err);
+    this.props.handleError();
+  };
+
   // Step 1: Create a project
   createProject = (callback?: any) => {
     console.log("Creating project");
     let { projectName, handleError } = this.props;
     let { user, setProjects, setCurrentProject } = this.context;
 
-    api.createProject(
-      "<token>",
-      { name: projectName },
-      {},
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          handleError();
-          return;
-        } else {
-          let proj = res.data;
-
-          // Need to set project list for dropdown
-          // TODO: consolidate into ProjectSection (case on exists in list on set)
-          api.getProjects(
+    api
+      .createProject("<token>", { name: projectName }, {})
+      .then((res) => {
+        let proj = res.data;
+
+        // Need to set project list for dropdown
+        // TODO: consolidate into ProjectSection (case on exists in list on set)
+        api
+          .getProjects(
             "<token>",
             {},
             {
               id: user.userId,
-            },
-            (err: any, res: any) => {
-              if (err) {
-                console.log(err);
-                handleError();
-                return;
-              }
-              setProjects(res.data);
-              setCurrentProject(proj);
-              callback && callback();
             }
-          );
-        }
-      }
-    );
+          )
+          .then((res) => {
+            setProjects(res.data);
+            setCurrentProject(proj);
+            callback && callback();
+          })
+          .catch(this.catchError);
+      })
+      .catch(this.catchError);
   };
 
   provisionGCR = (id: number, callback?: any) => {
     console.log("Provisioning GCR");
     let { currentProject } = this.context;
-    let { handleError } = this.props;
 
-    api.createGCR(
-      "<token>",
-      {
-        gcp_integration_id: id,
-      },
-      { project_id: currentProject.id },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          handleError();
-          return;
-        }
-        callback && callback();
-      }
-    );
+    return api
+      .createGCR(
+        "<token>",
+        {
+          gcp_integration_id: id,
+        },
+        { project_id: currentProject.id }
+      )
+      .catch(this.catchError);
   };
 
   provisionGKE = (id: number) => {
@@ -177,49 +164,54 @@ class GCPFormSection extends Component<PropsType, StateType> {
     let { currentProject } = this.context;
 
     let clusterName = `${currentProject.name}-cluster`;
-    api.createGKE(
-      "<token>",
-      {
-        gke_name: clusterName,
-        gcp_integration_id: id,
-      },
-      { project_id: currentProject.id },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          handleError();
-          return;
-        }
+    api
+      .createGKE(
+        "<token>",
+        {
+          gke_name: clusterName,
+          gcp_integration_id: id,
+        },
+        { project_id: currentProject.id }
+      )
+      .then((res) => {
         this.props.history.push("dashboard?tab=provisioner");
-    });
-  }
+      })
+      .catch(this.catchError);
+  };
 
   handleCreateFlow = () => {
     let { selectedInfras, gcpKeyData, gcpProjectId, gcpRegion } = this.state;
     let { currentProject } = this.context;
-    api.createGCPIntegration('<token>', {
-      gcp_region: gcpRegion,
-      gcp_key_data: gcpKeyData,
-      gcp_project_id: gcpProjectId,
-    }, { project_id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else if (res?.data) {
-        console.log('gcp provisioned with response: ', res.data);
-        let { id } = res.data;
-
-        if (selectedInfras.length === 2) {
-          // Case: project exists, provision GCR + GKE
-          this.provisionGCR(id, () => this.provisionGKE(id));
-        } else if (selectedInfras[0].value === 'gcr') {
-          // Case: project exists, only provision GCR
-          this.provisionGCR(id, () => this.props.history.push("dashboard?tab=provisioner"));
-        } else {
-          // Case: project exists, only provision GKE
-          this.provisionGKE(id);
+    api
+      .createGCPIntegration(
+        "<token>",
+        {
+          gcp_region: gcpRegion,
+          gcp_key_data: gcpKeyData,
+          gcp_project_id: gcpProjectId,
+        },
+        { project_id: currentProject.id }
+      )
+      .then((res) => {
+        if (res?.data) {
+          console.log("gcp provisioned with response: ", res.data);
+          let { id } = res.data;
+
+          if (selectedInfras.length === 2) {
+            // Case: project exists, provision GCR + GKE
+            this.provisionGCR(id).then(() => this.provisionGKE(id));
+          } else if (selectedInfras[0].value === "gcr") {
+            // Case: project exists, only provision GCR
+            this.provisionGCR(id).then(() =>
+              this.props.history.push("dashboard?tab=provisioner")
+            );
+          } else {
+            // Case: project exists, only provision GKE
+            this.provisionGKE(id);
+          }
         }
-      }
-    });
+      })
+      .catch(console.log);
   };
 
   // TODO: handle generically (with > 2 steps)

+ 54 - 53
dashboard/src/main/home/provisioner/Provisioner.tsx

@@ -1,39 +1,35 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-
-import api from 'shared/api';
-import { Context } from 'shared/Context';
-import loading from 'assets/loading.gif';
-import warning from 'assets/warning.png';
-import { InfraType, ProjectType } from 'shared/types';
-import Loading from 'components/Loading';
-
-import Helper from 'components/values-form/Helper';
-import InfraStatuses from './InfraStatuses';
-import ProvisionerLogs from './ProvisionerLogs'
+import React, { Component } from "react";
+import styled from "styled-components";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { InfraType, ProjectType } from "shared/types";
+import Loading from "components/Loading";
+
+import InfraStatuses from "./InfraStatuses";
+import ProvisionerLogs from "./ProvisionerLogs";
 import { RouteComponentProps, withRouter } from "react-router";
-import { Link } from "react-router-dom";
 
 type PropsType = RouteComponentProps & {};
 
 type StateType = {
-  error: boolean,
-  logs: string[],
-  websockets: any[],
-  maxStep : Record<string, number>,
-  currentStep: Record<string, number>,
-  triggerEnd: boolean,
-  infras: InfraType[],
-  loading: boolean,
-  selectedInfra: InfraType,
-  currentProject: ProjectType,
+  error: boolean;
+  logs: string[];
+  websockets: any[];
+  maxStep: Record<string, number>;
+  currentStep: Record<string, number>;
+  triggerEnd: boolean;
+  infras: InfraType[];
+  loading: boolean;
+  selectedInfra: InfraType;
+  currentProject: ProjectType;
 };
 
 class Provisioner extends Component<PropsType, StateType> {
   state = {
     error: false,
     logs: [] as string[],
-    websockets : [] as any[],
+    websockets: [] as any[],
     maxStep: {} as Record<string, any>,
     currentStep: {} as Record<string, number>,
     triggerEnd: false,
@@ -41,59 +37,64 @@ class Provisioner extends Component<PropsType, StateType> {
     selectedInfra: null as InfraType,
     loading: true,
     currentProject: this.context.currentProject,
-  }
+  };
 
   selectInfra = (infra: InfraType) => {
-    this.setState({ selectedInfra: infra })
-  }
+    this.setState({ selectedInfra: infra });
+  };
 
   componentDidMount() {
     let { currentProject } = this.state;
 
-    api.getInfra('<token>', {}, { 
-      project_id: currentProject.id 
-    }, (err: any, res: any) => {
-      if (err) return;
-
-      let infras = res.data.sort((a: InfraType, b: InfraType) => {
-        return b.id - a.id
-      });
-
-      this.setState({ 
-        error: false, 
-        infras, 
-        loading: false,
-        selectedInfra: infras[0],
-      });
-    });
+    api
+      .getInfra(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+        }
+      )
+      .then((res) => {
+        let infras = res.data.sort((a: InfraType, b: InfraType) => {
+          return b.id - a.id;
+        });
+
+        this.setState({
+          error: false,
+          infras,
+          loading: false,
+          selectedInfra: infras[0],
+        });
+      })
+      .catch();
   }
 
   render() {
     if (this.state.loading) {
       return (
-        <StyledProvisioner> 
+        <StyledProvisioner>
           <Loading />
         </StyledProvisioner>
-      )
+      );
     }
 
     if (this.state.infras.length > 0) {
       return (
         <StyledProvisioner>
           <TabWrapper>
-            <InfraStatuses 
-              infras={this.state.infras} 
+            <InfraStatuses
+              infras={this.state.infras}
               selectInfra={this.selectInfra.bind(this)}
               selectedInfra={this.state.selectedInfra}
             />
           </TabWrapper>
 
-          <ProvisionerLogs 
-            key={this.state.selectedInfra?.id} 
-            selectedInfra={this.state.selectedInfra} 
+          <ProvisionerLogs
+            key={this.state.selectedInfra?.id}
+            selectedInfra={this.state.selectedInfra}
           />
         </StyledProvisioner>
-      )
+      );
     }
 
     return (
@@ -126,4 +127,4 @@ const TabWrapper = styled.div`
   min-width: 250px;
   height: 100%;
   overflow-y: auto;
-`;
+`;

+ 34 - 41
dashboard/src/main/home/sidebar/ClusterSection.tsx

@@ -41,52 +41,45 @@ class ClusterSection extends Component<PropsType, StateType> {
     let { currentProject, setCurrentCluster } = this.context;
 
     // TODO: query with selected filter once implemented
-    api.getClusters(
-      "<token>",
-      {},
-      { id: currentProject.id },
-      (err: any, res: any) => {
-        if (err) {
-          // Assume intializing if no contexts
-          this.props.setWelcome(true);
-        } else {
-          this.props.setWelcome(false);
-          // TODO: handle uninitialized kubeconfig
-          if (res.data) {
-            let clusters = res.data;
-            clusters.sort((a: any, b: any) => a.id - b.id);
-            if (clusters.length > 0) {
-              this.setState({ clusters });
-              let saved = JSON.parse(
-                localStorage.getItem(currentProject.id + "-cluster")
-              );
-              if (saved !== "null") {
-                setCurrentCluster(clusters[0]);
-                for (let i = 0; i < clusters.length; i++) {
-                  if (
-                    clusters[i].id === saved.id &&
-                    clusters[i].project_id === saved.project_id &&
-                    clusters[i].name === saved.name
-                  ) {
-                    setCurrentCluster(clusters[i]);
-                    break;
-                  }
+    api
+      .getClusters("<token>", {}, { id: currentProject.id })
+      .then((res) => {
+        this.props.setWelcome(false);
+        // TODO: handle uninitialized kubeconfig
+        if (res.data) {
+          let clusters = res.data;
+          clusters.sort((a: any, b: any) => a.id - b.id);
+          if (clusters.length > 0) {
+            this.setState({ clusters });
+            let saved = JSON.parse(
+              localStorage.getItem(currentProject.id + "-cluster")
+            );
+            if (saved !== "null") {
+              setCurrentCluster(clusters[0]);
+              for (let i = 0; i < clusters.length; i++) {
+                if (
+                  clusters[i].id === saved.id &&
+                  clusters[i].project_id === saved.project_id &&
+                  clusters[i].name === saved.name
+                ) {
+                  setCurrentCluster(clusters[i]);
+                  break;
                 }
-              } else {
-                setCurrentCluster(clusters[0]);
               }
-            } else if (
-              this.props.currentView !== "provisioner" &&
-              this.props.currentView !== "new-project"
-            ) {
-              this.setState({ clusters: [] });
-              setCurrentCluster(null);
-              // this.props.history.push("dashboard?tab=overview");
+            } else {
+              setCurrentCluster(clusters[0]);
             }
+          } else if (
+            this.props.currentView !== "provisioner" &&
+            this.props.currentView !== "new-project"
+          ) {
+            this.setState({ clusters: [] });
+            setCurrentCluster(null);
+            // this.props.history.push("dashboard?tab=overview");
           }
         }
-      }
-    );
+      })
+      .catch((err) => this.props.setWelcome(true));
   };
 
   componentDidMount() {

+ 5 - 6
dashboard/src/main/home/templates/Templates.tsx

@@ -33,10 +33,9 @@ export default class Templates extends Component<PropsType, StateType> {
   };
 
   componentDidMount() {
-    api.getTemplates("<token>", {}, {}, (err: any, res: any) => {
-      if (err) {
-        this.setState({ loading: false, error: true });
-      } else {
+    api
+      .getTemplates("<token>", {}, {})
+      .then((res) => {
         this.setState({ porterTemplates: res.data, error: false }, () => {
           this.state.porterTemplates.sort((a, b) => (a.name > b.name ? 1 : -1));
           this.state.porterTemplates.sort((a, b) =>
@@ -44,8 +43,8 @@ export default class Templates extends Component<PropsType, StateType> {
           );
           this.setState({ loading: false });
         });
-      }
-    });
+      })
+      .catch(() => this.setState({ loading: false, error: true }));
   }
 
   renderIcon = (icon: string) => {

+ 21 - 23
dashboard/src/main/home/templates/expanded-template/ExpandedTemplate.tsx

@@ -36,30 +36,28 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
 
   componentDidMount() {
     this.setState({ loading: true });
-    api.getTemplateInfo(
-      "<token>",
-      {},
-      {
-        name: this.props.currentTemplate.name.toLowerCase().trim(),
-        version: "latest",
-      },
-      (err: any, res: any) => {
-        if (err) {
-          this.setState({ loading: false, error: true });
-        } else {
-          let { form, values, markdown, metadata } = res.data;
-          let keywords = metadata.keywords;
-          this.setState({
-            form,
-            values,
-            markdown,
-            keywords,
-            loading: false,
-            error: false,
-          });
+    api
+      .getTemplateInfo(
+        "<token>",
+        {},
+        {
+          name: this.props.currentTemplate.name.toLowerCase().trim(),
+          version: "latest",
         }
-      }
-    );
+      )
+      .then((res) => {
+        let { form, values, markdown, metadata } = res.data;
+        let keywords = metadata.keywords;
+        this.setState({
+          form,
+          values,
+          markdown,
+          keywords,
+          loading: false,
+          error: false,
+        });
+      })
+      .catch((err) => this.setState({ loading: false, error: true }));
   }
 
   renderContents = () => {

+ 120 - 132
dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx

@@ -79,29 +79,24 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
     let { currentProject, currentCluster } = this.context;
     let { actionConfig } = this.state;
 
-    api.createGHAction(
-      "<token>",
-      {
-        git_repo: actionConfig.git_repo,
-        image_repo_uri: actionConfig.image_repo_uri,
-        dockerfile_path: actionConfig.dockerfile_path,
-        git_repo_id: actionConfig.git_repo_id,
-      },
-      {
-        project_id: currentProject.id,
-        CLUSTER_ID: currentCluster.id,
-        RELEASE_NAME: chartName,
-        RELEASE_NAMESPACE: chartNamespace,
-      },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else {
-          // Exit to initial settings tab
-          console.log(res.data);
+    api
+      .createGHAction(
+        "<token>",
+        {
+          git_repo: actionConfig.git_repo,
+          image_repo_uri: actionConfig.image_repo_uri,
+          dockerfile_path: actionConfig.dockerfile_path,
+          git_repo_id: actionConfig.git_repo_id,
+        },
+        {
+          project_id: currentProject.id,
+          CLUSTER_ID: currentCluster.id,
+          RELEASE_NAME: chartName,
+          RELEASE_NAMESPACE: chartNamespace,
         }
-      }
-    );
+      )
+      .then((res) => console.log(res.data))
+      .catch(console.log);
   };
 
   onSubmitAddon = (wildcard?: any) => {
@@ -115,46 +110,46 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
       _.set(values, key, wildcard[key]);
     }
 
-    api.deployTemplate(
-      "<token>",
-      {
-        templateName: this.props.currentTemplate.name,
-        storage: StorageType.Secret,
-        formValues: values,
-        namespace: this.state.selectedNamespace,
-        name,
-      },
-      {
-        id: currentProject.id,
-        cluster_id: currentCluster.id,
-        name: this.props.currentTemplate.name.toLowerCase().trim(),
-        version: "latest",
-      },
-      (err: any, res: any) => {
-        if (err) {
-          this.setState({ saveValuesStatus: "error" });
-          posthog.capture("Failed to deploy template", {
-            name: this.props.currentTemplate.name,
-            namespace: this.state.selectedNamespace,
-            values: values,
-            error: err,
-          });
-        } else {
-          if (this.state.sourceType === "repo") {
-            this.createGHAction(name, this.state.selectedNamespace);
-          }
-          // this.props.setCurrentView('cluster-dashboard');
-          this.setState({ saveValuesStatus: "successful" }, () => {
-            // redirect to dashboard
-          });
-          posthog.capture("Deployed template", {
-            name: this.props.currentTemplate.name,
-            namespace: this.state.selectedNamespace,
-            values: values,
-          });
+    api
+      .deployTemplate(
+        "<token>",
+        {
+          templateName: this.props.currentTemplate.name,
+          storage: StorageType.Secret,
+          formValues: values,
+          namespace: this.state.selectedNamespace,
+          name,
+        },
+        {
+          id: currentProject.id,
+          cluster_id: currentCluster.id,
+          name: this.props.currentTemplate.name.toLowerCase().trim(),
+          version: "latest",
         }
-      }
-    );
+      )
+      .then((_) => {
+        if (this.state.sourceType === "repo") {
+          this.createGHAction(name, this.state.selectedNamespace);
+        }
+        // this.props.setCurrentView('cluster-dashboard');
+        this.setState({ saveValuesStatus: "successful" }, () => {
+          // redirect to dashboard
+        });
+        posthog.capture("Deployed template", {
+          name: this.props.currentTemplate.name,
+          namespace: this.state.selectedNamespace,
+          values: values,
+        });
+      })
+      .catch((err) => {
+        this.setState({ saveValuesStatus: "error" });
+        posthog.capture("Failed to deploy template", {
+          name: this.props.currentTemplate.name,
+          namespace: this.state.selectedNamespace,
+          values: values,
+          error: err,
+        });
+      });
   };
 
   onSubmit = (rawValues: any) => {
@@ -198,47 +193,47 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
       ${currentCluster.id}\n}
     `);
 
-    api.deployTemplate(
-      "<token>",
-      {
-        templateName: this.props.currentTemplate.name,
-        imageURL: this.state.selectedImageUrl,
-        storage: StorageType.Secret,
-        formValues: values,
-        namespace: this.state.selectedNamespace,
-        name,
-      },
-      {
-        id: currentProject.id,
-        cluster_id: currentCluster.id,
-        name: this.props.currentTemplate.name.toLowerCase().trim(),
-        version: "latest",
-      },
-      (err: any, res: any) => {
-        if (err) {
-          this.setState({ saveValuesStatus: "error" });
-          posthog.capture("Failed to deploy template", {
-            name: this.props.currentTemplate.name,
-            namespace: this.state.selectedNamespace,
-            values: values,
-            error: err,
-          });
-        } else {
-          if (this.state.sourceType === "repo") {
-            this.createGHAction(name, this.state.selectedNamespace);
-          }
-          // this.props.setCurrentView('cluster-dashboard');
-          this.setState({ saveValuesStatus: "successful" }, () => {
-            // redirect to dashboard with namespace
-          });
-          posthog.capture("Deployed template", {
-            name: this.props.currentTemplate.name,
-            namespace: this.state.selectedNamespace,
-            values: values,
-          });
+    api
+      .deployTemplate(
+        "<token>",
+        {
+          templateName: this.props.currentTemplate.name,
+          imageURL: this.state.selectedImageUrl,
+          storage: StorageType.Secret,
+          formValues: values,
+          namespace: this.state.selectedNamespace,
+          name,
+        },
+        {
+          id: currentProject.id,
+          cluster_id: currentCluster.id,
+          name: this.props.currentTemplate.name.toLowerCase().trim(),
+          version: "latest",
         }
-      }
-    );
+      )
+      .then((res) => {
+        if (this.state.sourceType === "repo") {
+          this.createGHAction(name, this.state.selectedNamespace);
+        }
+        // this.props.setCurrentView('cluster-dashboard');
+        this.setState({ saveValuesStatus: "successful" }, () => {
+          // redirect to dashboard with namespace
+        });
+        posthog.capture("Deployed template", {
+          name: this.props.currentTemplate.name,
+          namespace: this.state.selectedNamespace,
+          values: values,
+        });
+      })
+      .catch((err) => {
+        this.setState({ saveValuesStatus: "error" });
+        posthog.capture("Failed to deploy template", {
+          name: this.props.currentTemplate.name,
+          namespace: this.state.selectedNamespace,
+          values: values,
+          error: err,
+        });
+      });
   };
 
   renderTabContents = () => {
@@ -292,42 +287,35 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
 
     // TODO: query with selected filter once implemented
     let { currentProject, currentCluster } = this.context;
-    api.getClusters(
-      "<token>",
-      {},
-      { id: currentProject.id },
-      (err: any, res: any) => {
-        if (err) {
-          // console.log(err)
-        } else if (res.data) {
-          let clusterOptions: { label: string; value: string }[] = [];
-          let clusterMap: { [clusterId: string]: ClusterType } = {};
-          res.data.forEach((cluster: ClusterType, i: number) => {
-            clusterOptions.push({ label: cluster.name, value: cluster.name });
-            clusterMap[cluster.name] = cluster;
-          });
-          if (res.data.length > 0) {
-            this.setState({ clusterOptions, clusterMap });
-          }
+    api.getClusters("<token>", {}, { id: currentProject.id }).then((res) => {
+      if (res.data) {
+        let clusterOptions: { label: string; value: string }[] = [];
+        let clusterMap: { [clusterId: string]: ClusterType } = {};
+        res.data.forEach((cluster: ClusterType, i: number) => {
+          clusterOptions.push({ label: cluster.name, value: cluster.name });
+          clusterMap[cluster.name] = cluster;
+        });
+        if (res.data.length > 0) {
+          this.setState({ clusterOptions, clusterMap });
         }
       }
-    );
+    });
 
     this.updateNamespaces(currentCluster.id);
   }
 
   updateNamespaces = (id: number) => {
     let { currentProject } = this.context;
-    api.getNamespaces(
-      "<token>",
-      {
-        cluster_id: id,
-      },
-      { id: currentProject.id },
-      (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-        } else if (res.data) {
+    api
+      .getNamespaces(
+        "<token>",
+        {
+          cluster_id: id,
+        },
+        { id: currentProject.id }
+      )
+      .then((res) => {
+        if (res.data) {
           let namespaceOptions = res.data.items.map(
             (x: { metadata: { name: string } }) => {
               return { label: x.metadata.name, value: x.metadata.name };
@@ -337,8 +325,8 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
             this.setState({ namespaceOptions });
           }
         }
-      }
-    );
+      })
+      .catch(console.log);
   };
 
   setSelectedImageUrl = (x: string) => {
@@ -393,7 +381,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
           </Placeholder>
           <SaveButton
             text="Deploy"
-            onClick={() => this.onSubmitAddon()}
+            onClick={this.onSubmitAddon}
             status={this.state.saveValuesStatus}
             makeFlush={true}
           />

+ 18 - 51
dashboard/src/shared/baseApi.tsx

@@ -8,12 +8,7 @@ export const baseApi = <T extends {}, S = {}>(
   requestType: string,
   endpoint: ((pathParams: S) => string) | string
 ) => {
-  return (
-    token: string,
-    params: T,
-    pathParams: S,
-    callback?: (err: any, res: any) => void
-  ) => {
+  return (token: string, params: T, pathParams: S) => {
     // Generate endpoint literal
     let endpointString: ((pathParams: S) => string) | string;
     if (typeof endpoint === "string") {
@@ -24,54 +19,26 @@ export const baseApi = <T extends {}, S = {}>(
 
     // Handle request type (can refactor)
     if (requestType === "POST") {
-      axios
-        .post(endpointString, params, {
-          headers: {
-            Authorization: `Bearer ${token}`,
-          },
-        })
-        .then((res) => {
-          callback && callback(null, res);
-        })
-        .catch((err) => {
-          callback && callback(err, null);
-        });
+      return axios.post(endpointString, params, {
+        headers: {
+          Authorization: `Bearer ${token}`,
+        },
+      });
     } else if (requestType === "PUT") {
-      axios
-        .put(endpointString, params, {
-          headers: {
-            Authorization: `Bearer ${token}`,
-          },
-        })
-        .then((res) => {
-          callback && callback(null, res);
-        })
-        .catch((err) => {
-          callback && callback(err, null);
-        });
+      return axios.put(endpointString, params, {
+        headers: {
+          Authorization: `Bearer ${token}`,
+        },
+      });
     } else if (requestType === "DELETE") {
-      axios
-        .delete(endpointString, params)
-        .then((res) => {
-          callback && callback(null, res);
-        })
-        .catch((err) => {
-          callback && callback(err, null);
-        });
+      return axios.delete(endpointString, params);
     } else {
-      axios
-        .get(endpointString, {
-          params,
-          paramsSerializer: function (params) {
-            return qs.stringify(params, { arrayFormat: "repeat" });
-          },
-        })
-        .then((res) => {
-          callback && callback(null, res);
-        })
-        .catch((err) => {
-          callback && callback(err, null);
-        });
+      return axios.get(endpointString, {
+        params,
+        paramsSerializer: function (params) {
+          return qs.stringify(params, { arrayFormat: "repeat" });
+        },
+      });
     }
   };
 };

+ 10 - 3
internal/auth/sessionstore/sessionstore.go

@@ -111,8 +111,10 @@ func (store *PGStore) save(session *sessions.Session) error {
 
 // NewStore takes an initialized db and session key pairs to create a session-store in postgres db.
 func NewStore(repo *repository.Repository, conf config.ServerConf) (*PGStore, error) {
-	keyPairs := [][]byte{
-		conf.CookieSecret,
+	keyPairs := [][]byte{}
+
+	for _, key := range conf.CookieSecrets {
+		keyPairs = append(keyPairs, []byte(key))
 	}
 
 	dbStore := &PGStore{
@@ -129,9 +131,14 @@ func NewStore(repo *repository.Repository, conf config.ServerConf) (*PGStore, er
 
 // NewFilesystemStore takes session key pairs to create a session-store in the local fs without using a db.
 func NewFilesystemStore(conf config.ServerConf) *sessions.FilesystemStore {
+	keyPairs := [][]byte{}
+
+	for _, key := range conf.CookieSecrets {
+		keyPairs = append(keyPairs, []byte(key))
+	}
 
 	// Defaults to os.TempDir() when first argument (path) isn't specified.
-	store := sessions.NewFilesystemStore("", conf.CookieSecret)
+	store := sessions.NewFilesystemStore("", keyPairs...)
 
 	return store
 }

+ 4 - 2
internal/auth/sessionstore/sessionstore_test.go

@@ -34,7 +34,7 @@ func TestPGStore(t *testing.T) {
 	repo := test.NewRepository(true)
 
 	ss, err := sessionstore.NewStore(repo, config.ServerConf{
-		CookieSecret: []byte("secret"),
+		CookieSecrets: []string{"secret"},
 	})
 
 	if err != nil {
@@ -128,13 +128,15 @@ func TestPGStore(t *testing.T) {
 	if err = ss.Save(req, headerOnlyResponseWriter(m), session); err != nil {
 		t.Fatal("Failed to save session:", err.Error())
 	}
+
+	t.Errorf("")
 }
 
 func TestSessionOptionsAreUniquePerSession(t *testing.T) {
 	repo := test.NewRepository(true)
 
 	ss, err := sessionstore.NewStore(repo, config.ServerConf{
-		CookieSecret: []byte("secret"),
+		CookieSecrets: []string{"secret"},
 	})
 
 	if err != nil {

+ 2 - 2
internal/config/config.go

@@ -22,7 +22,7 @@ type ServerConf struct {
 	Port                 int           `env:"SERVER_PORT,default=8080"`
 	StaticFilePath       string        `env:"STATIC_FILE_PATH,default=/porter/static"`
 	CookieName           string        `env:"COOKIE_NAME,default=porter"`
-	CookieSecret         []byte        `env:"COOKIE_SECRET,default=secret"`
+	CookieSecrets        []string      `env:"COOKIE_SECRETS,default=hashkey;blockkey"`
 	TokenGeneratorSecret string        `env:"TOKEN_GENERATOR_SECRET,default=secret"`
 	TimeoutRead          time.Duration `env:"SERVER_TIMEOUT_READ,default=5s"`
 	TimeoutWrite         time.Duration `env:"SERVER_TIMEOUT_WRITE,default=10s"`
@@ -66,7 +66,7 @@ func FromEnv() *Conf {
 	var c Conf
 
 	if err := envdecode.StrictDecode(&c); err != nil {
-		log.Fatalf("Failed to decode: %s", err)
+		log.Fatalf("Failed to decode server conf: %s", err)
 	}
 
 	return &c

+ 24 - 15
internal/integrations/ci/actions/actions.go

@@ -75,34 +75,35 @@ func (g *GithubActions) Setup() (string, error) {
 		return "", err
 	}
 
-	return g.commitGithubFile(client, fileBytes)
+	return g.commitGithubFile(client, g.getPorterYMLFileName(), fileBytes)
 }
 
 type GithubActionYAMLStep struct {
-	Name string `yaml:"name"`
-	ID   string `yaml:"id"`
-	Run  string `yaml:"run"`
+	Name string `yaml:"name,omitempty"`
+	ID   string `yaml:"id,omitempty"`
+	Uses string `yaml:"uses,omitempty"`
+	Run  string `yaml:"run,omitempty"`
 }
 
 type GithubActionYAMLOnPushBranches struct {
-	Branches []string `yaml:"branches"`
+	Branches []string `yaml:"branches,omitempty"`
 }
 
 type GithubActionYAMLOnPush struct {
-	Push GithubActionYAMLOnPushBranches `yaml:"push"`
+	Push GithubActionYAMLOnPushBranches `yaml:"push,omitempty"`
 }
 
 type GithubActionYAMLJob struct {
-	RunsOn string                 `yaml:"runs-on"`
-	Steps  []GithubActionYAMLStep `yaml:"steps"`
+	RunsOn string                 `yaml:"runs-on,omitempty"`
+	Steps  []GithubActionYAMLStep `yaml:"steps,omitempty"`
 }
 
 type GithubActionYAML struct {
-	On GithubActionYAMLOnPush `yaml:"on"`
+	On GithubActionYAMLOnPush `yaml:"on,omitempty"`
 
-	Name string `yaml:"name"`
+	Name string `yaml:"name,omitempty"`
 
-	Jobs map[string]GithubActionYAMLJob `yaml:"jobs"`
+	Jobs map[string]GithubActionYAMLJob `yaml:"jobs,omitempty"`
 }
 
 func (g *GithubActions) GetGithubActionYAML() ([]byte, error) {
@@ -119,6 +120,7 @@ func (g *GithubActions) GetGithubActionYAML() ([]byte, error) {
 			"porter-deploy": GithubActionYAMLJob{
 				RunsOn: "ubuntu-latest",
 				Steps: []GithubActionYAMLStep{
+					getCheckoutCodeStep(),
 					getDownloadPorterStep(),
 					getConfigurePorterStep(g.getPorterTokenSecretName()),
 					getDockerBuildPushStep(g.DockerFilePath, g.ImageRepoURL),
@@ -199,18 +201,25 @@ func (g *GithubActions) getWebhookSecretName() string {
 	)
 }
 
+func (g *GithubActions) getPorterYMLFileName() string {
+	return fmt.Sprintf("porter_%s.yml", strings.Replace(
+		strings.ToLower(g.ReleaseName), "-", "_", -1),
+	)
+}
+
 func (g *GithubActions) getPorterTokenSecretName() string {
 	return fmt.Sprintf("PORTER_TOKEN_%d", g.ProjectID)
 }
 
 func (g *GithubActions) commitGithubFile(
 	client *github.Client,
+	filename string,
 	contents []byte,
 ) (string, error) {
-	fmt.Println("GITHUB ACTION CONTENTS ARE", string(contents))
+	filepath := ".github/workflows/" + filename
 
 	opts := &github.RepositoryContentFileOptions{
-		Message: github.String("Create porter.yml file"),
+		Message: github.String(fmt.Sprintf("Create %s file", filename)),
 		Content: contents,
 		Branch:  github.String(g.defaultBranch),
 		Committer: &github.CommitAuthor{
@@ -219,11 +228,11 @@ func (g *GithubActions) commitGithubFile(
 		},
 	}
 
-	resp, _, err := client.Repositories.CreateFile(
+	resp, _, err := client.Repositories.UpdateFile(
 		context.TODO(),
 		g.GitRepoOwner,
 		g.GitRepoName,
-		".github/workflows/porter.yml",
+		filepath,
 		opts,
 	)
 

+ 7 - 0
internal/integrations/ci/actions/steps.go

@@ -2,6 +2,13 @@ package actions
 
 import "fmt"
 
+func getCheckoutCodeStep() GithubActionYAMLStep {
+	return GithubActionYAMLStep{
+		Name: "Checkout code",
+		Uses: "actions/checkout@v2.3.4",
+	}
+}
+
 const download string = `
 name=$(curl -s https://api.github.com/repos/porter-dev/porter/releases/latest | grep "browser_download_url.*/porter_.*_Linux_x86_64\.zip" | cut -d ":" -f 2,3 | tr -d \")
 name=$(basename $name)

+ 0 - 4
internal/repository/gorm/infra.go

@@ -1,8 +1,6 @@
 package gorm
 
 import (
-	"fmt"
-
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	"gorm.io/gorm"
@@ -61,8 +59,6 @@ func (repo *InfraRepository) ReadInfra(id uint) (*models.Infra, error) {
 		return nil, err
 	}
 
-	fmt.Println("INNFRA LAST APPLIED", string(infra.LastApplied))
-
 	err := repo.DecryptInfraData(infra, repo.key)
 
 	if err != nil {

+ 24 - 0
server/api/api.go

@@ -2,11 +2,14 @@ package api
 
 import (
 	"fmt"
+	"net/http"
+	"strings"
 
 	"github.com/go-playground/locales/en"
 	ut "github.com/go-playground/universal-translator"
 	vr "github.com/go-playground/validator/v10"
 	"github.com/porter-dev/porter/internal/auth/sessionstore"
+	"github.com/porter-dev/porter/internal/auth/token"
 	"github.com/porter-dev/porter/internal/oauth"
 	"golang.org/x/oauth2"
 	"gorm.io/gorm"
@@ -75,6 +78,7 @@ type App struct {
 	db         *gorm.DB
 	validator  *vr.Validate
 	translator *ut.Translator
+	tokenConf  *token.TokenGeneratorConf
 }
 
 // New returns a new App instance
@@ -142,5 +146,25 @@ func New(conf *AppConfig) (*App, error) {
 		})
 	}
 
+	app.tokenConf = &token.TokenGeneratorConf{
+		TokenSecret: conf.ServerConf.TokenGeneratorSecret,
+	}
+
 	return app, nil
 }
+
+func (app *App) getTokenFromRequest(r *http.Request) *token.Token {
+	reqToken := r.Header.Get("Authorization")
+
+	splitToken := strings.Split(reqToken, "Bearer")
+
+	if len(splitToken) != 2 {
+		return nil
+	}
+
+	reqToken = strings.TrimSpace(splitToken[1])
+
+	tok, _ := token.GetTokenFromEncoded(reqToken, app.tokenConf)
+
+	return tok
+}

+ 3 - 1
server/api/git_repo_handler.go

@@ -71,7 +71,9 @@ func (app *App) HandleListRepos(w http.ResponseWriter, r *http.Request) {
 	client := github.NewClient(app.GithubProjectConf.Client(oauth2.NoContext, tok))
 
 	// list all repositories for specified user
-	repos, _, err := client.Repositories.List(context.Background(), "", nil)
+	repos, _, err := client.Repositories.List(context.Background(), "", &github.RepositoryListOptions{
+		Sort: "updated",
+	})
 
 	if err != nil {
 		app.handleErrorInternal(err, w)

+ 8 - 7
server/api/helpers_test.go

@@ -59,13 +59,14 @@ func newTester(canQuery bool) *tester {
 	appConf := config.Conf{
 		Debug: true,
 		Server: config.ServerConf{
-			Port:         8080,
-			CookieName:   "porter",
-			CookieSecret: []byte("secret"),
-			TimeoutRead:  time.Second * 5,
-			TimeoutWrite: time.Second * 10,
-			TimeoutIdle:  time.Second * 15,
-			IsTesting:    true,
+			Port:                 8080,
+			CookieName:           "porter",
+			CookieSecrets:        []string{"secret"},
+			TimeoutRead:          time.Second * 5,
+			TimeoutWrite:         time.Second * 10,
+			TimeoutIdle:          time.Second * 15,
+			IsTesting:            true,
+			TokenGeneratorSecret: "secret",
 		},
 		// unimportant here
 		Db: config.DBConf{},

+ 1 - 1
server/api/oauth_github_handler.go

@@ -229,7 +229,7 @@ func (app *App) upsertUserFromToken(tok *oauth2.Token) (*models.User, error) {
 			return nil, err
 		}
 	} else if err != nil {
-		return nil, fmt.Errorf("unexpected error occurred:", err.Error())
+		return nil, fmt.Errorf("unexpected error occurred:%s", err.Error())
 	}
 
 	return user, nil

+ 20 - 0
server/api/user_handler.go

@@ -68,6 +68,26 @@ func (app *App) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
 
 // HandleAuthCheck checks whether current session is authenticated and returns user ID if so.
 func (app *App) HandleAuthCheck(w http.ResponseWriter, r *http.Request) {
+	// first, check for token
+	tok := app.getTokenFromRequest(r)
+
+	if tok != nil {
+		// read the user
+		user, err := app.Repo.User.ReadUser(tok.IBy)
+
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		if err := app.sendUser(w, tok.IBy, user.Email, ""); err != nil {
+			app.handleErrorFormDecoding(err, ErrUserDecode, w)
+			return
+		}
+
+		return
+	}
+
 	session, err := app.Store.Get(r, app.ServerConf.CookieName)
 
 	if err != nil {

+ 49 - 0
server/api/user_handler_test.go

@@ -2,6 +2,7 @@ package api_test
 
 import (
 	"encoding/json"
+	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"reflect"
@@ -9,6 +10,7 @@ import (
 	"testing"
 
 	"github.com/go-test/deep"
+	"github.com/porter-dev/porter/internal/auth/token"
 	"github.com/porter-dev/porter/internal/models"
 )
 
@@ -106,6 +108,53 @@ func TestHandleAuthCheck(t *testing.T) {
 	testUserRequests(t, authCheckTests, true)
 }
 
+func TestHandleAuthCheckToken(t *testing.T) {
+	tester := newTester(true)
+
+	initUserDefault(tester)
+	initProject(tester)
+
+	// generate a new token
+	tokGen, _ := token.GetTokenForAPI(1, 1)
+
+	tok, _ := tokGen.EncodeToken(&token.TokenGeneratorConf{
+		TokenSecret: "secret",
+	})
+
+	req, err := http.NewRequest(
+		"GET",
+		"/api/auth/check",
+		strings.NewReader(""),
+	)
+
+	tester.req = req
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", tok))
+	tester.execute()
+
+	rr := tester.rr
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// first, check that the status matches
+	if status := rr.Code; status != 200 {
+		t.Errorf("%s, handler returned wrong status code: got %v want %v",
+			"auth check token", status, 200)
+	}
+
+	gotBody := &models.UserExternal{}
+	expBody := &models.UserExternal{}
+
+	json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
+	json.Unmarshal([]byte(`{"id":1,"email":"belanger@getporter.dev"}`), expBody)
+
+	if !reflect.DeepEqual(gotBody, expBody) {
+		t.Errorf("%s, handler returned wrong body: got %v want %v",
+			"auth check token", gotBody, expBody)
+	}
+}
+
 var createUserTests = []*userTest{
 	&userTest{
 		msg:      "Create user",

+ 19 - 8
server/router/middleware/auth.go

@@ -131,18 +131,19 @@ type bodyDOIntegrationID struct {
 // the one stored in the session
 func (auth *Auth) DoesUserIDMatch(next http.Handler, loc IDLocation) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		var err error
+		id, err := findUserIDInRequest(r, loc)
+
 		// first check for token
 		tok := auth.getTokenFromRequest(r)
 
-		if tok != nil && tok.SubKind == token.User && auth.doesSessionMatchID(r, tok.IBy) {
+		if err != nil {
+			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+			return
+		} else if tok != nil && tok.IBy == uint(id) {
 			next.ServeHTTP(w, r)
 			return
-		}
-
-		var err error
-		id, err := findUserIDInRequest(r, loc)
-
-		if err == nil && auth.doesSessionMatchID(r, uint(id)) {
+		} else if auth.doesSessionMatchID(r, uint(id)) {
 			next.ServeHTTP(w, r)
 		} else {
 			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
@@ -657,6 +658,14 @@ func (auth *Auth) doesSessionMatchID(r *http.Request, id uint) bool {
 func (auth *Auth) isLoggedIn(w http.ResponseWriter, r *http.Request) bool {
 	// first check for Bearer token
 
+	tok := auth.getTokenFromRequest(r)
+
+	fmt.Println("CHECKED TOKEN FROM REQUEST", tok)
+
+	if tok != nil {
+		return true
+	}
+
 	session, err := auth.store.Get(r, auth.cookieName)
 	if err != nil {
 		session.Values["authenticated"] = false
@@ -683,7 +692,9 @@ func (auth *Auth) getTokenFromRequest(r *http.Request) *token.Token {
 
 	reqToken = strings.TrimSpace(splitToken[1])
 
-	tok, _ := token.GetTokenFromEncoded(reqToken, auth.tokenConf)
+	tok, err := token.GetTokenFromEncoded(reqToken, auth.tokenConf)
+
+	fmt.Printf("ERROR WAS %v\n", err)
 
 	return tok
 }

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor