Jelajahi Sumber

Merge pull request #2315 from porter-dev/master

Stacks updates -> staging
abelanger5 3 tahun lalu
induk
melakukan
7bc868b904

+ 3 - 11
api/server/handlers/stack/create.go

@@ -261,21 +261,13 @@ func getSourceConfigModels(sourceConfigs []*types.CreateStackSourceConfigRequest
 				return nil, err
 				return nil, err
 			}
 			}
 
 
-			newSourceConfig := &models.StackSourceConfig{
+			res = append(res, models.StackSourceConfig{
 				UID:          uid,
 				UID:          uid,
+				DisplayName:  sourceConfig.DisplayName,
 				Name:         sourceConfig.Name,
 				Name:         sourceConfig.Name,
 				ImageRepoURI: sourceConfig.ImageRepoURI,
 				ImageRepoURI: sourceConfig.ImageRepoURI,
 				ImageTag:     sourceConfig.ImageTag,
 				ImageTag:     sourceConfig.ImageTag,
-			}
-
-			// If the source config had a source config ID then we need to copy it over
-			if sourceConfig.StableSourceConfigID != "" {
-				newSourceConfig.StableSourceConfigID = sourceConfig.StableSourceConfigID
-			} else {
-				newSourceConfig.StableSourceConfigID = string(uid)
-			}
-
-			res = append(res, *newSourceConfig)
+			})
 		}
 		}
 	}
 	}
 
 

+ 7 - 6
api/types/stacks.go

@@ -212,9 +212,12 @@ type StackSourceConfig struct {
 	// The numerical revision id that this source config belongs to
 	// The numerical revision id that this source config belongs to
 	StackRevisionID uint `json:"stack_revision_id"`
 	StackRevisionID uint `json:"stack_revision_id"`
 
 
-	// The display name of the stack source
+	// Unique name for the source config
 	Name string `json:"name"`
 	Name string `json:"name"`
 
 
+	// Display name for the stack source
+	DisplayName string `json:"display_name"`
+
 	// The unique id of the stack source config
 	// The unique id of the stack source config
 	ID string `json:"id"`
 	ID string `json:"id"`
 
 
@@ -226,9 +229,6 @@ type StackSourceConfig struct {
 
 
 	// If this field is empty, the resource is deployed directly from the image repo uri
 	// If this field is empty, the resource is deployed directly from the image repo uri
 	StackSourceConfigBuild *StackSourceConfigBuild `json:"build,omitempty"`
 	StackSourceConfigBuild *StackSourceConfigBuild `json:"build,omitempty"`
-
-	// Unique ID to identify between revisions
-	StableSourceConfigID string `json:"stable_source_config_id"`
 }
 }
 
 
 // swagger:model
 // swagger:model
