Selaa lähdekoodia

read latest app revision with source (#3556)

ianedwards 2 vuotta sitten
vanhempi
sitoutus
e790307866

+ 134 - 0
api/server/handlers/porter_app/latest_app_revisions.go

@@ -0,0 +1,134 @@
+package porter_app
+
+import (
+	"net/http"
+
+	"connectrpc.com/connect"
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+	"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/porter_app"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+// LatestAppRevisionsHandler handles requests to the /apps/{porter_app_name}/revisions endpoint
+type LatestAppRevisionsHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewLatestAppRevisionsHandler returns a new LatestAppRevisionsHandler
+func NewLatestAppRevisionsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *LatestAppRevisionsHandler {
+	return &LatestAppRevisionsHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// LatestAppRevisionsRequest represents the response from the /apps/{porter_app_name}/revisions endpoint
+type LatestAppRevisionsRequest struct{}
+
+// LatestRevisionWithSource is an app revision and its source porter app
+type LatestRevisionWithSource struct {
+	AppRevision porter_app.Revision `json:"app_revision"`
+	Source      types.PorterApp     `json:"source"`
+}
+
+// LatestAppRevisionsResponse represents the response from the /apps/{porter_app_name}/revisions endpoint
+type LatestAppRevisionsResponse struct {
+	AppRevisions []LatestRevisionWithSource `json:"app_revisions"`
+}
+
+func (c *LatestAppRevisionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-list-app-revisions")
+	defer span.End()
+
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	deploymentTargets, err := c.Repo().DeploymentTarget().List(project.ID)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error reading deployment targets")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	if len(deploymentTargets) == 0 {
+		res := &LatestAppRevisionsResponse{
+			AppRevisions: []LatestRevisionWithSource{},
+		}
+
+		c.WriteResult(w, r, res)
+		return
+	}
+
+	if len(deploymentTargets) > 1 {
+		err = telemetry.Error(ctx, span, err, "more than one deployment target found")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	// todo(ianedwards): once we have a way to select a deployment target, we can add it to the request
+	deploymentTarget := deploymentTargets[0]
+
+	listAppRevisionsReq := connect.NewRequest(&porterv1.LatestAppRevisionsRequest{
+		ProjectId:          int64(project.ID),
+		DeploymentTargetId: deploymentTarget.ID.String(),
+	})
+
+	latestAppRevisionsResp, err := c.Config().ClusterControlPlaneClient.LatestAppRevisions(ctx, listAppRevisionsReq)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting latest app revisions")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	if latestAppRevisionsResp == nil || latestAppRevisionsResp.Msg == nil {
+		err = telemetry.Error(ctx, span, nil, "latest app revisions response is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	appRevisions := latestAppRevisionsResp.Msg.AppRevisions
+	if appRevisions == nil {
+		appRevisions = []*porterv1.AppRevision{}
+	}
+
+	res := &LatestAppRevisionsResponse{
+		AppRevisions: make([]LatestRevisionWithSource, 0),
+	}
+
+	for _, revision := range appRevisions {
+		encodedRevision, err := porter_app.EncodedRevisionFromProto(ctx, revision)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error getting encoded revision from proto")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+
+		porterApp, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, revision.App.Name)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error reading porter app")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+		if porterApp == nil {
+			err := telemetry.Error(ctx, span, err, "porter app is nil")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+
+		res.AppRevisions = append(res.AppRevisions, LatestRevisionWithSource{
+			AppRevision: encodedRevision,
+			Source:      *porterApp.ToPorterAppType(),
+		})
+	}
+
+	c.WriteResult(w, r, res)
+}

+ 29 - 0
api/server/router/porter_app.go

@@ -774,6 +774,35 @@ func getPorterAppRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/revisions -> porter_app.NewCurrentAppRevisionHandler
+	latestAppRevisionsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/apps/revisions",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	latestAppRevisionsHandler := porter_app.NewLatestAppRevisionsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: latestAppRevisionsEndpoint,
+		Handler:  latestAppRevisionsHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/subdomain -> porter_app.NewCreateSubdomainHandler
 	createSubdomainEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 1 - 1
go.mod

@@ -81,7 +81,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.0.100
+	github.com/porter-dev/api-contracts v0.1.0
 	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 - 2
go.sum

@@ -1512,8 +1512,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.0.100 h1:378cKlIjPKlTsEVeyoGTQXskxy0xFmuIpxODSD1hzmo=
-github.com/porter-dev/api-contracts v0.0.100/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
+github.com/porter-dev/api-contracts v0.1.0 h1:WuYWO3zix/lbQgHy9AgYjNAkN5t8oR2ZuI34mM+BIeI=
+github.com/porter-dev/api-contracts v0.1.0/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=

+ 2 - 0
internal/repository/deployment_target.go

@@ -8,4 +8,6 @@ import (
 type DeploymentTargetRepository interface {
 	// DeploymentTargetBySelectorAndSelectorType finds a deployment target for a projectID and clusterID by its selector and selector type
 	DeploymentTargetBySelectorAndSelectorType(projectID uint, clusterID uint, selector, selectorType string) (*models.DeploymentTarget, error)
+	// List returns all deployment targets for a project
+	List(projectID uint) ([]*models.DeploymentTarget, error)
 }

+ 11 - 0
internal/repository/gorm/deployment_target.go

@@ -34,3 +34,14 @@ func (repo *DeploymentTargetRepository) DeploymentTargetBySelectorAndSelectorTyp
 
 	return deploymentTarget, nil
 }
+
+// List finds all deployment targets for a given project
+func (repo *DeploymentTargetRepository) List(projectID uint) ([]*models.DeploymentTarget, error) {
+	deploymentTargets := []*models.DeploymentTarget{}
+
+	if err := repo.db.Where("project_id = ?", projectID).Find(&deploymentTargets).Error; err != nil {
+		return nil, err
+	}
+
+	return deploymentTargets, nil
+}

+ 5 - 0
internal/repository/test/deployment_target.go

@@ -21,3 +21,8 @@ func NewDeploymentTargetRepository() repository.DeploymentTargetRepository {
 func (repo *DeploymentTargetRepository) DeploymentTargetBySelectorAndSelectorType(projectID uint, clusterID uint, selector, selectorType string) (*models.DeploymentTarget, error) {
 	return nil, errors.New("cannot read database")
 }
+
+// List returns all deployment targets for a project
+func (repo *DeploymentTargetRepository) List(projectID uint) ([]*models.DeploymentTarget, error) {
+	return nil, errors.New("cannot read database")
+}