Browse Source

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

jose-fully-ported 2 years ago
parent
commit
a343fec558
41 changed files with 225 additions and 98 deletions
  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)
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 	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.HandleAPIError(p.config.Logger, p.config.Alerter, w, r,
 			apierrors.NewErrForbidden(errPreviewProjectDisabled), true)
 			apierrors.NewErrForbidden(errPreviewProjectDisabled), true)
 		return
 		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)
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	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")))
 		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("api token endpoints are not enabled for this project")))
 		return
 		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) {
 func (p *APITokenGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	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")))
 		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("api token endpoints are not enabled for this project")))
 		return
 		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) {
 func (p *APITokenListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	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")))
 		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("api token endpoints are not enabled for this project")))
 		return
 		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) {
 func (p *APITokenRevokeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	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")))
 		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("api token endpoints are not enabled for this project")))
 		return
 		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/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"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/kubernetes/resolver"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/repository"
@@ -46,7 +47,7 @@ func (c *CreateClusterManualHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 		return
 		return
 	}
 	}
 
 
-	cluster, err = c.Repo().Cluster().CreateCluster(cluster)
+	cluster, err = c.Repo().Cluster().CreateCluster(cluster, c.Config().LaunchDarklyClient)
 
 
 	if err != nil {
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
@@ -117,6 +118,7 @@ func createClusterFromCandidate(
 	user *models.User,
 	user *models.User,
 	candidate *models.ClusterCandidate,
 	candidate *models.ClusterCandidate,
 	clResolver *types.ClusterResolverAll,
 	clResolver *types.ClusterResolverAll,
+	launchDarklyClient *features.Client,
 ) (*models.Cluster, *models.ClusterCandidate, error) {
 ) (*models.Cluster, *models.ClusterCandidate, error) {
 	// we query the repo again to get the decrypted version of the cluster candidate
 	// we query the repo again to get the decrypted version of the cluster candidate
 	cc, err := repo.Cluster().ReadClusterCandidate(project.ID, candidate.ID)
 	cc, err := repo.Cluster().ReadClusterCandidate(project.ID, candidate.ID)
@@ -137,7 +139,7 @@ func createClusterFromCandidate(
 		return nil, nil, err
 		return nil, nil, err
 	}
 	}
 
 
-	cluster, err := cResolver.ResolveCluster(repo)
+	cluster, err := cResolver.ResolveCluster(repo, launchDarklyClient)
 	if err != nil {
 	if err != nil {
 		return nil, nil, err
 		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
 		// automatically
 		if len(cc.Resolvers) == 0 {
 		if len(cc.Resolvers) == 0 {
 			var cluster *models.Cluster
 			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 {
 			if err != nil {
 				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 				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.VanityName = request.Name
 	}
 	}
 
 
-	cluster, err := c.Repo().Cluster().UpdateCluster(cluster)
+	cluster, err := c.Repo().Cluster().UpdateCluster(cluster, c.Config().LaunchDarklyClient)
 	if err != nil {
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return

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

@@ -45,7 +45,7 @@ func (c *ResolveClusterCandidateHandler) ServeHTTP(w http.ResponseWriter, r *htt
 		return
 		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 {
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		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.Name = request.Name
 	}
 	}
 
 
-	cluster, err := c.Repo().Cluster().UpdateCluster(cluster)
+	cluster, err := c.Repo().Cluster().UpdateCluster(cluster, c.Config().LaunchDarklyClient)
 	if err != nil {
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		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
 		return
 	}
 	}
 
 
-	if project.ValidateApplyV2 {
+	if project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) {
 		if parsed.Version != nil && *parsed.Version != "v2" {
 		if parsed.Version != nil && *parsed.Version != "v2" {
 			err = telemetry.Error(ctx, span, nil, "porter YAML version is not supported")
 			err = telemetry.Error(ctx, span, nil, "porter YAML version is not supported")
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 			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
 	// 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
 		version := *parsed.Version
 		if version != "v1stack" {
 		if version != "v1stack" {
 			err = telemetry.Error(ctx, span, nil, "porter YAML version is not supported")
 			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},
 		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")
 		err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled")
 		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
 		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
 		return
 		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)
 	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
 	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")
 		err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled")
 		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
 		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
 		return
 		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
 	// this is a temporary fix until we figure out how to reconcile the new revisions table
 	// with dependencies on helm releases throuhg the api
 	// 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())
 		c.WriteResult(w, r, app.ToPorterAppType())
 		return
 		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)
 	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")
 		err := telemetry.Error(ctx, span, nil, "project does not have apply v2 enabled")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
 		return
 		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},
 		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")
 		err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled")
 		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
 		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
 		return
 		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)
 	user, _ := ctx.Value(types.UserScope).(*models.User)
 	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
 	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)
 		clusters, err := p.Config().Repo.Cluster().ListClustersByProjectID(proj.ID)
 		if err != nil {
 		if err != nil {
 			p.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error finding clusters for project: %w", err)))
 			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(),
 		AWSIntegration: aws.ToAWSIntegrationType(),
 	}
 	}
 
 
-	if project.CapiProvisionerEnabled && p.Config().EnableCAPIProvisioner {
+	if project.GetFeatureFlag(models.CapiProvisionerEnabled, p.Config().LaunchDarklyClient) && p.Config().EnableCAPIProvisioner {
 		credReq := porterv1.CreateAssumeRoleChainRequest{
 		credReq := porterv1.CreateAssumeRoleChainRequest{
 			ProjectId:       int64(project.ID),
 			ProjectId:       int64(project.ID),
 			SourceArn:       "arn:aws:iam::108458755588:role/CAPIManagement", // hard coded as this is the final hop for a CAPI cluster
 			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
 		return
 	}
 	}
 
 
-	if project.CapiProvisionerEnabled {
+	if project.GetFeatureFlag(models.CapiProvisionerEnabled, p.Config().LaunchDarklyClient) {
 		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "capi-provisioner-enabled", Value: true})
 		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "capi-provisioner-enabled", Value: true})
 
 
 		b64Key := base64.StdEncoding.EncodeToString([]byte(request.GCPKeyData))
 		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()
 	ctx := r.Context()
 	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 	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)
 		dblinks, err := p.Repo().AWSAssumeRoleChainer().List(ctx, project.ID)
 		if err != nil {
 		if err != nil {
 			e := fmt.Errorf("unable to find assume role chain links: %w", err)
 			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
 		return
 	}
 	}
 
 
-	if proj.CapiProvisionerEnabled {
+	if proj.GetFeatureFlag(models.CapiProvisionerEnabled, c.Config().LaunchDarklyClient) {
 		ecrRequest := porterv1.ECRTokenForRegistryRequest{
 		ecrRequest := porterv1.ECRTokenForRegistryRequest{
 			ProjectId:    int64(proj.ID),
 			ProjectId:    int64(proj.ID),
 			Region:       request.Region,
 			Region:       request.Region,
@@ -247,7 +247,7 @@ func (c *RegistryGetGARTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 		return
 		return
 	}
 	}
 
 
-	if proj.CapiProvisionerEnabled {
+	if proj.GetFeatureFlag(models.CapiProvisionerEnabled, c.Config().LaunchDarklyClient) {
 		regInput := connect.NewRequest(&porterv1.TokenForRegistryRequest{
 		regInput := connect.NewRequest(&porterv1.TokenForRegistryRequest{
 			ProjectId:   int64(proj.ID),
 			ProjectId:   int64(proj.ID),
 			RegistryUri: regs[0].URL,
 			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})
 	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})
 		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "capi-provisioned", Value: true})
 
 
 		if c.Config().ClusterControlPlaneClient == nil {
 		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...)
 		sc.GithubAppSecret = append(sc.GithubAppSecret, secret...)
 	}
 	}
 
 
-	launchDarklyClient, err := features.GetClient(envConf)
+	launchDarklyClient, err := features.GetClient(envConf.ServerConf.LaunchDarklySDKKey)
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("could not create launch darkly client: %s", err)
 		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,
 				AuthMechanism:       models.InCluster,
 				ProjectID:           1,
 				ProjectID:           1,
 				MonitorHelmReleases: true,
 				MonitorHelmReleases: true,
-			})
+			}, conf.LaunchDarklyClient)
 
 
 			if err != nil {
 			if err != nil {
 				return err
 				return err

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

@@ -1,12 +1,14 @@
 package enable_cluster_preview_envs
 package enable_cluster_preview_envs
 
 
 import (
 import (
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 	lr "github.com/porter-dev/porter/pkg/logger"
 	lr "github.com/porter-dev/porter/pkg/logger"
 	_gorm "gorm.io/gorm"
 	_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")
 	logger.Info().Msg("starting to enable preview envs for existing clusters whose parent projects have preview envs enabled")
 
 
 	var clusters []*models.Cluster
 	var clusters []*models.Cluster
@@ -24,7 +26,7 @@ func EnableClusterPreviewEnvs(db *_gorm.DB, logger *lr.Logger) error {
 			continue
 			continue
 		}
 		}
 
 
-		if project.PreviewEnvsEnabled {
+		if project.GetFeatureFlag(models.PreviewEnvsEnabled, client) {
 			c.PreviewEnvsEnabled = true
 			c.PreviewEnvsEnabled = true
 
 
 			if err := db.Save(c).Error; err != nil {
 			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 (
 import (
 	"testing"
 	"testing"
 
 
+	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
+	"github.com/porter-dev/porter/internal/features"
 	lr "github.com/porter-dev/porter/pkg/logger"
 	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) {
 func TestEnableForProjectEnabled(t *testing.T) {
 	logger := lr.NewConsole(true)
 	logger := lr.NewConsole(true)
 
 
@@ -20,7 +30,7 @@ func TestEnableForProjectEnabled(t *testing.T) {
 	initProjectPreviewEnabled(tester, t)
 	initProjectPreviewEnabled(tester, t)
 	initCluster(tester, t)
 	initCluster(tester, t)
 
 
-	err := EnableClusterPreviewEnvs(tester.DB, logger)
+	err := EnableClusterPreviewEnvs(tester.DB, &features.Client{Client: FeaturesTestClient{true}}, logger)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("%v\n", err)
 		t.Fatalf("%v\n", err)
 		return
 		return
@@ -51,7 +61,7 @@ func TestEnableForProjectDisabled(t *testing.T) {
 	initProjectPreviewDisabled(tester, t)
 	initProjectPreviewDisabled(tester, t)
 	initCluster(tester, t)
 	initCluster(tester, t)
 
 
-	err := EnableClusterPreviewEnvs(tester.DB, logger)
+	err := EnableClusterPreviewEnvs(tester.DB, &features.Client{Client: FeaturesTestClient{false}}, logger)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("%v\n", err)
 		t.Fatalf("%v\n", err)
 		return
 		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/api/server/shared/config/env"
 	"github.com/porter-dev/porter/internal/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/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/repository"
 	"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 {
 	if err != nil {
 		t.Fatalf("%v\n", err)
 		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/server/shared/config/env"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/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/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/repository"
 	"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 {
 	if err != nil {
 		t.Fatalf("%v\n", err)
 		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"
 	"github.com/porter-dev/porter/cmd/migrate/startup_migrations"
 
 
 	adapter "github.com/porter-dev/porter/internal/adapter"
 	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/models"
 	"github.com/porter-dev/porter/internal/repository/gorm"
 	"github.com/porter-dev/porter/internal/repository/gorm"
 	lr "github.com/porter-dev/porter/pkg/logger"
 	lr "github.com/porter-dev/porter/pkg/logger"
@@ -29,6 +30,12 @@ func main() {
 		return
 		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)
 	db, err := adapter.New(envConf.DBConf)
 	if err != nil {
 	if err != nil {
 		logger.Fatal().Err(err).Msg("could not connect to the database")
 		logger.Fatal().Err(err).Msg("could not connect to the database")
@@ -103,7 +110,7 @@ func main() {
 	if dbMigration.Version < latestMigrationVersion {
 	if dbMigration.Version < latestMigrationVersion {
 		for ver, fn := range startup_migrations.StartupMigrations {
 		for ver, fn := range startup_migrations.StartupMigrations {
 			if ver > dbMigration.Version {
 			if ver > dbMigration.Version {
-				err := fn(tx, logger)
+				err := fn(tx, launchDarklyClient, logger)
 				if err != nil {
 				if err != nil {
 					tx.Rollback()
 					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/api/types"
 	"github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/encryption"
 	"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/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/repository"
 	"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 {
 	if err != nil {
 		t.Fatalf("%v\n", err)
 		t.Fatalf("%v\n", err)
 	}
 	}

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

@@ -2,6 +2,7 @@ package startup_migrations
 
 
 import (
 import (
 	"github.com/porter-dev/porter/cmd/migrate/enable_cluster_preview_envs"
 	"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"
 	lr "github.com/porter-dev/porter/pkg/logger"
 	"gorm.io/gorm"
 	"gorm.io/gorm"
 )
 )
@@ -9,7 +10,7 @@ import (
 // this should be incremented with every new startup migration script
 // this should be incremented with every new startup migration script
 const LatestMigrationVersion uint = 1
 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)
 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"
 	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
 	ld "github.com/launchdarkly/go-server-sdk/v6"
 	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
 // Client is a struct wrapper around the launchdarkly client
 type Client struct {
 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.
 // 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
 // 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) {
 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 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
 // 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 {
 	if err != nil {
 		return &Client{}, fmt.Errorf("failed to create new launchdarkly client: %w", err)
 		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"
 	"strings"
 
 
 	"github.com/porter-dev/porter/api/types"
 	"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/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	"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.
 // rcf.ResolveIntegration, since it relies on the previously created integration.
 func (rcf *CandidateResolver) ResolveCluster(
 func (rcf *CandidateResolver) ResolveCluster(
 	repo repository.Repository,
 	repo repository.Repository,
+	launchDarklyClient *features.Client,
 ) (*models.Cluster, error) {
 ) (*models.Cluster, error) {
 	// build a cluster from the candidate
 	// build a cluster from the candidate
 	cluster, err := rcf.buildCluster()
 	cluster, err := rcf.buildCluster()
@@ -352,7 +354,7 @@ func (rcf *CandidateResolver) ResolveCluster(
 	}
 	}
 
 
 	// save cluster to db
 	// save cluster to db
-	return repo.Cluster().CreateCluster(cluster)
+	return repo.Cluster().CreateCluster(cluster, launchDarklyClient)
 }
 }
 
 
 func (rcf *CandidateResolver) buildCluster() (*models.Cluster, error) {
 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"
 	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
 // 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
 type ProjectPlan string
@@ -79,30 +124,56 @@ type Project struct {
 	AzureIntegrations  []ints.AzureIntegration  `json:"azure_integrations"`
 	AzureIntegrations  []ints.AzureIntegration  `json:"azure_integrations"`
 	GitlabIntegrations []ints.GitlabIntegration `json:"gitlab_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
 	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
 // GetFeatureFlag calls launchdarkly for the specified flag
 // and returns the configured value
 // 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
 	projectID := p.ID
 	projectName := p.Name
 	projectName := p.Name
 	ldContext := getProjectContext(projectID, projectName)
 	ldContext := getProjectContext(projectID, projectName)
 
 
 	defaultValue := ProjectFeatureFlags[flagName]
 	defaultValue := ProjectFeatureFlags[flagName]
-	value, _ := launchDarklyClient.BoolVariation(flagName, ldContext, defaultValue)
+	value, _ := launchDarklyClient.BoolVariation(string(flagName), ldContext, defaultValue)
 	return value
 	return value
 }
 }
 
 
@@ -122,19 +193,19 @@ func (p *Project) ToProjectType(launchDarklyClient *features.Client) types.Proje
 		Name:  projectName,
 		Name:  projectName,
 		Roles: roles,
 		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")
 		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
 		// TODO: Remove this conditional when AWS list repos is supported in CCP
 		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "registry-uri", Value: r.URL})
 		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")
 		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
 		// no need to create repository if pushing to ACR or GAR
 		if strings.Contains(r.URL, ".azurecr.") || strings.Contains(r.URL, "-docker.pkg.dev") {
 		if strings.Contains(r.URL, ".azurecr.") || strings.Contains(r.URL, "-docker.pkg.dev") {
 			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "skipping-create-repo", Value: true})
 			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)
 		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://")
 		uri := strings.TrimPrefix(r.URL, "https://")
 		splits := strings.Split(uri, ".")
 		splits := strings.Split(uri, ".")
 		accountID := splits[0]
 		accountID := splits[0]

+ 3 - 2
internal/repository/cluster.go

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

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

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

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

@@ -6,6 +6,7 @@ import (
 
 
 	"github.com/go-test/deep"
 	"github.com/go-test/deep"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	orm "gorm.io/gorm"
 	orm "gorm.io/gorm"
@@ -228,7 +229,7 @@ func TestCreateCluster(t *testing.T) {
 
 
 	expCluster := *cluster
 	expCluster := *cluster
 
 
-	cluster, err := tester.repo.Cluster().CreateCluster(cluster)
+	cluster, err := tester.repo.Cluster().CreateCluster(cluster, &features.Client{})
 	if err != nil {
 	if err != nil {
 		t.Fatalf("%v\n", err)
 		t.Fatalf("%v\n", err)
 	}
 	}
@@ -315,6 +316,7 @@ func TestUpdateCluster(t *testing.T) {
 
 
 	cluster, err := tester.repo.Cluster().UpdateCluster(
 	cluster, err := tester.repo.Cluster().UpdateCluster(
 		cluster,
 		cluster,
+		&features.Client{},
 	)
 	)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("%v\n", err)
 		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 {
 	if err != nil {
 		t.Fatalf("%v\n", err)
 		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/server/shared/config/env"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/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/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/repository"
@@ -416,7 +417,7 @@ func initCluster(tester *tester, t *testing.T) {
 		CertificateAuthorityData: []byte("-----BEGIN"),
 		CertificateAuthorityData: []byte("-----BEGIN"),
 	}
 	}
 
 
-	cluster, err := tester.repo.Cluster().CreateCluster(cluster)
+	cluster, err := tester.repo.Cluster().CreateCluster(cluster, &features.Client{})
 	if err != nil {
 	if err != nil {
 		t.Fatalf("%v\n", err)
 		t.Fatalf("%v\n", err)
 	}
 	}

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

@@ -3,6 +3,7 @@ package test
 import (
 import (
 	"errors"
 	"errors"
 
 
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/repository"
 	"gorm.io/gorm"
 	"gorm.io/gorm"
@@ -93,6 +94,7 @@ func (repo *ClusterRepository) UpdateClusterCandidateCreatedClusterID(
 // CreateCluster creates a new servicea account
 // CreateCluster creates a new servicea account
 func (repo *ClusterRepository) CreateCluster(
 func (repo *ClusterRepository) CreateCluster(
 	cluster *models.Cluster,
 	cluster *models.Cluster,
+	launchDarklyClient *features.Client,
 ) (*models.Cluster, error) {
 ) (*models.Cluster, error) {
 	if !repo.canQuery {
 	if !repo.canQuery {
 		return nil, errors.New("Cannot write database")
 		return nil, errors.New("Cannot write database")
@@ -153,6 +155,7 @@ func (repo *ClusterRepository) ListClustersByProjectID(
 // UpdateCluster modifies an existing Cluster in the database
 // UpdateCluster modifies an existing Cluster in the database
 func (repo *ClusterRepository) UpdateCluster(
 func (repo *ClusterRepository) UpdateCluster(
 	cluster *models.Cluster,
 	cluster *models.Cluster,
+	launchDarklyClient *features.Client,
 ) (*models.Cluster, error) {
 ) (*models.Cluster, error) {
 	if !repo.canQuery {
 	if !repo.canQuery {
 		return nil, errors.New("Cannot write database")
 		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/api/server/shared/config/env"
 	"github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/analytics"
 	"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"
 	klocal "github.com/porter-dev/porter/internal/kubernetes/local"
 	klocal "github.com/porter-dev/porter/internal/kubernetes/local"
 	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/oauth"
@@ -69,6 +70,9 @@ type Config struct {
 	// DOConf is the configuration for a DigitalOcean OAuth client
 	// DOConf is the configuration for a DigitalOcean OAuth client
 	DOConf *oauth2.Config
 	DOConf *oauth2.Config
 
 
+	// LaunchDarklyClient is the client for the LaunchDarkly feature flag service
+	LaunchDarklyClient *features.Client
+
 	RedisClient *redis.Client
 	RedisClient *redis.Client
 
 
 	Provisioner provisioner.Provisioner
 	Provisioner provisioner.Provisioner
@@ -121,6 +125,9 @@ type ProvisionerConf struct {
 
 
 	// Client key for segment to report provisioning events
 	// Client key for segment to report provisioning events
 	SegmentClientKey string `env:"SEGMENT_CLIENT_KEY"`
 	SegmentClientKey string `env:"SEGMENT_CLIENT_KEY"`
+
+	// Launch Darkly SDK key
+	LaunchDarklySDKKey string `env:"LAUNCHDARKLY_SDK_KEY"`
 }
 }
 
 
 type EnvConf struct {
 type EnvConf struct {
@@ -165,6 +172,12 @@ func GetConfig(envConf *EnvConf) (*Config, error) {
 
 
 	res.Repo = gorm.NewRepository(db, &key, InstanceCredentialBackend)
 	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 != "" {
 	if envConf.ProvisionerConf.SentryDSN != "" {
 		res.Alerter, err = alerter.NewSentryAlerter(envConf.ProvisionerConf.SentryDSN, envConf.ProvisionerConf.SentryEnv)
 		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/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/analytics"
 	"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"
 	"github.com/porter-dev/porter/internal/kubernetes/envgroup"
 	"github.com/porter-dev/porter/internal/kubernetes/envgroup"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
@@ -79,7 +80,7 @@ func (c *CreateResourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 	switch req.Kind {
 	switch req.Kind {
 	case string(types.InfraEKS), string(types.InfraDOKS), string(types.InfraGKE), string(types.InfraAKS):
 	case string(types.InfraEKS), string(types.InfraDOKS), string(types.InfraGKE), string(types.InfraAKS):
 		var cluster *models.Cluster
 		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 {
 		if cluster != nil {
 			c.Config.AnalyticsClient.Track(analytics.ClusterProvisioningSuccessTrack(
 			c.Config.AnalyticsClient.Track(analytics.ClusterProvisioningSuccessTrack(
 				&analytics.ClusterProvisioningSuccessTrackOpts{
 				&analytics.ClusterProvisioningSuccessTrackOpts{
@@ -197,7 +198,7 @@ func createS3Bucket(ctx context.Context, config *config.Config, infra *models.In
 	return createS3EnvGroup(ctx, config, infra, lastApplied, output)
 	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
 	// check for infra id being 0 as a safeguard so that all non-provisioned
 	// clusters are not matched by read
 	// clusters are not matched by read
 	if infra.ID == 0 {
 	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.Server = output["cluster_endpoint"].(string)
 	cluster.CertificateAuthorityData = caData
 	cluster.CertificateAuthorityData = caData
 	if isNotFound {
 	if isNotFound {
-		cluster, err = config.Repo.Cluster().CreateCluster(cluster)
+		cluster, err = config.Repo.Cluster().CreateCluster(cluster, launchDarklyClient)
 	} else {
 	} else {
-		cluster, err = config.Repo.Cluster().UpdateCluster(cluster)
+		cluster, err = config.Repo.Cluster().UpdateCluster(cluster, launchDarklyClient)
 	}
 	}
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err