@@ -253,6 +253,9 @@ type CreateStackEnvGroupRequest struct {
 
 
 // swagger:model
 // swagger:model
 type CreateStackSourceConfigRequest struct {
 type CreateStackSourceConfigRequest struct {
+	// required: true
+	DisplayName string `json:"display_name" form:"required"`
+
 	// required: true
 	// required: true
 	Name string `json:"name" form:"required"`
 	Name string `json:"name" form:"required"`
 
 
@@ -262,8 +265,6 @@ type CreateStackSourceConfigRequest struct {
 	// required: true
 	// required: true
 	ImageTag string `json:"image_tag" form:"required"`
 	ImageTag string `json:"image_tag" form:"required"`
 
 
-	StableSourceConfigID string `json:"source_config_id,omitempty"`
-
 	// If this field is empty, the resource is deployed directly from the image repo uri
 	// If this field is empty, the resource is deployed directly from the image repo uri
 	StackSourceConfigBuild *StackSourceConfigBuild `json:"build,omitempty"`
 	StackSourceConfigBuild *StackSourceConfigBuild `json:"build,omitempty"`
 }
 }

+ 66 - 3
cli/cmd/deploy.go

@@ -14,6 +14,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/deploy"
 	"github.com/porter-dev/porter/cli/cmd/deploy"
+	"github.com/porter-dev/porter/cli/cmd/docker"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	templaterUtils "github.com/porter-dev/porter/internal/templater/utils"
 	templaterUtils "github.com/porter-dev/porter/internal/templater/utils"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
@@ -148,10 +149,19 @@ for the application:
 
 
 var updatePushCmd = &cobra.Command{
 var updatePushCmd = &cobra.Command{
 	Use:   "push",
 	Use:   "push",
-	Short: "Pushes a new image for an application specified by the --app flag.",
+	Short: "Pushes an image to a Docker registry linked to your Porter project.",
+	Args:  cobra.MaximumNArgs(1),
 	Long: fmt.Sprintf(`
 	Long: fmt.Sprintf(`
 %s
 %s
 
 
+Pushes a local Docker image to a registry linked to your Porter project. This command
+requires the project ID to be set either by using the %s command
+or the --project flag. For example, to push a local nginx image:
+
+  %s
+
+%s
+
 Pushes a new image for an application specified by the --app flag. This command uses
 Pushes a new image for an application specified by the --app flag. This command uses
 the image repository saved in the application config by default. For example, if an
 the image repository saved in the application config by default. For example, if an
 application "nginx" was created from the image repo "gcr.io/snowflake-123456/nginx",
 application "nginx" was created from the image repo "gcr.io/snowflake-123456/nginx",
@@ -164,6 +174,9 @@ are using an image registry that was created outside of Porter, make sure that y
 linked it via "porter connect".
 linked it via "porter connect".
 `,
 `,
 		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update push\":"),
 		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update push\":"),
+		color.New(color.FgBlue).Sprintf("porter config set-project"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter update push gcr.io/snowflake-123456/nginx:1234567"),
+		color.New(color.Bold).Sprintf("LEGACY USAGE:"),
 		color.New(color.FgGreen, color.Bold).Sprintf("porter update push --app nginx --tag new-tag"),
 		color.New(color.FgGreen, color.Bold).Sprintf("porter update push --app nginx --tag new-tag"),
 	),
 	),
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
@@ -369,8 +382,6 @@ func init() {
 
 
 	updateBuildCmd.MarkPersistentFlagRequired("app")
 	updateBuildCmd.MarkPersistentFlagRequired("app")
 
 
-	updatePushCmd.MarkPersistentFlagRequired("app")
-
 	updateConfigCmd.MarkPersistentFlagRequired("app")
 	updateConfigCmd.MarkPersistentFlagRequired("app")
 
 
 	updateEnvGroupCmd.PersistentFlags().StringVar(
 	updateEnvGroupCmd.PersistentFlags().StringVar(
@@ -490,6 +501,58 @@ func updateBuild(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 }
 }
 
 
 func updatePush(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
 func updatePush(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+	if app == "" {
+		if len(args) == 0 {
+			return fmt.Errorf("please provide the docker image name")
+		}
+
+		image := args[0]
+
+		registries, err := client.ListRegistries(context.Background(), cliConf.Project)
+
+		if err != nil {
+			return err
+		}
+
+		regs := *registries
+		regID := uint(0)
+
+		for _, reg := range regs {
+			if strings.Contains(image, reg.URL) {
+				regID = reg.ID
+				break
+			}
+		}
+
+		if regID == 0 {
+			return fmt.Errorf("could not find registry for image: %s", image)
+		}
+
+		err = client.CreateRepository(context.Background(), cliConf.Project, regID,
+			&types.CreateRegistryRepositoryRequest{
+				ImageRepoURI: strings.Split(image, ":")[0],
+			},
+		)
+
+		if err != nil {
+			return err
+		}
+
+		agent, err := docker.NewAgentWithAuthGetter(client, cliConf.Project)
+
+		if err != nil {
+			return err
+		}
+
+		err = agent.PushImage(image)
+
+		if err != nil {
+			return err
+		}
+
+		return nil
+	}
+
 	updateAgent, err := updateGetAgent(client)
 	updateAgent, err := updateGetAgent(client)
 
 
 	if err != nil {
 	if err != nil {

+ 28 - 0
cmd/migrate/main.go

@@ -5,6 +5,7 @@ import (
 
 
 	"github.com/porter-dev/porter/api/server/shared/config/envloader"
 	"github.com/porter-dev/porter/api/server/shared/config/envloader"
 	"github.com/porter-dev/porter/cmd/migrate/keyrotate"
 	"github.com/porter-dev/porter/cmd/migrate/keyrotate"
+	"github.com/porter-dev/porter/cmd/migrate/populate_source_config_display_name"
 
 
 	adapter "github.com/porter-dev/porter/internal/adapter"
 	adapter "github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/repository/gorm"
 	"github.com/porter-dev/porter/internal/repository/gorm"
@@ -61,6 +62,14 @@ func main() {
 		}
 		}
 	}
 	}
 
 
+	if shouldPopulateSourceConfigDisplayName() {
+		err := populate_source_config_display_name.PopulateSourceConfigDisplayName(db, logger)
+
+		if err != nil {
+			logger.Fatal().Err(err).Msg("failed to populate source config display name")
+		}
+	}
+
 	if err := InstanceMigrate(db, envConf.DBConf); err != nil {
 	if err := InstanceMigrate(db, envConf.DBConf); err != nil {
 		logger.Fatal().Err(err).Msg("vault migration failed")
 		logger.Fatal().Err(err).Msg("vault migration failed")
 	}
 	}
@@ -83,3 +92,22 @@ func shouldKeyRotate() (bool, string, string) {
 
 
 	return c.OldEncryptionKey != "" && c.NewEncryptionKey != "", c.OldEncryptionKey, c.NewEncryptionKey
 	return c.OldEncryptionKey != "" && c.NewEncryptionKey != "", c.OldEncryptionKey, c.NewEncryptionKey
 }
 }
+
+type PopulateSourceConfigDisplayNameConf struct {
+	// we add a dummy field to avoid empty struct issue with envdecode
+	DummyField string `env:"ASDF,default=asdf"`
+
+	// if true, will populate the display name for all source configs
+	PopulateSourceConfigDisplayName bool `env:"POPULATE_SOURCE_CONFIG_DISPLAY_NAME"`
+}
+
+func shouldPopulateSourceConfigDisplayName() bool {
+	var c PopulateSourceConfigDisplayNameConf
+
+	if err := envdecode.StrictDecode(&c); err != nil {
+		log.Fatalf("Failed to decode migration conf: %s", err)
+		return false
+	}
+
+	return c.PopulateSourceConfigDisplayName
+}

+ 365 - 0
cmd/migrate/populate_source_config_display_name/helpers_test.go

@@ -0,0 +1,365 @@
+package populate_source_config_display_name_test
+
+import (
+	"fmt"
+	"os"
+	"testing"
+	"time"
+
+	"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/encryption"
+	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/repository"
+	"github.com/porter-dev/porter/internal/repository/gorm"
+	_gorm "gorm.io/gorm"
+)
+
+type tester struct {
+	Key *[32]byte
+	DB  *_gorm.DB
+
+	repo         repository.Repository
+	dbFileName   string
+	key          *[32]byte
+	initUsers    []*models.User
+	initProjects []*models.Project
+	initClusters []*models.Cluster
+	initKIs      []*ints.KubeIntegration
+	initStacks   []*models.Stack
+}
+
+func setupTestEnv(tester *tester, t *testing.T) {
+	t.Helper()
+
+	db, err := adapter.New(&env.DBConf{
+		EncryptionKey: "__random_strong_encryption_key__",
+		SQLLite:       true,
+		SQLLitePath:   tester.dbFileName,
+	})
+
+	if err != nil {
+		t.Fatalf("%\n", err)
+	}
+
+	err = db.AutoMigrate(
+		&models.Project{},
+		&models.User{},
+		&models.Cluster{},
+		&models.Stack{},
+		&models.StackEnvGroup{},
+		&models.StackSourceConfig{},
+		&models.StackRevision{},
+		&models.StackResource{},
+		&ints.KubeIntegration{},
+		&ints.ClusterTokenCache{},
+	)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	var key [32]byte
+
+	for i, b := range []byte("__random_strong_encryption_key__") {
+		key[i] = b
+	}
+
+	tester.key = &key
+	tester.Key = &key
+	tester.DB = db
+
+	tester.repo = gorm.NewRepository(db, &key, nil)
+}
+
+func cleanup(tester *tester, t *testing.T) {
+	t.Helper()
+
+	// remove the created file file
+	os.Remove(tester.dbFileName)
+}
+
+func initUser(tester *tester, t *testing.T) {
+	t.Helper()
+
+	user := &models.User{
+		Email:    "example@example.com",
+		Password: "hello1234",
+	}
+
+	user, err := tester.repo.User().CreateUser(user)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initUsers = append(tester.initUsers, user)
+}
+
+func initCluster(tester *tester, t *testing.T) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	if len(tester.initKIs) == 0 {
+		initKubeIntegration(tester, t)
+	}
+
+	cluster := &models.Cluster{
+		ProjectID:                tester.initProjects[0].ID,
+		Name:                     "cluster-test",
+		Server:                   "https://localhost",
+		KubeIntegrationID:        tester.initKIs[0].ID,
+		CertificateAuthorityData: []byte("-----BEGIN"),
+		TokenCache: ints.ClusterTokenCache{
+			TokenCache: ints.TokenCache{
+				Token:  []byte("token-1"),
+				Expiry: time.Now().Add(-1 * time.Hour),
+			},
+		},
+	}
+
+	cluster, err := tester.repo.Cluster().CreateCluster(cluster)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initClusters = append(tester.initClusters, cluster)
+}
+
+func initProject(tester *tester, t *testing.T) {
+	t.Helper()
+
+	proj := &models.Project{
+		Name: "project-test",
+	}
+
+	proj, err := tester.repo.Project().CreateProject(proj)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initProjects = append(tester.initProjects, proj)
+}
+
+func initKubeIntegration(tester *tester, t *testing.T) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	if len(tester.initUsers) == 0 {
+		initUser(tester, t)
+	}
+
+	ki := &ints.KubeIntegration{
+		Mechanism:             ints.KubeLocal,
+		ProjectID:             tester.initProjects[0].ID,
+		UserID:                tester.initUsers[0].ID,
+		Kubeconfig:            []byte("current-context: testing\n"),
+		ClientCertificateData: []byte("clientcertdata"),
+		ClientKeyData:         []byte("clientkeydata"),
+		Token:                 []byte("token"),
+		Username:              []byte("username"),
+		Password:              []byte("password"),
+	}
+
+	ki, err := tester.repo.KubeIntegration().CreateKubeIntegration(ki)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initKIs = append(tester.initKIs, ki)
+}
+
+func initEmptyStack(tester *tester, t *testing.T, stackName string) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	if len(tester.initUsers) == 0 {
+		initUser(tester, t)
+	}
+
+	if len(tester.initClusters) == 0 {
+		initCluster(tester, t)
+	}
+
+	uid, _ := encryption.GenerateRandomBytes(16)
+
+	// write stack to the database with creating status
+	stack := &models.Stack{
+		ProjectID: tester.initProjects[0].ID,
+		ClusterID: tester.initClusters[0].ID,
+		Namespace: "test-namespace",
+		Name:      stackName,
+		UID:       uid,
+		Revisions: []models.StackRevision{
+			{
+				RevisionNumber: 1,
+				Status:         string(types.StackRevisionStatusDeploying),
+				SourceConfigs:  []models.StackSourceConfig{},
+			},
+		},
+	}
+
+	newStack, err := tester.repo.Stack().CreateStack(stack)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initStacks = append(tester.initStacks, newStack)
+}
+
+func initStack(tester *tester, t *testing.T, stackName string) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	if len(tester.initUsers) == 0 {
+		initUser(tester, t)
+	}
+
+	if len(tester.initClusters) == 0 {
+		initCluster(tester, t)
+	}
+
+	uid, _ := encryption.GenerateRandomBytes(16)
+
+	sourceConfigs := []models.StackSourceConfig{
+		{
+			Name:         "source-config-1",
+			ImageRepoURI: "some-repo",
+			ImageTag:     "some-tag",
+			UID:          uid,
+		},
+	}
+
+	// write stack to the database with creating status
+	stack := &models.Stack{
+		ProjectID: tester.initProjects[0].ID,
+		ClusterID: tester.initClusters[0].ID,
+		Namespace: "test-namespace",
+		Name:      stackName,
+		UID:       uid,
+		Revisions: []models.StackRevision{
+			{
+				RevisionNumber: 1,
+				Status:         string(types.StackRevisionStatusDeploying),
+				SourceConfigs:  sourceConfigs,
+			},
+		},
+	}
+
+	newStack, err := tester.repo.Stack().CreateStack(stack)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initStacks = append(tester.initStacks, newStack)
+}
+
+func createNewStackRevision(tester *tester, t *testing.T, stackName string) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+	if len(tester.initUsers) == 0 {
+		initUser(tester, t)
+	}
+	if len(tester.initClusters) == 0 {
+		initCluster(tester, t)
+	}
+	if len(tester.initStacks) == 0 {
+		initStack(tester, t, stackName)
+	}
+
+	stack := tester.initStacks[0]
+
+	for _, s := range tester.initStacks {
+		if s.Name == stackName {
+			stack = s
+			break
+		}
+	}
+
+	prevRevision := findLatestRevisionByRevisionNumber(t, stack.Revisions)
+
+	oldSourceConfig := prevRevision.SourceConfigs[0]
+
+	newUid, _ := encryption.GenerateRandomBytes(16)
+	sourceConfigs := []models.StackSourceConfig{
+		{
+			Name:         oldSourceConfig.Name,
+			ImageRepoURI: "some-repo-" + fmt.Sprint(prevRevision.RevisionNumber+1),
+			ImageTag:     "some-tag-" + fmt.Sprint(prevRevision.RevisionNumber+1),
+			UID:          newUid,
+		},
+	}
+
+	newRevision := models.StackRevision{
+		RevisionNumber: prevRevision.RevisionNumber + 1,
+		Status:         string(types.StackRevisionStatusDeploying),
+		SourceConfigs:  sourceConfigs,
+		StackID:        stack.ID,
+	}
+
+	tester.repo.Stack().AppendNewRevision(&newRevision)
+}
+
+func findLatestRevisionByRevisionNumber(t *testing.T, revisions []models.StackRevision) *models.StackRevision {
+	t.Helper()
+
+	latestRevision := revisions[0]
+	for _, revision := range revisions {
+		if revision.RevisionNumber > latestRevision.RevisionNumber {
+			latestRevision = revision
+		}
+	}
+
+	return &latestRevision
+}
+
+func appendNewSourceConfig(t *testing.T, tester *tester, stack *models.Stack, sourceConfig models.StackSourceConfig) {
+	t.Helper()
+
+	prevRevision := findLatestRevisionByRevisionNumber(t, stack.Revisions)
+
+	previousSourceConfigs := []models.StackSourceConfig{}
+
+	for _, sourceConfig := range prevRevision.SourceConfigs {
+		newUid, _ := encryption.GenerateRandomBytes(16)
+
+		sc := models.StackSourceConfig{
+			Name:         sourceConfig.Name,
+			ImageRepoURI: sourceConfig.ImageRepoURI,
+			ImageTag:     sourceConfig.ImageTag,
+			UID:          newUid,
+		}
+		previousSourceConfigs = append(previousSourceConfigs, sc)
+	}
+
+	newRevision := models.StackRevision{
+		RevisionNumber: prevRevision.RevisionNumber + 1,
+		Status:         string(types.StackRevisionStatusDeploying),
+		SourceConfigs:  append(prevRevision.SourceConfigs, sourceConfig),
+		StackID:        stack.ID,
+	}
+
+	tester.repo.Stack().AppendNewRevision(&newRevision)
+}

