Procházet zdrojové kódy

Merge branch 'master' into env-group-touchup

jusrhee před 2 roky
rodič
revize
6abb672606

+ 2 - 0
api/client/porter_app.go

@@ -298,6 +298,7 @@ type UpdateAppInput struct {
 	Base64PorterYAML   string
 	IsEnvOverride      bool
 	WithPredeploy      bool
+	Exact              bool
 }
 
 // UpdateApp updates a porter app
@@ -318,6 +319,7 @@ func (c *Client) UpdateApp(
 		Base64PorterYAML:   inp.Base64PorterYAML,
 		IsEnvOverride:      inp.IsEnvOverride,
 		WithPredeploy:      inp.WithPredeploy,
+		Exact:              inp.Exact,
 	}
 
 	err := c.postRequest(

+ 32 - 13
api/server/handlers/api_contract/list.go

@@ -1,23 +1,25 @@
 package api_contract
 
 import (
-	"fmt"
 	"net/http"
 	"strconv"
 
-	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"github.com/porter-dev/porter/internal/telemetry"
 )
 
+// APIContractRevisionListHandler is the handler for the GET /api/projects/{project_id}/contracts endpoint
 type APIContractRevisionListHandler struct {
 	handlers.PorterHandlerReadWriter
 }
 
+// NewAPIContractRevisionListHandler returns a new APIContractRevisionListHandler
 func NewAPIContractRevisionListHandler(
 	config *config.Config,
 	decoderValidator shared.RequestDecoderValidator,
@@ -28,32 +30,49 @@ func NewAPIContractRevisionListHandler(
 	}
 }
 
+// APIContractRevisionListRequest is the request schema for the APIContractRevisionListHandler
+type APIContractRevisionListRequest struct {
+	Latest    bool   `schema:"latest"`
+	ClusterID string `schema:"cluster_id"`
+}
+
 // ServeHTTP returns a list of Porter API contract revisions for a given project.
 // If clusterID is also given, it will list by project_id, cluster_id
+// If latest is provided, it will only return the latest revision for each contract
 func (c *APIContractRevisionListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-api-contract-revisions")
+	defer span.End()
+
+	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
+	request := &APIContractRevisionListRequest{}
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		err := telemetry.Error(ctx, span, nil, "error decoding request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
 
 	clusterID := 0
-	clusterIDParam := chi.URLParam(r, "cluster_id")
+	clusterIDParam := request.ClusterID
 	if clusterIDParam != "" {
 		i, err := strconv.Atoi(clusterIDParam)
 		if err != nil {
-			e := fmt.Errorf("invalid cluster_id query param given: %w", err)
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(e))
+			err = telemetry.Error(ctx, span, err, "error parsing cluster id")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 			return
 		}
 		clusterID = i
 	}
-
-	ctx := r.Context()
-
-	revisions, err := c.Config().Repo.APIContractRevisioner().List(ctx, proj.ID, uint(clusterID))
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "cluster-id", Value: clusterID},
+		telemetry.AttributeKV{Key: "latest", Value: request.Latest},
+	)
+	revisions, err := c.Config().Repo.APIContractRevisioner().List(ctx, proj.ID, repository.WithClusterID(uint(clusterID)), repository.WithLatest(request.Latest))
 	if err != nil {
-		e := fmt.Errorf("error listing api contract revision: %w", err)
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(e))
+		err = telemetry.Error(ctx, span, err, "error getting latest api contract revisions")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
 
-	w.WriteHeader(http.StatusOK)
 	c.WriteResult(w, r, revisions)
 }

+ 2 - 1
api/server/handlers/cluster/delete.go

