Prechádzať zdrojové kódy

[POR-1503] respect ld feature flags across codebase (#3542)

jose-fully-ported 2 rokov pred
rodič
commit
a343fec558
41 zmenil súbory, kde vykonal 225 pridanie a 98 odobranie
  1. 1 1
      api/server/authz/preview_environment.go
  2. 1 1
      api/server/handlers/api_token/create.go
  3. 1 1
      api/server/handlers/api_token/get.go
  4. 1 1
      api/server/handlers/api_token/list.go
  5. 1 1
      api/server/handlers/api_token/revoke.go
  6. 4 2
      api/server/handlers/cluster/create.go
  7. 1 1
      api/server/handlers/cluster/create_candidate.go
  8. 1 1
      api/server/handlers/cluster/rename.go
  9. 1 1
      api/server/handlers/cluster/resolve_candidate.go
  10. 1 1
      api/server/handlers/cluster/update.go
  11. 2 2
      api/server/handlers/gitinstallation/get_porter_yaml.go
  12. 1 1
      api/server/handlers/porter_app/apply.go
  13. 1 1
      api/server/handlers/porter_app/create_app.go
  14. 1 1
      api/server/handlers/porter_app/get.go
  15. 1 1
      api/server/handlers/porter_app/parse_yaml.go
  16. 1 1
      api/server/handlers/porter_app/validate.go
  17. 1 1
      api/server/handlers/project/delete.go
  18. 1 1
      api/server/handlers/project_integration/create_aws.go
  19. 1 1
      api/server/handlers/project_integration/create_gcp.go
  20. 1 1
      api/server/handlers/project_integration/list_aws.go
  21. 3 3
      api/server/handlers/registry/get_token.go
  22. 1 1
      api/server/shared/config/loader/loader.go
  23. 1 1
      cmd/app/main.go
  24. 4 2
      cmd/migrate/enable_cluster_preview_envs/enable.go
  25. 12 2
      cmd/migrate/enable_cluster_preview_envs/enable_test.go
  26. 2 1
      cmd/migrate/enable_cluster_preview_envs/helpers_test.go
  27. 2 1
      cmd/migrate/keyrotate/helpers_test.go
  28. 8 1
      cmd/migrate/main.go
  29. 2 1
      cmd/migrate/populate_source_config_display_name/helpers_test.go
  30. 2 1
      cmd/migrate/startup_migrations/global_map.go
  31. 11 6
      internal/features/launch_darkly.go
  32. 3 1
      internal/kubernetes/resolver/resolver.go
  33. 112 41
      internal/models/project.go
  34. 3 3
      internal/registry/registry.go
  35. 3 2
      internal/repository/cluster.go
  36. 5 2
      internal/repository/gorm/cluster.go
  37. 4 2
      internal/repository/gorm/cluster_test.go
  38. 2 1
      internal/repository/gorm/helpers_test.go
  39. 3 0
      internal/repository/test/cluster.go
  40. 13 0
      provisioner/server/config/config.go
  41. 5 4
      provisioner/server/handlers/state/create_resource.go

+ 1 - 1
api/server/authz/preview_environment.go

@@ -38,7 +38,7 @@ func (p *PreviewEnvironmentScopedMiddleware) ServeHTTP(w http.ResponseWriter, r
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
+	if !project.GetFeatureFlag(models.PreviewEnvsEnabled, p.config.LaunchDarklyClient) {
 		apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r,
 			apierrors.NewErrForbidden(errPreviewProjectDisabled), true)
 		return

+ 1 - 1
api/server/handlers/api_token/create.go

@@ -35,7 +35,7 @@ func (p *APITokenCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
-	if !proj.APITokensEnabled {
+	if !proj.GetFeatureFlag(models.APITokensEnabled, p.Config().LaunchDarklyClient) {
 		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("api token endpoints are not enabled for this project")))
 		return
 	}

+ 1 - 1
api/server/handlers/api_token/get.go

@@ -33,7 +33,7 @@ func NewAPITokenGetHandler(
 func (p *APITokenGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
-	if !proj.APITokensEnabled {
+	if !proj.GetFeatureFlag(models.APITokensEnabled, p.Config().LaunchDarklyClient) {
 		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("api token endpoints are not enabled for this project")))
 		return
 	}

+ 1 - 1
api/server/handlers/api_token/list.go

@@ -29,7 +29,7 @@ func NewAPITokenListHandler(
 func (p *APITokenListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
-	if !proj.APITokensEnabled {
+	if !proj.GetFeatureFlag(models.APITokensEnabled, p.Config().LaunchDarklyClient) {
 		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("api token endpoints are not enabled for this project")))
 		return
 	}

+ 1 - 1
api/server/handlers/api_token/revoke.go

@@ -32,7 +32,7 @@ func NewAPITokenRevokeHandler(
 func (p *APITokenRevokeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
-	if !proj.APITokensEnabled {
+	if !proj.GetFeatureFlag(models.APITokensEnabled, p.Config().LaunchDarklyClient) {
 		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("api token endpoints are not enabled for this project")))
 		return
 	}

+ 4 - 2
api/server/handlers/cluster/create.go

@@ -11,6 +11,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/kubernetes/resolver"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
@@ -46,7 +47,7 @@ func (c *CreateClusterManualHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 		return
 	}
 
-	cluster, err = c.Repo().Cluster().CreateCluster(cluster)
+	cluster, err = c.Repo().Cluster().CreateCluster(cluster, c.Config().LaunchDarklyClient)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
@@ -117,6 +118,7 @@ func createClusterFromCandidate(
 	user *models.User,
 	candidate *models.ClusterCandidate,
 	clResolver *types.ClusterResolverAll,
+	launchDarklyClient *features.Client,
 ) (*models.Cluster, *models.ClusterCandidate, error) {
 	// we query the repo again to get the decrypted version of the cluster candidate
 	cc, err := repo.Cluster().ReadClusterCandidate(project.ID, candidate.ID)
@@ -137,7 +139,7 @@ func createClusterFromCandidate(
 		return nil, nil, err
 	}
 
-	cluster, err := cResolver.ResolveCluster(repo)
+	cluster, err := cResolver.ResolveCluster(repo, launchDarklyClient)
 	if err != nil {
 		return nil, nil, err
 	}

+ 1 - 1
api/server/handlers/cluster/create_candidate.go

@@ -67,7 +67,7 @@ func (c *CreateClusterCandidateHandler) ServeHTTP(w http.ResponseWriter, r *http
 		// automatically
 		if len(cc.Resolvers) == 0 {
 			var cluster *models.Cluster
-			cluster, cc, err = createClusterFromCandidate(c.Repo(), proj, user, cc, &types.ClusterResolverAll{})
+			cluster, cc, err = createClusterFromCandidate(c.Repo(), proj, user, cc, &types.ClusterResolverAll{}, c.Config().LaunchDarklyClient)
 
 			if err != nil {
 				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/cluster/rename.go

@@ -40,7 +40,7 @@ func (c *RenameClusterHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		cluster.VanityName = request.Name
 	}
 
-	cluster, err := c.Repo().Cluster().UpdateCluster(cluster)
+	cluster, err := c.Repo().Cluster().UpdateCluster(cluster, c.Config().LaunchDarklyClient)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return

+ 1 - 1
api/server/handlers/cluster/resolve_candidate.go

@@ -45,7 +45,7 @@ func (c *ResolveClusterCandidateHandler) ServeHTTP(w http.ResponseWriter, r *htt
 		return
 	}
 
-	cluster, cc, err := createClusterFromCandidate(c.Repo(), proj, user, cc, request)
+	cluster, cc, err := createClusterFromCandidate(c.Repo(), proj, user, cc, request, c.Config().LaunchDarklyClient)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return

+ 1 - 1
api/server/handlers/cluster/update.go

@@ -72,7 +72,7 @@ func (c *ClusterUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		cluster.Name = request.Name
 	}
 
-	cluster, err := c.Repo().Cluster().UpdateCluster(cluster)
+	cluster, err := c.Repo().Cluster().UpdateCluster(cluster, c.Config().LaunchDarklyClient)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return

+ 2 - 2
api/server/handlers/gitinstallation/get_porter_yaml.go

@@ -102,7 +102,7 @@ func (c *GithubGetPorterYamlHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 		return
 	}
 
-	if project.ValidateApplyV2 {
+	if project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) {
 		if parsed.Version != nil && *parsed.Version != "v2" {
 			err = telemetry.Error(ctx, span, nil, "porter YAML version is not supported")
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
@@ -111,7 +111,7 @@ func (c *GithubGetPorterYamlHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 	}
 
 	// backwards compatibility so that old porter yamls are no longer valid
-	if !project.ValidateApplyV2 && parsed.Version != nil {
+	if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) && parsed.Version != nil {
 		version := *parsed.Version
 		if version != "v1stack" {
 			err = telemetry.Error(ctx, span, nil, "porter YAML version is not supported")

+ 1 - 1
api/server/handlers/porter_app/apply.go

@@ -62,7 +62,7 @@ func (c *ApplyPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		telemetry.AttributeKV{Key: "cluster-id", Value: cluster.ID},
 	)
 
-	if !project.ValidateApplyV2 {
+	if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) {
 		err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled")
 		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
 		return

+ 1 - 1
api/server/handlers/porter_app/create_app.go

@@ -97,7 +97,7 @@ func (c *CreateAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.ValidateApplyV2 {
+	if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) {
 		err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled")
 		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
 		return

+ 1 - 1
api/server/handlers/porter_app/get.go

@@ -60,7 +60,7 @@ func (c *GetPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 
 	// this is a temporary fix until we figure out how to reconcile the new revisions table
 	// with dependencies on helm releases throuhg the api
-	if project.ValidateApplyV2 {
+	if project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) {
 		c.WriteResult(w, r, app.ToPorterAppType())
 		return
 	}

+ 1 - 1
api/server/handlers/porter_app/parse_yaml.go

@@ -52,7 +52,7 @@ func (c *ParsePorterYAMLToProtoHandler) ServeHTTP(w http.ResponseWriter, r *http
 
 	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
-	if !project.ValidateApplyV2 {
+	if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) {
 		err := telemetry.Error(ctx, span, nil, "project does not have apply v2 enabled")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
 		return

+ 1 - 1
api/server/handlers/porter_app/validate.go

@@ -68,7 +68,7 @@ func (c *ValidatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		telemetry.AttributeKV{Key: "cluster-id", Value: cluster.ID},
 	)
 
-	if !project.ValidateApplyV2 {
+	if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) {
 		err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled")
 		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
 		return

+ 1 - 1
api/server/handlers/project/delete.go

@@ -33,7 +33,7 @@ func (p *ProjectDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	user, _ := ctx.Value(types.UserScope).(*models.User)
 	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
-	if proj.CapiProvisionerEnabled {
+	if proj.GetFeatureFlag(models.CapiProvisionerEnabled, p.Config().LaunchDarklyClient) {
 		clusters, err := p.Config().Repo.Cluster().ListClustersByProjectID(proj.ID)
 		if err != nil {
 			p.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error finding clusters for project: %w", err)))

+ 1 - 1
api/server/handlers/project_integration/create_aws.go

@@ -56,7 +56,7 @@ func (p *CreateAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		AWSIntegration: aws.ToAWSIntegrationType(),
 	}
 
-	if project.CapiProvisionerEnabled && p.Config().EnableCAPIProvisioner {
+	if project.GetFeatureFlag(models.CapiProvisionerEnabled, p.Config().LaunchDarklyClient) && p.Config().EnableCAPIProvisioner {
 		credReq := porterv1.CreateAssumeRoleChainRequest{
 			ProjectId:       int64(project.ID),
 			SourceArn:       "arn:aws:iam::108458755588:role/CAPIManagement", // hard coded as this is the final hop for a CAPI cluster

+ 1 - 1
api/server/handlers/project_integration/create_gcp.go

@@ -43,7 +43,7 @@ func (p *CreateGCPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if project.CapiProvisionerEnabled {
+	if project.GetFeatureFlag(models.CapiProvisionerEnabled, p.Config().LaunchDarklyClient) {
 		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "capi-provisioner-enabled", Value: true})
 
 		b64Key := base64.StdEncoding.EncodeToString([]byte(request.GCPKeyData))

+ 1 - 1
api/server/handlers/project_integration/list_aws.go

@@ -40,7 +40,7 @@ func (p *ListAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	ctx := r.Context()
 	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
-	if project.CapiProvisionerEnabled {
+	if project.GetFeatureFlag(models.CapiProvisionerEnabled, p.Config().LaunchDarklyClient) {
 		dblinks, err := p.Repo().AWSAssumeRoleChainer().List(ctx, project.ID)
 		if err != nil {
 			e := fmt.Errorf("unable to find assume role chain links: %w", err)

+ 3 - 3
api/server/handlers/registry/get_token.go

@@ -47,7 +47,7 @@ func (c *RegistryGetECRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 		return
 	}
 
-	if proj.CapiProvisionerEnabled {
+	if proj.GetFeatureFlag(models.CapiProvisionerEnabled, c.Config().LaunchDarklyClient) {
 		ecrRequest := porterv1.ECRTokenForRegistryRequest{
 			ProjectId:    int64(proj.ID),
 			Region:       request.Region,
@@ -247,7 +247,7 @@ func (c *RegistryGetGARTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 		return
 	}
 
-	if proj.CapiProvisionerEnabled {
+	if proj.GetFeatureFlag(models.CapiProvisionerEnabled, c.Config().LaunchDarklyClient) {
 		regInput := connect.NewRequest(&porterv1.TokenForRegistryRequest{
 			ProjectId:   int64(proj.ID),
 			RegistryUri: regs[0].URL,
@@ -493,7 +493,7 @@ func (c *RegistryGetACRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 
 	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "registry-name", Value: matchingReg.Name})
 
-	if proj.CapiProvisionerEnabled {
+	if proj.GetFeatureFlag(models.CapiProvisionerEnabled, c.Config().LaunchDarklyClient) {
 		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "capi-provisioned", Value: true})
 
 		if c.Config().ClusterControlPlaneClient == nil {

+ 1 - 1
api/server/shared/config/loader/loader.go

@@ -243,7 +243,7 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 		sc.GithubAppSecret = append(sc.GithubAppSecret, secret...)
 	}
 
-	launchDarklyClient, err := features.GetClient(envConf)
+	launchDarklyClient, err := features.GetClient(envConf.ServerConf.LaunchDarklySDKKey)
 	if err != nil {
 		return nil, fmt.Errorf("could not create launch darkly client: %s", err)
 	}

+ 1 - 1
cmd/app/main.go

@@ -181,7 +181,7 @@ func initData(conf *config.Config) error {
 				AuthMechanism:       models.InCluster,
 				ProjectID:           1,
 				MonitorHelmReleases: true,
-			})
+			}, conf.LaunchDarklyClient)
 
 			if err != nil {
 				return err

+ 4 - 2
cmd/migrate/enable_cluster_preview_envs/enable.go

@@ -1,12 +1,14 @@
 package enable_cluster_preview_envs
 
 import (
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/models"
 	lr "github.com/porter-dev/porter/pkg/logger"
 	_gorm "gorm.io/gorm"
 )
 
-func EnableClusterPreviewEnvs(db *_gorm.DB, logger *lr.Logger) error {
+// EnableClusterPreviewEnvs enables preview environments for clusters where it is enabled for the project
+func EnableClusterPreviewEnvs(db *_gorm.DB, client *features.Client, logger *lr.Logger) error {
 	logger.Info().Msg("starting to enable preview envs for existing clusters whose parent projects have preview envs enabled")
 
 	var clusters []*models.Cluster
@@ -24,7 +26,7 @@ func EnableClusterPreviewEnvs(db *_gorm.DB, logger *lr.Logger) error {
 			continue
 		}
 
-		if project.PreviewEnvsEnabled {
+		if project.GetFeatureFlag(models.PreviewEnvsEnabled, client) {
 			c.PreviewEnvsEnabled = true
 
 			if err := db.Save(c).Error; err != nil {

+ 12 - 2
cmd/migrate/enable_cluster_preview_envs/enable_test.go

@@ -3,9 +3,19 @@ package enable_cluster_preview_envs
 import (
 	"testing"
 
+	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
+	"github.com/porter-dev/porter/internal/features"
 	lr "github.com/porter-dev/porter/pkg/logger"
 )
 
+type FeaturesTestClient struct {
+	override bool
+}
+
+func (c FeaturesTestClient) BoolVariation(key string, context ldcontext.Context, defaultVal bool) (bool, error) {
+	return c.override, nil
+}
+
 func TestEnableForProjectEnabled(t *testing.T) {
 	logger := lr.NewConsole(true)
 
@@ -20,7 +30,7 @@ func TestEnableForProjectEnabled(t *testing.T) {
 	initProjectPreviewEnabled(tester, t)
 	initCluster(tester, t)
 
-	err := EnableClusterPreviewEnvs(tester.DB, logger)
+	err := EnableClusterPreviewEnvs(tester.DB, &features.Client{Client: FeaturesTestClient{true}}, logger)
 	if err != nil {
 		t.Fatalf("%v\n", err)
 		return
@@ -51,7 +61,7 @@ func TestEnableForProjectDisabled(t *testing.T) {
 	initProjectPreviewDisabled(tester, t)
 	initCluster(tester, t)
 
-	err := EnableClusterPreviewEnvs(tester.DB, logger)
+	err := EnableClusterPreviewEnvs(tester.DB, &features.Client{Client: FeaturesTestClient{false}}, logger)
 	if err != nil {
 		t.Fatalf("%v\n", err)
 		return

+ 2 - 1
cmd/migrate/enable_cluster_preview_envs/helpers_test.go

@@ -7,6 +7,7 @@ import (
 
 	"github.com/porter-dev/porter/api/server/shared/config/env"
 	"github.com/porter-dev/porter/internal/adapter"
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/repository"
@@ -109,7 +110,7 @@ func initCluster(tester *tester, t *testing.T) {
 		},
 	}
 
-	cluster, err := tester.repo.Cluster().CreateCluster(cluster)
+	cluster, err := tester.repo.Cluster().CreateCluster(cluster, &features.Client{})
 	if err != nil {
 		t.Fatalf("%v\n", err)
 	}

+ 2 - 1
cmd/migrate/keyrotate/helpers_test.go

@@ -8,6 +8,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config/env"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/adapter"
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/repository"
@@ -377,7 +378,7 @@ func initCluster(tester *tester, t *testing.T) {
 		},
 	}
 
-	cluster, err := tester.repo.Cluster().CreateCluster(cluster)
+	cluster, err := tester.repo.Cluster().CreateCluster(cluster, &features.Client{})
 	if err != nil {
 		t.Fatalf("%v\n", err)
 	}

+ 8 - 1
cmd/migrate/main.go

@@ -11,6 +11,7 @@ import (
 	"github.com/porter-dev/porter/cmd/migrate/startup_migrations"
 
 	adapter "github.com/porter-dev/porter/internal/adapter"
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository/gorm"
 	lr "github.com/porter-dev/porter/pkg/logger"
@@ -29,6 +30,12 @@ func main() {
 		return
 	}
 
+	launchDarklyClient, err := features.GetClient(envConf.ServerConf.LaunchDarklySDKKey)
+	if err != nil {
+		logger.Fatal().Err(err).Msg("could not load launch darkly client")
+		return
+	}
+
 	db, err := adapter.New(envConf.DBConf)
 	if err != nil {
 		logger.Fatal().Err(err).Msg("could not connect to the database")
@@ -103,7 +110,7 @@ func main() {
 	if dbMigration.Version < latestMigrationVersion {
 		for ver, fn := range startup_migrations.StartupMigrations {
 			if ver > dbMigration.Version {
-				err := fn(tx, logger)
+				err := fn(tx, launchDarklyClient, logger)
 				if err != nil {
 					tx.Rollback()
 

+ 2 - 1
cmd/migrate/populate_source_config_display_name/helpers_test.go

@@ -10,6 +10,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/encryption"
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/repository"
@@ -121,7 +122,7 @@ func initCluster(tester *tester, t *testing.T) {
 		},
 	}
 
-	cluster, err := tester.repo.Cluster().CreateCluster(cluster)
+	cluster, err := tester.repo.Cluster().CreateCluster(cluster, &features.Client{})
 	if err != nil {
 		t.Fatalf("%v\n", err)
 	}

+ 2 - 1
cmd/migrate/startup_migrations/global_map.go

@@ -2,6 +2,7 @@ package startup_migrations
 
 import (
 	"github.com/porter-dev/porter/cmd/migrate/enable_cluster_preview_envs"
+	"github.com/porter-dev/porter/internal/features"
 	lr "github.com/porter-dev/porter/pkg/logger"
 	"gorm.io/gorm"
 )
@@ -9,7 +10,7 @@ import (
 // this should be incremented with every new startup migration script
 const LatestMigrationVersion uint = 1
 
-type migrationFunc func(db *gorm.DB, logger *lr.Logger) error
+type migrationFunc func(db *gorm.DB, config *features.Client, logger *lr.Logger) error
 
 var StartupMigrations = make(map[uint]migrationFunc)
 

+ 11 - 6
internal/features/launch_darkly.go

@@ -7,12 +7,17 @@ import (
 
 	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
 	ld "github.com/launchdarkly/go-server-sdk/v6"
-	"github.com/porter-dev/porter/api/server/shared/config/envloader"
 )
 
 // Client is a struct wrapper around the launchdarkly client
 type Client struct {
-	client *ld.LDClient
+	Client LDClient
+}
+
+// LDClient is an interface that allows us to mock
+// the LaunchDarkly client in tests
+type LDClient interface {
+	BoolVariation(key string, context ldcontext.Context, defaultVal bool) (bool, error)
 }
 
 // BoolVariation returns the value of a boolean feature flag for a given evaluation context.
@@ -22,15 +27,15 @@ type Client struct {
 //
 // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go
 func (c Client) BoolVariation(field string, context ldcontext.Context, defaultValue bool) (bool, error) {
-	if c.client == nil {
+	if c.Client == nil {
 		return defaultValue, errors.New("failed to participate in launchdarkly test: no client available")
 	}
-	return c.client.BoolVariation(field, context, defaultValue)
+	return c.Client.BoolVariation(field, context, defaultValue)
 }
 
 // GetClient retrieves a Client for interacting with LaunchDarkly
-func GetClient(envConf *envloader.EnvConf) (*Client, error) {
-	ldClient, err := ld.MakeClient(envConf.ServerConf.LaunchDarklySDKKey, 5*time.Second)
+func GetClient(launchDarklySDKKey string) (*Client, error) {
+	ldClient, err := ld.MakeClient(launchDarklySDKKey, 5*time.Second)
 	if err != nil {
 		return &Client{}, fmt.Errorf("failed to create new launchdarkly client: %w", err)
 	}

+ 3 - 1
internal/kubernetes/resolver/resolver.go

@@ -7,6 +7,7 @@ import (
 	"strings"
 
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
@@ -344,6 +345,7 @@ func (rcf *CandidateResolver) resolveAWS(
 // rcf.ResolveIntegration, since it relies on the previously created integration.
 func (rcf *CandidateResolver) ResolveCluster(
 	repo repository.Repository,
+	launchDarklyClient *features.Client,
 ) (*models.Cluster, error) {
 	// build a cluster from the candidate
 	cluster, err := rcf.buildCluster()
@@ -352,7 +354,7 @@ func (rcf *CandidateResolver) ResolveCluster(
 	}
 
 	// save cluster to db
-	return repo.Cluster().CreateCluster(cluster)
+	return repo.Cluster().CreateCluster(cluster, launchDarklyClient)
 }
 
 func (rcf *CandidateResolver) buildCluster() (*models.Cluster, error) {

+ 112 - 41
internal/models/project.go

@@ -11,21 +11,66 @@ import (
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 )
 
+// FeatureFlagLabel strongly types project feature flags
+type FeatureFlagLabel string
+
+const (
+	// APITokensEnabled allows users to create Bearer tokens for use with the Porter API
+	// #nosec G101 - Not actually an api token
+	APITokensEnabled FeatureFlagLabel = "api_tokens_enabled"
+
+	// AzureEnabled enables Azure Provisioning
+	AzureEnabled FeatureFlagLabel = "azure_enabled"
+
+	// CapiProvisionerEnabled enables the CAPI Provisioning flow
+	CapiProvisionerEnabled FeatureFlagLabel = "capi_provisioner_enabled"
+
+	// EnableReprovision enables the provisioning button after initial creation of the cluster
+	EnableReprovision FeatureFlagLabel = "enable_reprovision"
+
+	// FullAddOns shows all addons, not just curated
+	FullAddOns FeatureFlagLabel = "full_add_ons"
+
+	// HelmValuesEnabled shows the helm values tab for porter apps (when simplified_view_enabled=true)
+	HelmValuesEnabled FeatureFlagLabel = "helm_values_enabled"
+
+	// ManagedInfraEnabled uses terraform provisioning instead of capi
+	ManagedInfraEnabled FeatureFlagLabel = "managed_infra_enabled"
+
+	// MultiCluster allows multiple clusters in simplified view (simplified_view_enabled=true)
+	MultiCluster FeatureFlagLabel = "multi_cluster"
+
+	// PreviewEnvsEnabled allows legacy user the ability to see preview environments in sidebar (simplified_view_enabled=false)
+	PreviewEnvsEnabled FeatureFlagLabel = "preview_envs_enabled"
+
+	// RDSDatabasesEnabled allows for users to provision RDS instances within their cluster vpc
+	RDSDatabasesEnabled FeatureFlagLabel = "rds_databases_enabled"
+
+	// SimplifiedViewEnabled shows the new UI dashboard or not
+	SimplifiedViewEnabled FeatureFlagLabel = "simplified_view_enabled"
+
+	// StacksEnabled uses stack view for legacy (simplified_view_enabled=false)
+	StacksEnabled FeatureFlagLabel = "stacks_enabled"
+
+	// ValidateApplyV2 controls whether apps deploys use a porter app revision contract vs helm
+	ValidateApplyV2 FeatureFlagLabel = "validate_apply_v2"
+)
+
 // ProjectFeatureFlags keeps track of all project-related feature flags
-var ProjectFeatureFlags = map[string]bool{
-	"api_tokens_enabled":       false,
-	"azure_enabled":            false,
-	"capi_provisioner_enabled": true,
-	"enable_reprovision":       false,
-	"full_add_ons":             false,
-	"helm_values_enabled":      false,
-	"managed_infra_enabled":    false,
-	"multi_cluster":            false,
-	"preview_envs_enabled":     false,
-	"rds_databases_enabled":    false,
-	"simplified_view_enabled":  true,
-	"stacks_enabled":           false,
-	"validate_apply_v2":        false,
+var ProjectFeatureFlags = map[FeatureFlagLabel]bool{
+	APITokensEnabled:       false,
+	AzureEnabled:           false,
+	CapiProvisionerEnabled: true,
+	EnableReprovision:      false,
+	FullAddOns:             false,
+	HelmValuesEnabled:      false,
+	ManagedInfraEnabled:    false,
+	MultiCluster:           false,
+	PreviewEnvsEnabled:     false,
+	RDSDatabasesEnabled:    false,
+	SimplifiedViewEnabled:  true,
+	StacksEnabled:          false,
+	ValidateApplyV2:        false,
 }
 
 type ProjectPlan string
@@ -79,30 +124,56 @@ type Project struct {
 	AzureIntegrations  []ints.AzureIntegration  `json:"azure_integrations"`
 	GitlabIntegrations []ints.GitlabIntegration `json:"gitlab_integrations"`
 
-	PreviewEnvsEnabled     bool
-	RDSDatabasesEnabled    bool
-	ManagedInfraEnabled    bool
-	StacksEnabled          bool
-	APITokensEnabled       bool
+	// Deprecated: use p.GetFeatureFlag(PreviewEnvsEnabled, *features.Client) instead
+	PreviewEnvsEnabled bool
+
+	// Deprecated: use p.GetFeatureFlag(RDSDatabasesEnabled, *features.Client) instead
+
+	RDSDatabasesEnabled bool
+	// Deprecated: use p.GetFeatureFlag(ManagedInfraEnabled, *features.Client) instead
+
+	ManagedInfraEnabled bool
+	// Deprecated: use p.GetFeatureFlag(StacksEnabled, *features.Client) instead
+
+	StacksEnabled bool
+	// Deprecated: use p.GetFeatureFlag(APITokensEnabled, *features.Client) instead
+
+	APITokensEnabled bool
+	// Deprecated: use p.GetFeatureFlag(CapiProvisionerEnabled, *features.Client) instead
+
 	CapiProvisionerEnabled bool
-	SimplifiedViewEnabled  bool
-	AzureEnabled           bool
-	HelmValuesEnabled      bool
-	MultiCluster           bool `gorm:"default:false"`
-	FullAddOns             bool `gorm:"default:false"`
-	ValidateApplyV2        bool `gorm:"default:false"`
-	EnableReprovision      bool `gorm:"default:false"`
+	// Deprecated: use p.GetFeatureFlag(SimplifiedViewEnabled, *features.Client) instead
+
+	SimplifiedViewEnabled bool
+	// Deprecated: use p.GetFeatureFlag(AzureEnabled, *features.Client) instead
+
+	AzureEnabled bool
+	// Deprecated: use p.GetFeatureFlag(HelmValuesEnabled, *features.Client) instead
+
+	HelmValuesEnabled bool
+	// Deprecated: use p.GetFeatureFlag(MultiCluster, *features.Client) instead
+
+	MultiCluster bool `gorm:"default:false"`
+	// Deprecated: use p.GetFeatureFlag(FullAddOns, *features.Client) instead
+
+	FullAddOns bool `gorm:"default:false"`
+	// Deprecated: use p.GetFeatureFlag(ValidateApplyV2, *features.Client) instead
+
+	ValidateApplyV2 bool `gorm:"default:false"`
+	// Deprecated: use p.GetFeatureFlag(EnableReprovision, *features.Client) instead
+
+	EnableReprovision bool `gorm:"default:false"`
 }
 
 // GetFeatureFlag calls launchdarkly for the specified flag
 // and returns the configured value
-func (p *Project) GetFeatureFlag(flagName string, launchDarklyClient *features.Client) bool {
+func (p *Project) GetFeatureFlag(flagName FeatureFlagLabel, launchDarklyClient *features.Client) bool {
 	projectID := p.ID
 	projectName := p.Name
 	ldContext := getProjectContext(projectID, projectName)
 
 	defaultValue := ProjectFeatureFlags[flagName]
-	value, _ := launchDarklyClient.BoolVariation(flagName, ldContext, defaultValue)
+	value, _ := launchDarklyClient.BoolVariation(string(flagName), ldContext, defaultValue)
 	return value
 }
 
@@ -122,19 +193,19 @@ func (p *Project) ToProjectType(launchDarklyClient *features.Client) types.Proje
 		Name:  projectName,
 		Roles: roles,
 
-		PreviewEnvsEnabled:     p.GetFeatureFlag("preview_envs_enabled", launchDarklyClient),
-		RDSDatabasesEnabled:    p.GetFeatureFlag("rds_databases_enabled", launchDarklyClient),
-		ManagedInfraEnabled:    p.GetFeatureFlag("managed_infra_enabled", launchDarklyClient),
-		StacksEnabled:          p.GetFeatureFlag("stacks_enabled", launchDarklyClient),
-		APITokensEnabled:       p.GetFeatureFlag("api_tokens_enabled", launchDarklyClient),
-		CapiProvisionerEnabled: p.GetFeatureFlag("capi_provisioner_enabled", launchDarklyClient),
-		SimplifiedViewEnabled:  p.GetFeatureFlag("simplified_view_enabled", launchDarklyClient),
-		AzureEnabled:           p.GetFeatureFlag("azure_enabled", launchDarklyClient),
-		HelmValuesEnabled:      p.GetFeatureFlag("helm_values_enabled", launchDarklyClient),
-		MultiCluster:           p.GetFeatureFlag("multi_cluster", launchDarklyClient),
-		EnableReprovision:      p.GetFeatureFlag("enable_reprovision", launchDarklyClient),
-		ValidateApplyV2:        p.GetFeatureFlag("validate_apply_v2", launchDarklyClient),
-		FullAddOns:             p.GetFeatureFlag("full_add_ons", launchDarklyClient),
+		PreviewEnvsEnabled:     p.GetFeatureFlag(PreviewEnvsEnabled, launchDarklyClient),
+		RDSDatabasesEnabled:    p.GetFeatureFlag(RDSDatabasesEnabled, launchDarklyClient),
+		ManagedInfraEnabled:    p.GetFeatureFlag(ManagedInfraEnabled, launchDarklyClient),
+		StacksEnabled:          p.GetFeatureFlag(StacksEnabled, launchDarklyClient),
+		APITokensEnabled:       p.GetFeatureFlag(APITokensEnabled, launchDarklyClient),
+		CapiProvisionerEnabled: p.GetFeatureFlag(CapiProvisionerEnabled, launchDarklyClient),
+		SimplifiedViewEnabled:  p.GetFeatureFlag(SimplifiedViewEnabled, launchDarklyClient),
+		AzureEnabled:           p.GetFeatureFlag(AzureEnabled, launchDarklyClient),
+		HelmValuesEnabled:      p.GetFeatureFlag(HelmValuesEnabled, launchDarklyClient),
+		MultiCluster:           p.GetFeatureFlag(MultiCluster, launchDarklyClient),
+		EnableReprovision:      p.GetFeatureFlag(EnableReprovision, launchDarklyClient),
+		ValidateApplyV2:        p.GetFeatureFlag(ValidateApplyV2, launchDarklyClient),
+		FullAddOns:             p.GetFeatureFlag(FullAddOns, launchDarklyClient),
 	}
 }
 

+ 3 - 3
internal/registry/registry.go

@@ -154,7 +154,7 @@ func (r *Registry) ListRepositories(
 		return nil, telemetry.Error(ctx, span, err, "error getting project for repository")
 	}
 
-	if project.CapiProvisionerEnabled {
+	if project.GetFeatureFlag(models.CapiProvisionerEnabled, conf.LaunchDarklyClient) {
 		// TODO: Remove this conditional when AWS list repos is supported in CCP
 		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "registry-uri", Value: r.URL})
 
@@ -899,7 +899,7 @@ func (r *Registry) CreateRepository(
 		return telemetry.Error(ctx, span, err, "error getting project for repository")
 	}
 
-	if project.CapiProvisionerEnabled {
+	if project.GetFeatureFlag(models.CapiProvisionerEnabled, conf.LaunchDarklyClient) {
 		// no need to create repository if pushing to ACR or GAR
 		if strings.Contains(r.URL, ".azurecr.") || strings.Contains(r.URL, "-docker.pkg.dev") {
 			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "skipping-create-repo", Value: true})
@@ -1063,7 +1063,7 @@ func (r *Registry) ListImages(
 		return nil, fmt.Errorf("error getting project for repository: %w", err)
 	}
 
-	if project.CapiProvisionerEnabled {
+	if project.GetFeatureFlag(models.CapiProvisionerEnabled, conf.LaunchDarklyClient) {
 		uri := strings.TrimPrefix(r.URL, "https://")
 		splits := strings.Split(uri, ".")
 		accountID := splits[0]

+ 3 - 2
internal/repository/cluster.go

@@ -1,6 +1,7 @@
 package repository
 
 import (
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 )
@@ -13,11 +14,11 @@ type ClusterRepository interface {
 	ListClusterCandidatesByProjectID(projectID uint) ([]*models.ClusterCandidate, error)
 	UpdateClusterCandidateCreatedClusterID(id uint, createdClusterID uint) (*models.ClusterCandidate, error)
 
-	CreateCluster(cluster *models.Cluster) (*models.Cluster, error)
+	CreateCluster(cluster *models.Cluster, launchDarklyClient *features.Client) (*models.Cluster, error)
 	ReadCluster(projectID, clusterID uint) (*models.Cluster, error)
 	ReadClusterByInfraID(projectID, infraID uint) (*models.Cluster, error)
 	ListClustersByProjectID(projectID uint) ([]*models.Cluster, error)
-	UpdateCluster(cluster *models.Cluster) (*models.Cluster, error)
+	UpdateCluster(cluster *models.Cluster, launchDarklyClient *features.Client) (*models.Cluster, error)
 	UpdateClusterTokenCache(tokenCache *ints.ClusterTokenCache) (*models.Cluster, error)
 	DeleteCluster(cluster *models.Cluster) error
 }

+ 5 - 2
internal/repository/gorm/cluster.go

@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	"github.com/porter-dev/porter/internal/encryption"
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	"gorm.io/gorm"
@@ -118,6 +119,7 @@ func (repo *ClusterRepository) UpdateClusterCandidateCreatedClusterID(
 // CreateCluster creates a new cluster
 func (repo *ClusterRepository) CreateCluster(
 	cluster *models.Cluster,
+	launchDarkluClient *features.Client,
 ) (*models.Cluster, error) {
 	err := repo.EncryptClusterData(cluster, repo.key)
 	if err != nil {
@@ -130,7 +132,7 @@ func (repo *ClusterRepository) CreateCluster(
 		return nil, err
 	}
 
-	if cluster.PreviewEnvsEnabled && !project.PreviewEnvsEnabled {
+	if cluster.PreviewEnvsEnabled && !project.GetFeatureFlag(models.PreviewEnvsEnabled, launchDarkluClient) {
 		// this should only work if the corresponding project has preview environments enabled
 		cluster.PreviewEnvsEnabled = false
 	}
@@ -246,6 +248,7 @@ func (repo *ClusterRepository) ListClustersByProjectID(
 // UpdateCluster modifies an existing Cluster in the database
 func (repo *ClusterRepository) UpdateCluster(
 	cluster *models.Cluster,
+	launchDarklyClient *features.Client,
 ) (*models.Cluster, error) {
 	err := repo.EncryptClusterData(cluster, repo.key)
 	if err != nil {
@@ -260,7 +263,7 @@ func (repo *ClusterRepository) UpdateCluster(
 			return nil, fmt.Errorf("error fetching details about cluster's project: %w", err)
 		}
 
-		if !project.PreviewEnvsEnabled {
+		if !project.GetFeatureFlag(models.PreviewEnvsEnabled, launchDarklyClient) {
 			cluster.PreviewEnvsEnabled = false
 		}
 	}

+ 4 - 2
internal/repository/gorm/cluster_test.go

@@ -6,6 +6,7 @@ import (
 
 	"github.com/go-test/deep"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	orm "gorm.io/gorm"
@@ -228,7 +229,7 @@ func TestCreateCluster(t *testing.T) {
 
 	expCluster := *cluster
 
-	cluster, err := tester.repo.Cluster().CreateCluster(cluster)
+	cluster, err := tester.repo.Cluster().CreateCluster(cluster, &features.Client{})
 	if err != nil {
 		t.Fatalf("%v\n", err)
 	}
@@ -315,6 +316,7 @@ func TestUpdateCluster(t *testing.T) {
 
 	cluster, err := tester.repo.Cluster().UpdateCluster(
 		cluster,
+		&features.Client{},
 	)
 	if err != nil {
 		t.Fatalf("%v\n", err)
@@ -369,7 +371,7 @@ func TestUpdateClusterToken(t *testing.T) {
 		},
 	}
 
-	cluster, err := tester.repo.Cluster().CreateCluster(cluster)
+	cluster, err := tester.repo.Cluster().CreateCluster(cluster, &features.Client{})
 	if err != nil {
 		t.Fatalf("%v\n", err)
 	}

+ 2 - 1
internal/repository/gorm/helpers_test.go

@@ -9,6 +9,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config/env"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/adapter"
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/repository"
@@ -416,7 +417,7 @@ func initCluster(tester *tester, t *testing.T) {
 		CertificateAuthorityData: []byte("-----BEGIN"),
 	}
 
-	cluster, err := tester.repo.Cluster().CreateCluster(cluster)
+	cluster, err := tester.repo.Cluster().CreateCluster(cluster, &features.Client{})
 	if err != nil {
 		t.Fatalf("%v\n", err)
 	}

+ 3 - 0
internal/repository/test/cluster.go

@@ -3,6 +3,7 @@ package test
 import (
 	"errors"
 
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	"gorm.io/gorm"
@@ -93,6 +94,7 @@ func (repo *ClusterRepository) UpdateClusterCandidateCreatedClusterID(
 // CreateCluster creates a new servicea account
 func (repo *ClusterRepository) CreateCluster(
 	cluster *models.Cluster,
+	launchDarklyClient *features.Client,
 ) (*models.Cluster, error) {
 	if !repo.canQuery {
 		return nil, errors.New("Cannot write database")
@@ -153,6 +155,7 @@ func (repo *ClusterRepository) ListClustersByProjectID(
 // UpdateCluster modifies an existing Cluster in the database
 func (repo *ClusterRepository) UpdateCluster(
 	cluster *models.Cluster,
+	launchDarklyClient *features.Client,
 ) (*models.Cluster, error) {
 	if !repo.canQuery {
 		return nil, errors.New("Cannot write database")

+ 13 - 0
provisioner/server/config/config.go

@@ -13,6 +13,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config/env"
 	"github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/analytics"
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	klocal "github.com/porter-dev/porter/internal/kubernetes/local"
 	"github.com/porter-dev/porter/internal/oauth"
@@ -69,6 +70,9 @@ type Config struct {
 	// DOConf is the configuration for a DigitalOcean OAuth client
 	DOConf *oauth2.Config
 
+	// LaunchDarklyClient is the client for the LaunchDarkly feature flag service
+	LaunchDarklyClient *features.Client
+
 	RedisClient *redis.Client
 
 	Provisioner provisioner.Provisioner
@@ -121,6 +125,9 @@ type ProvisionerConf struct {
 
 	// Client key for segment to report provisioning events
 	SegmentClientKey string `env:"SEGMENT_CLIENT_KEY"`
+
+	// Launch Darkly SDK key
+	LaunchDarklySDKKey string `env:"LAUNCHDARKLY_SDK_KEY"`
 }
 
 type EnvConf struct {
@@ -165,6 +172,12 @@ func GetConfig(envConf *EnvConf) (*Config, error) {
 
 	res.Repo = gorm.NewRepository(db, &key, InstanceCredentialBackend)
 
+	launchDarklyClient, err := features.GetClient(envConf.LaunchDarklySDKKey)
+	if err != nil {
+		return nil, fmt.Errorf("could not create launch darkly client: %s", err)
+	}
+	res.LaunchDarklyClient = launchDarklyClient
+
 	if envConf.ProvisionerConf.SentryDSN != "" {
 		res.Alerter, err = alerter.NewSentryAlerter(envConf.ProvisionerConf.SentryDSN, envConf.ProvisionerConf.SentryEnv)
 	}

+ 5 - 4
provisioner/server/handlers/state/create_resource.go

@@ -15,6 +15,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/analytics"
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes/envgroup"
 	"github.com/porter-dev/porter/internal/models"
@@ -79,7 +80,7 @@ func (c *CreateResourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 	switch req.Kind {
 	case string(types.InfraEKS), string(types.InfraDOKS), string(types.InfraGKE), string(types.InfraAKS):
 		var cluster *models.Cluster
-		cluster, err = createCluster(c.Config, infra, operation, req.Output)
+		cluster, err = createCluster(c.Config, infra, c.Config.LaunchDarklyClient, req.Output)
 		if cluster != nil {
 			c.Config.AnalyticsClient.Track(analytics.ClusterProvisioningSuccessTrack(
 				&analytics.ClusterProvisioningSuccessTrackOpts{
@@ -197,7 +198,7 @@ func createS3Bucket(ctx context.Context, config *config.Config, infra *models.In
 	return createS3EnvGroup(ctx, config, infra, lastApplied, output)
 }
 
-func createCluster(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Cluster, error) {
+func createCluster(config *config.Config, infra *models.Infra, launchDarklyClient *features.Client, output map[string]interface{}) (*models.Cluster, error) {
 	// check for infra id being 0 as a safeguard so that all non-provisioned
 	// clusters are not matched by read
 	if infra.ID == 0 {
@@ -239,9 +240,9 @@ func createCluster(config *config.Config, infra *models.Infra, operation *models
 	cluster.Server = output["cluster_endpoint"].(string)
 	cluster.CertificateAuthorityData = caData
 	if isNotFound {
-		cluster, err = config.Repo.Cluster().CreateCluster(cluster)
+		cluster, err = config.Repo.Cluster().CreateCluster(cluster, launchDarklyClient)
 	} else {
-		cluster, err = config.Repo.Cluster().UpdateCluster(cluster)
+		cluster, err = config.Repo.Cluster().UpdateCluster(cluster, launchDarklyClient)
 	}
 	if err != nil {
 		return nil, err