+ 40 - 0
cmd/migrate/populate_source_config_display_name/populate.go

@@ -0,0 +1,40 @@
+package populate_source_config_display_name
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	lr "github.com/porter-dev/porter/pkg/logger"
+	_gorm "gorm.io/gorm"
+)
+
+func PopulateSourceConfigDisplayName(db *_gorm.DB, logger *lr.Logger) error {
+	logger.Info().Msg("Initiated source config display name population")
+	// get all source configs
+	sourceConfigs := make([]*models.StackSourceConfig, 0)
+
+	if err := db.Find(&sourceConfigs).Error; err != nil {
+		logger.Error().Msgf("failed to get source configs %v", err)
+		return err
+	}
+
+	if len(sourceConfigs) == 0 {
+		logger.Info().Msg("no source configs to populate")
+		return nil
+	}
+
+	updatedCount := 0
+	// copy name to display name if display name is empty
+	for _, sourceConfig := range sourceConfigs {
+		if sourceConfig.DisplayName == "" {
+			sourceConfig.DisplayName = sourceConfig.Name
+			updatedCount++
+		}
+	}
+	// update source configs
+	if err := db.Save(&sourceConfigs).Error; err != nil {
+		logger.Error().Msgf("failed to update source configs %v", err)
+		return err
+	}
+
+	logger.Info().Msgf("source config display name population completed, %d source configs updated", updatedCount)
+	return nil
+}