@@ -13,6 +13,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
 )
 
 type ClusterDeleteHandler struct {
@@ -36,7 +37,7 @@ func (c *ClusterDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 
 	if cluster.ProvisionedBy == "CAPI" {
 		if c.Config().EnableCAPIProvisioner {
-			revisions, err := c.Config().Repo.APIContractRevisioner().List(ctx, cluster.ProjectID, cluster.ID)
+			revisions, err := c.Config().Repo.APIContractRevisioner().List(ctx, cluster.ProjectID, repository.WithClusterID(cluster.ID))
 			if err != nil {
 				e := fmt.Errorf("error listing revisions for cluster %d: %w", cluster.ID, err)
 				c.HandleAPIError(w, r, apierrors.NewErrInternal(e))

+ 8 - 1
api/server/handlers/porter_app/update_app.go

@@ -71,6 +71,8 @@ type UpdateAppRequest struct {
 	IsEnvOverride bool `json:"is_env_override"`
 	// WithPredeploy is a flag to indicate whether to run the predeploy job
 	WithPredeploy bool `json:"with_predeploy"`
+	// Exact is a flag to indicate whether to apply the update exactly as specified in the request (default is to merge with existing app)
+	Exact bool `json:"exact"`
 }
 
 // UpdateAppResponse is the response object for the POST /apps/update endpoint
@@ -120,6 +122,7 @@ func (c *UpdateAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	var overrides *porterv1.PorterApp
 	appProto := &porterv1.PorterApp{}
 
+	var previewEnvVariables map[string]string
 	envVariables := request.Variables
 
 	// get app definition from either base64 yaml or base64 porter app proto
@@ -180,7 +183,7 @@ func (c *UpdateAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		if appFromYaml.PreviewApp != nil {
 			overrides = appFromYaml.PreviewApp.AppProto
 			addonOverrides = appFromYaml.PreviewApp.Addons
-			envVariables = mergeEnvVariables(envVariables, appFromYaml.PreviewApp.EnvVariables)
+			previewEnvVariables = appFromYaml.PreviewApp.EnvVariables
 		}
 
 		addons = appFromYaml.Addons
@@ -250,6 +253,9 @@ func (c *UpdateAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			Normal: envVariables,
 			Secret: request.Secrets,
 		},
+		AppEnvOverrides: &porterv1.EnvGroupVariables{
+			Normal: previewEnvVariables,
+		},
 		Deletions: &porterv1.Deletions{
 			ServiceNames:     request.Deletions.ServiceNames,
 			PredeployNames:   request.Deletions.Predeploy,
@@ -263,6 +269,7 @@ func (c *UpdateAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		Addons:              addons,
 		AddonOverrides:      addonOverrides,
 		IsPredeployEligible: request.WithPredeploy,
+		Exact:               request.Exact,
 	})
 
 	ccpResp, err := c.Config().ClusterControlPlaneClient.UpdateApp(ctx, updateReq)

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

@@ -12,6 +12,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/notifier"
+	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/telemetry"
 )
 
@@ -50,7 +51,7 @@ func (p *ProjectDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 					continue
 				}
 
-				contractRevision, err := p.Config().Repo.APIContractRevisioner().List(ctx, proj.ID, cluster.ID)
+				contractRevision, err := p.Config().Repo.APIContractRevisioner().List(ctx, proj.ID, repository.WithClusterID(cluster.ID))
 				if err != nil {
 					e := "error finding contract revisions for cluster"
 					err = telemetry.Error(ctx, span, err, e)

+ 3 - 0
cli/cmd/commands/apply.go

@@ -46,6 +46,7 @@ var (
 	// pullImageBeforeBuild is a flag that determines whether to pull the docker image from a repo before building
 	pullImageBeforeBuild bool
 	predeploy            bool
+	exact                bool
 )
 
 func registerCommand_Apply(cliConf config.CLIConfig) *cobra.Command {
@@ -114,6 +115,7 @@ applying a configuration:
 	applyCmd.PersistentFlags().BoolVar(&pullImageBeforeBuild, "pull-before-build", false, "attempt to pull image from registry before building")
 	applyCmd.PersistentFlags().StringVar(&imageTagOverride, "tag", "", "set the image tag used for the application (overrides field in yaml)")
 	applyCmd.PersistentFlags().BoolVar(&predeploy, "predeploy", false, "run predeploy job before deploying the application")
+	applyCmd.PersistentFlags().BoolVar(&exact, "exact", false, "apply the exact configuration as specified in the porter.yaml file (default is to merge with existing configuration)")
 	applyCmd.PersistentFlags().BoolVarP(
 		&appWait,
 		"wait",
@@ -159,6 +161,7 @@ func apply(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client ap
 			WaitForSuccessfulDeployment: appWait,
 			PullImageBeforeBuild:        pullImageBeforeBuild,
 			WithPredeploy:               predeploy,
+			Exact:                       exact,
 		}
 		err := v2.Apply(ctx, inp)
 		if err != nil {

+ 3 - 0
cli/cmd/v2/apply.go

@@ -44,6 +44,8 @@ type ApplyInput struct {
 	PullImageBeforeBuild bool
 	// WithPredeploy is true when Apply should run the predeploy step
 	WithPredeploy bool
+	// Exact is true when Apply should use the exact app config provided by the user
+	Exact bool
 }
 
 // Apply implements the functionality of the `porter apply` command for validate apply v2 projects
@@ -121,6 +123,7 @@ func Apply(ctx context.Context, inp ApplyInput) error {
 		CommitSHA:          commitSHA,
 		Base64PorterYAML:   b64YAML,
 		WithPredeploy:      inp.WithPredeploy,
+		Exact:              inp.Exact,
 	}
 
 	updateResp, err := client.UpdateApp(ctx, updateInput)

+ 1 - 1
go.mod

@@ -83,7 +83,7 @@ require (
 	github.com/matryer/is v1.4.0
 	github.com/nats-io/nats.go v1.24.0
 	github.com/open-policy-agent/opa v0.44.0
-	github.com/porter-dev/api-contracts v0.2.102
+	github.com/porter-dev/api-contracts v0.2.103
 	github.com/riandyrn/otelchi v0.5.1
 	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1
 	github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d

+ 2 - 6
go.sum

@@ -1523,12 +1523,8 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
-github.com/porter-dev/api-contracts v0.2.99 h1:eHzYSwacLEV5Di2boCnuv8ddoxzvaNCORAD0VOibOBU=
-github.com/porter-dev/api-contracts v0.2.99/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
-github.com/porter-dev/api-contracts v0.2.101 h1:pNPtkTqcr+khUwBfT1z8y86DaVhFIkcHjtD+GL2xCF0=
-github.com/porter-dev/api-contracts v0.2.101/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
-github.com/porter-dev/api-contracts v0.2.102 h1:iZnwSmBM4gfRqdNXV5t9fFqd/pz5bj49MV+yqXgb46Q=
-github.com/porter-dev/api-contracts v0.2.102/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
+github.com/porter-dev/api-contracts v0.2.103 h1:z6tMxs5aF7owi4SRiFUKN/IzXYkOHKIb5Ekqlw8N5U8=
+github.com/porter-dev/api-contracts v0.2.103/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 github.com/porter-dev/switchboard v0.0.3 h1:dBuYkiVLa5Ce7059d6qTe9a1C2XEORFEanhbtV92R+M=
 github.com/porter-dev/switchboard v0.0.3/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=

+ 3 - 8
internal/porter_app/parse.go

@@ -71,14 +71,9 @@ func ParseYAML(ctx context.Context, porterYaml []byte, appName string) (v2.AppWi
 		}
 
 		appDefinition.AppProto.Name = appName
-
-		if appDefinition.PreviewApp != nil && appDefinition.PreviewApp.AppProto != nil {
-			if appDefinition.PreviewApp.AppProto.Name != "" && appDefinition.PreviewApp.AppProto.Name != appName {
-				telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "parsed-preview-name", Value: appDefinition.PreviewApp.AppProto.Name})
-				return appDefinition, telemetry.Error(ctx, span, nil, "name specified in porter.yaml does not match preview app name")
-			}
-			appDefinition.PreviewApp.AppProto.Name = appName
-		}
+	}
+	if appDefinition.PreviewApp != nil && appDefinition.PreviewApp.AppProto != nil {
+		appDefinition.PreviewApp.AppProto.Name = appDefinition.AppProto.Name
 	}
 
 	return appDefinition, nil

+ 24 - 1
internal/repository/api_contract.go

@@ -7,11 +7,34 @@ import (
 	"github.com/porter-dev/porter/internal/models"
 )
 
+// APIContractRevisionFilter is used to filter the APIContractRevisions
+type APIContractRevisionFilter struct {
+	ClusterID uint
+	Latest    bool
+}
+
+// APIContractRevisionFilters is a function that applies filters to the APIContractRevisions
+type APIContractRevisionFilters func(*APIContractRevisionFilter)
+
+// WithClusterID filters the APIContractRevisions by clusterID
+func WithClusterID(clusterID uint) APIContractRevisionFilters {
+	return func(f *APIContractRevisionFilter) {
+		f.ClusterID = clusterID
+	}
+}
+
+// WithLatest filters the APIContractRevisions by the latest revision
+func WithLatest(latest bool) APIContractRevisionFilters {
+	return func(f *APIContractRevisionFilter) {
+		f.Latest = latest
+	}
+}
+
 // APIContractRevisioner represents queries on the api_contracts table, which stores the all the versions of an applied API contract
 type APIContractRevisioner interface {
 	Insert(ctx context.Context, conf models.APIContractRevision) (models.APIContractRevision, error)
 	// List returns a slice of APIContractRevision, sorted by created_at descending
-	List(ctx context.Context, projectID uint, clusterID uint) ([]*models.APIContractRevision, error)
+	List(ctx context.Context, projectID uint, opts ...APIContractRevisionFilters) ([]*models.APIContractRevision, error)
 	Get(ctx context.Context, revisionID uuid.UUID) (models.APIContractRevision, error)
 	Delete(ctx context.Context, projectID uint, clusterID uint, revisionID uuid.UUID) error
 }

+ 69 - 10
internal/repository/gorm/api_contract.go

@@ -8,6 +8,7 @@ import (
 	"github.com/google/uuid"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
+	"github.com/porter-dev/porter/internal/telemetry"
 	"gorm.io/gorm"
 )
 
@@ -35,19 +36,39 @@ func (cr APIContractRepository) Insert(ctx context.Context, conf models.APIContr
 
 // List returns a list of api contract revisions sorted by created date for a given projectID.
 // If clusterID is not specified (set to 0), this will return all revisions for a given project
-func (cr APIContractRepository) List(ctx context.Context, projectID uint, clusterID uint) ([]*models.APIContractRevision, error) {
+// If latest is true, it will only return the latest revision for each contract
+func (cr APIContractRepository) List(ctx context.Context, projectID uint, filters ...repository.APIContractRevisionFilters) ([]*models.APIContractRevision, error) {
+	ctx, span := telemetry.NewSpan(ctx, "list-api-contract-revisions")
+	defer span.End()
+
+	var opts repository.APIContractRevisionFilter
+	for _, opt := range filters {
+		opt(&opts)
+	}
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "project-id", Value: projectID},
+		telemetry.AttributeKV{Key: "cluster-id", Value: opts.ClusterID},
+		telemetry.AttributeKV{Key: "latest", Value: opts.Latest},
+	)
+
+	if projectID == 0 {
+		return nil, telemetry.Error(ctx, span, nil, "project id cannot be 0")
+	}
+
+	if opts.Latest {
+		return cr.Latest(ctx, projectID, filters...)
+	}
+
 	var confs []*models.APIContractRevision
+	query := cr.db.Model(&models.APIContractRevision{}).Where("project_id = ?", projectID)
 
-	if clusterID == 0 {
-		tx := cr.db.Where("project_id = ?", projectID).Order("created_at desc").Find(&confs)
-		if tx.Error != nil {
-			return nil, tx.Error
-		}
-		return confs, nil
+	if opts.ClusterID != 0 {
+		query = query.Where("cluster_id = ?", opts.ClusterID)
 	}
-	tx := cr.db.Where("project_id = ? and cluster_id = ?", projectID, clusterID).Order("created_at desc").Find(&confs)
-	if tx.Error != nil {
-		return nil, tx.Error
+
+	if err := query.Find(&confs).Error; err != nil {
+		return nil, err
 	}
 
 	return confs, nil
@@ -85,3 +106,41 @@ func (cr APIContractRepository) Get(ctx context.Context, revisionID uuid.UUID) (
 
 	return acr, nil
 }
+
+// Latest returns the latest version for each contract specified by filters
+func (cr APIContractRepository) Latest(ctx context.Context, projectID uint, filters ...repository.APIContractRevisionFilters) ([]*models.APIContractRevision, error) {
+	ctx, span := telemetry.NewSpan(ctx, "list-latest-api-contract-revisions")
+	defer span.End()
+
+	var confs []*models.APIContractRevision
+
+	var opts repository.APIContractRevisionFilter
+	for _, opt := range filters {
+		opt(&opts)
+	}
+
+	queryString := `
+		SELECT DISTINCT ON (cluster_id) *
+		FROM api_contract_revisions
+		WHERE project_id = ?
+		ORDER BY cluster_id, created_at DESC
+    `
+	args := []any{projectID}
+
+	if opts.ClusterID != 0 {
+		queryString = `
+			SELECT DISTINCT ON (cluster_id) *
+			FROM api_contract_revisions
+			WHERE project_id = ? AND cluster_id = ?
+			ORDER BY cluster_id, created_at DESC
+    	`
+		args = append(args, opts.ClusterID)
+	}
+
+	tx := cr.db.Raw(queryString, args...).Scan(&confs)
+	if tx.Error != nil {
+		return nil, telemetry.Error(ctx, span, tx.Error, "error getting latest api contract revisions")
+	}
+
+	return confs, nil
+}

+ 1 - 1
internal/repository/test/api_contract.go

@@ -23,7 +23,7 @@ func (cr APIContractRepository) Insert(ctx context.Context, conf models.APIContr
 }
 
 // List returns a list of api contract revisions sorted by created date for a given project and cluster
-func (cr APIContractRepository) List(ctx context.Context, projectID uint, clusterID uint) ([]*models.APIContractRevision, error) {
+func (cr APIContractRepository) List(ctx context.Context, projectID uint, opts ...repository.APIContractRevisionFilters) ([]*models.APIContractRevision, error) {
 	var confs []*models.APIContractRevision
 	return confs, errors.New("not implemented")
 }