浏览代码

New list contracts backend (#4269)

Feroze Mohideen 2 年之前
父节点
当前提交
92ebeacb22

+ 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))

+ 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)

+ 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")
 }