+ 76 - 0
cmd/migrate/populate_source_config_display_name/populate_test.go

@@ -0,0 +1,76 @@
+package populate_source_config_display_name_test
+
+import (
+	"testing"
+
+	"github.com/porter-dev/porter/cmd/migrate/populate_source_config_display_name"
+
+	"github.com/porter-dev/porter/internal/models"
+	lr "github.com/porter-dev/porter/pkg/logger"
+)
+
+func TestAllSourceConfigsArePopulated(t *testing.T) {
+	logger := lr.NewConsole(true)
+
+	tester := &tester{
+		dbFileName: "./porter_stable_source_config_id_population.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	defer cleanup(tester, t)
+
+	stackName := "first-stack"
+
+	initStack(tester, t, stackName)
+
+	createNewStackRevision(tester, t, stackName)
+
+	createNewStackRevision(tester, t, stackName)
+
+	createNewStackRevision(tester, t, stackName)
+
+	err := populate_source_config_display_name.PopulateSourceConfigDisplayName(tester.DB, logger)
+
+	if err != nil {
+		t.Fatalf("%\n", err)
+		return
+	}
+
+	sourceConfigs := []*models.StackSourceConfig{}
+
+	if err := tester.DB.Find(&sourceConfigs).Error; err != nil {
+		t.Fatalf("failed to find source configs: %s", err)
+	}
+
+	if len(sourceConfigs) != 4 {
+		t.Fatalf("expected 4 source configs, got %d", len(sourceConfigs))
+	}
+
+	for _, sc := range sourceConfigs {
+		if sc.DisplayName == "" {
+			t.Fatalf("expected display name to be populated, got empty string")
+		}
+	}
+}
+
+func TestPopulateOnEmptyStack(t *testing.T) {
+	logger := lr.NewConsole(true)
+
+	tester := &tester{
+		dbFileName: "./porter_stable_source_config_id_population.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	initEmptyStack(tester, t, "empty-stack")
+
+	defer cleanup(tester, t)
+
+	err := populate_source_config_display_name.PopulateSourceConfigDisplayName(tester.DB, logger)
+
+	if err != nil {
+		t.Fatalf("expected no error, got %s", err)
+		return
+	}
+}

+ 15 - 13
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_SourceConfig.tsx

@@ -30,7 +30,12 @@ const _SourceConfig = ({
     const index = newSourceConfigArray.findIndex(
     const index = newSourceConfigArray.findIndex(
       (sc) => sc.id === sourceConfig.id
       (sc) => sc.id === sourceConfig.id
     );
     );
-    newSourceConfigArray[index] = sourceConfig;
+
+    newSourceConfigArray[index] = {
+      ...sourceConfig,
+      display_name: sourceConfig.display_name || sourceConfig.name,
+    };
+
     setSourceConfigArrayCopy(newSourceConfigArray);
     setSourceConfigArrayCopy(newSourceConfigArray);
   };
   };
 
 
@@ -136,17 +141,19 @@ const SourceConfigItem = ({
   disabled: boolean;
   disabled: boolean;
 }) => {
 }) => {
   const [editNameMode, toggleEditNameMode] = useReducer((prev) => !prev, false);
   const [editNameMode, toggleEditNameMode] = useReducer((prev) => !prev, false);
-  const prevName = useRef(sourceConfig.name);
-  const [name, setName] = useState(sourceConfig.name);
+  const prevName = useRef(sourceConfig.display_name || sourceConfig.name);
+  const [name, setName] = useState(
+    sourceConfig.display_name || sourceConfig.name
+  );
 
 
   const handleNameChange = (newName: string) => {
   const handleNameChange = (newName: string) => {
     setName(newName);
     setName(newName);
-    handleChange({ ...sourceConfig, name: newName });
+    handleChange({ ...sourceConfig, display_name: newName });
   };
   };
 
 
   const handleNameChangeCancel = () => {
   const handleNameChangeCancel = () => {
     setName(prevName.current);
     setName(prevName.current);
-    handleChange({ ...sourceConfig, name: prevName.current });
+    handleChange({ ...sourceConfig, display_name: prevName.current });
     toggleEditNameMode();
     toggleEditNameMode();
   };
   };
 
 
@@ -170,14 +177,9 @@ const SourceConfigItem = ({
         <SourceConfigStyles.ItemTitle>
         <SourceConfigStyles.ItemTitle>
           <span>{name}</span>
           <span>{name}</span>
 
 
-          {sourceConfig.stable_source_config_id && (
-            <EditButton
-              onClick={toggleEditNameMode}
-              disabled={!sourceConfig.stable_source_config_id}
-            >
-              <i className="material-icons-outlined">edit</i>
-            </EditButton>
-          )}
+          <EditButton onClick={toggleEditNameMode}>
+            <i className="material-icons-outlined">edit</i>
+          </EditButton>
         </SourceConfigStyles.ItemTitle>
         </SourceConfigStyles.ItemTitle>
       )}
       )}
 
 

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/stacks/launch/SelectSource.tsx

@@ -22,8 +22,8 @@ const SelectSource = () => {
       return;
       return;
     }
     }
 
 
-    const newSource: CreateStackBody["source_configs"][0] = {
-      name: sourceName,
+    const newSource: Omit<CreateStackBody["source_configs"][0], "name"> = {
+      display_name: sourceName,
       image_repo_uri: imageUrl,
       image_repo_uri: imageUrl,
       image_tag: imageTag,
       image_tag: imageTag,
     };
     };

+ 10 - 5
dashboard/src/main/home/cluster-dashboard/stacks/launch/Store.tsx

@@ -11,7 +11,9 @@ export type StacksLaunchContextType = {
   setStackName: (name: string) => void;
   setStackName: (name: string) => void;
   setStackNamespace: (namespace: string) => void;
   setStackNamespace: (namespace: string) => void;
 
 
-  addSourceConfig: (sourceConfig: CreateStackBody["source_configs"][0]) => void;
+  addSourceConfig: (
+    sourceConfig: Omit<CreateStackBody["source_configs"][0], "name">
+  ) => void;
 
 
   addAppResource: (
   addAppResource: (
     appResource: CreateStackBody["app_resources"][0],
     appResource: CreateStackBody["app_resources"][0],
@@ -40,7 +42,9 @@ const defaultValues: StacksLaunchContextType = {
   setStackName: (name: string) => {},
   setStackName: (name: string) => {},
   setStackNamespace: (namespace: string) => {},
   setStackNamespace: (namespace: string) => {},
 
 
-  addSourceConfig: (sourceConfig: CreateStackBody["source_configs"][0]) => {},
+  addSourceConfig: (
+    sourceConfig: Omit<CreateStackBody["source_configs"][0], "name">
+  ) => {},
 
 
   addAppResource: (appResource: CreateStackBody["app_resources"][0]) => {},
   addAppResource: (appResource: CreateStackBody["app_resources"][0]) => {},
 
 
@@ -92,10 +96,11 @@ const StacksLaunchContextProvider: React.FC<{}> = ({ children }) => {
       source_configs: [
       source_configs: [
         ...prev.source_configs,
         ...prev.source_configs,
         {
         {
-          name:
-            sourceConfig.name ||
-            newSourceConfigName(prev.source_configs.length),
           ...sourceConfig,
           ...sourceConfig,
+          display_name:
+            sourceConfig.display_name ||
+            newSourceConfigName(prev.source_configs.length),
+          name: newSourceConfigName(prev.source_configs.length),
         },
         },
       ],
       ],
     }));
     }));

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/stacks/types.ts

@@ -9,6 +9,7 @@ export type CreateStackBody = {
     values: unknown;
     values: unknown;
   }[];
   }[];
   source_configs: {
   source_configs: {
+    display_name: string;
     name: string;
     name: string;
     image_repo_uri: string;
     image_repo_uri: string;
     image_tag: string;
     image_tag: string;
@@ -80,6 +81,7 @@ export type StackRevision = {
 
 
 export type SourceConfig = {
 export type SourceConfig = {
   id: string;
   id: string;
+  display_name: string;
   name: string;
   name: string;
   created_at: string;
   created_at: string;
   updated_at: string;
   updated_at: string;
@@ -90,8 +92,6 @@ export type SourceConfig = {
   stack_id: string;
   stack_id: string;
   stack_revision_id: number;
   stack_revision_id: number;
 
 
-  stable_source_config_id: string;
-
   build?: {
   build?: {
     method: "pack" | "docker";
     method: "pack" | "docker";
     folder_path: string;
     folder_path: string;

+ 17 - 17
dashboard/src/shared/api.tsx

@@ -2003,6 +2003,22 @@ const createStack = baseApi<
     `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks`
     `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks`
 );
 );
 
 
+const updateStack = baseApi<
+  {
+    name: string;
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+    namespace: string;
+    stack_id: string;
+  }
+>(
+  "PATCH",
+  ({ project_id, cluster_id, namespace, stack_id }) =>
+    `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks/${stack_id}`
+);
+
 const listStacks = baseApi<
 const listStacks = baseApi<
   {},
   {},
   { project_id: number; cluster_id: number; namespace: string }
   { project_id: number; cluster_id: number; namespace: string }
@@ -2145,22 +2161,6 @@ const removeStackEnvGroup = baseApi<
     `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks/${stack_id}/remove_env_group/${env_group_name}`
     `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks/${stack_id}/remove_env_group/${env_group_name}`
 );
 );
 
 
-const updateStack = baseApi<
-  {
-    name: string;
-  },
-  {
-    project_id: number;
-    cluster_id: number;
-    namespace: string;
-    stack_id: string;
-  }
->(
-  "PATCH",
-  ({ project_id, cluster_id, namespace, stack_id }) =>
-    `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks/${stack_id}`
-);
-
 const getGithubStatus = baseApi<{}, {}>("GET", ({}) => `/api/status/github`);
 const getGithubStatus = baseApi<{}, {}>("GET", ({}) => `/api/status/github`);
 
 
 // Bundle export to allow default api import (api.<method> is more readable)
 // Bundle export to allow default api import (api.<method> is more readable)
@@ -2355,6 +2355,7 @@ export default {
   getStack,
   getStack,
   getStackRevision,
   getStackRevision,
   createStack,
   createStack,
+  updateStack,
   rollbackStack,
   rollbackStack,
   deleteStack,
   deleteStack,
   updateStackSourceConfig,
   updateStackSourceConfig,
@@ -2362,7 +2363,6 @@ export default {
   removeStackAppResource,
   removeStackAppResource,
   addStackEnvGroup,
   addStackEnvGroup,
   removeStackEnvGroup,
   removeStackEnvGroup,
-  updateStack,
 
 
   // STATUS
   // STATUS
   getGithubStatus,
   getGithubStatus,

+ 3 - 1
internal/kubernetes/prometheus/metrics.go

@@ -301,7 +301,7 @@ func getSelectionRegex(kind, name string) (string, error) {
 
 
 	switch strings.ToLower(kind) {
 	switch strings.ToLower(kind) {
 	case "deployment":
 	case "deployment":
-		suffix = "[a-z0-9]+-[a-z0-9]+"
+		suffix = "[a-z0-9]+"
 	case "statefulset":
 	case "statefulset":
 		suffix = "[0-9]+"
 		suffix = "[0-9]+"
 	case "job":
 	case "job":
@@ -310,6 +310,8 @@ func getSelectionRegex(kind, name string) (string, error) {
 		suffix = "[a-z0-9]+-[a-z0-9]+"
 		suffix = "[a-z0-9]+-[a-z0-9]+"
 	case "ingress":
 	case "ingress":
 		return name, nil
 		return name, nil
+	case "daemonset":
+		suffix = "[a-z0-9]+"
 	default:
 	default:
 		return "", fmt.Errorf("not a supported controller to query for metrics")
 		return "", fmt.Errorf("not a supported controller to query for metrics")
 	}
 	}

+ 11 - 13
internal/models/stack.go

@@ -160,14 +160,12 @@ func (s StackResource) ToStackResource(stackID string, stackRevisionID uint, sou
 type StackSourceConfig struct {
 type StackSourceConfig struct {
 	gorm.Model
 	gorm.Model
 
 
-	// A unique identifier for this source config, this will allow us identify a same source config
-	// across multiple revisions and updates. This is not the same as the UID or ID which are updated over revisions.
-	StableSourceConfigID string
-
 	StackRevisionID uint
 	StackRevisionID uint
 
 
 	Name string
 	Name string
 
 
+	DisplayName string
+
 	UID string
 	UID string
 
 
 	ImageRepoURI string
 	ImageRepoURI string
@@ -179,15 +177,15 @@ type StackSourceConfig struct {
 
 
 func (s StackSourceConfig) ToStackSourceConfigType(stackID string, stackRevisionID uint) *types.StackSourceConfig {
 func (s StackSourceConfig) ToStackSourceConfigType(stackID string, stackRevisionID uint) *types.StackSourceConfig {
 	return &types.StackSourceConfig{
 	return &types.StackSourceConfig{
-		CreatedAt:            s.CreatedAt,
-		UpdatedAt:            s.UpdatedAt,
-		StackID:              stackID,
-		StackRevisionID:      stackRevisionID,
-		Name:                 s.Name,
-		ID:                   s.UID,
-		ImageRepoURI:         s.ImageRepoURI,
-		ImageTag:             s.ImageTag,
-		StableSourceConfigID: s.StableSourceConfigID,
+		CreatedAt:       s.CreatedAt,
+		UpdatedAt:       s.UpdatedAt,
+		StackID:         stackID,
+		StackRevisionID: stackRevisionID,
+		Name:            s.Name,
+		ID:              s.UID,
+		ImageRepoURI:    s.ImageRepoURI,
+		ImageTag:        s.ImageTag,
+		DisplayName:     s.DisplayName,
 	}
 	}
 }
 }
 
 

+ 1 - 1
internal/repository/stack.go

@@ -9,8 +9,8 @@ type StackRepository interface {
 	ReadStackByStringID(projectID uint, stackID string) (*models.Stack, error)
 	ReadStackByStringID(projectID uint, stackID string) (*models.Stack, error)
 	ListStacks(projectID uint, clusterID uint, namespace string) ([]*models.Stack, error)
 	ListStacks(projectID uint, clusterID uint, namespace string) ([]*models.Stack, error)
 	DeleteStack(stack *models.Stack) (*models.Stack, error)
 	DeleteStack(stack *models.Stack) (*models.Stack, error)
-
 	UpdateStack(stack *models.Stack) (*models.Stack, error)
 	UpdateStack(stack *models.Stack) (*models.Stack, error)
+
 	UpdateStackRevision(revision *models.StackRevision) (*models.StackRevision, error)
 	UpdateStackRevision(revision *models.StackRevision) (*models.StackRevision, error)
 	ReadStackRevision(stackRevisionID uint) (*models.StackRevision, error)
 	ReadStackRevision(stackRevisionID uint) (*models.StackRevision, error)
 	ReadStackRevisionByNumber(stackID uint, revisionNumber uint) (*models.StackRevision, error)
 	ReadStackRevisionByNumber(stackID uint, revisionNumber uint) (*models.StackRevision, error)

+ 2 - 1
internal/stacks/helpers.go

@@ -22,6 +22,7 @@ func CloneSourceConfigs(sourceConfigs []models.StackSourceConfig) ([]models.Stac
 		res = append(res, models.StackSourceConfig{
 		res = append(res, models.StackSourceConfig{
 			UID:          uid,
 			UID:          uid,
 			Name:         sourceConfig.Name,
 			Name:         sourceConfig.Name,
+			DisplayName:  sourceConfig.DisplayName,
 			ImageRepoURI: sourceConfig.ImageRepoURI,
 			ImageRepoURI: sourceConfig.ImageRepoURI,
 			ImageTag:     sourceConfig.ImageTag,
 			ImageTag:     sourceConfig.ImageTag,
 		})
 		})
@@ -52,7 +53,7 @@ func CloneAppResources(
 			if prevSourceConfig.UID == appResource.StackSourceConfigUID {
 			if prevSourceConfig.UID == appResource.StackSourceConfigUID {
 				// find the corresponding new source config
 				// find the corresponding new source config
 				for _, newSourceConfig := range newSourceConfigs {
 				for _, newSourceConfig := range newSourceConfigs {
-					if newSourceConfig.StableSourceConfigID == prevSourceConfig.StableSourceConfigID {
+					if newSourceConfig.Name == prevSourceConfig.Name {
 						linkedSourceConfigUID = newSourceConfig.UID
 						linkedSourceConfigUID = newSourceConfig.UID
 					}
 					}
 				}
 				}