Przeglądaj źródła

support target CLI (#4465)

d-g-town 2 lat temu
rodzic
commit
52808da7fe

+ 28 - 16
api/client/deployment_target.go

@@ -4,28 +4,40 @@ import (
 	"context"
 	"fmt"
 
-	"github.com/porter-dev/porter/api/server/handlers/deployment_target"
+	"github.com/porter-dev/porter/api/types"
 )
 
-// CreateDeploymentTarget creates a new deployment target for a given project and cluster with the provided name
+// CreateDeploymentTarget creates a deployment target with the given request options
 func (c *Client) CreateDeploymentTarget(
 	ctx context.Context,
-	projectID, clusterID uint,
-	selector string,
-	preview bool,
-) (*deployment_target.CreateDeploymentTargetResponse, error) {
-	resp := &deployment_target.CreateDeploymentTargetResponse{}
-
-	req := &deployment_target.CreateDeploymentTargetRequest{
-		Selector: selector,
-		Preview:  preview,
-	}
+	projectId uint,
+	req *types.CreateDeploymentTargetRequest,
+) (*types.CreateDeploymentTargetResponse, error) {
+	resp := &types.CreateDeploymentTargetResponse{}
 
 	err := c.postRequest(
-		fmt.Sprintf(
-			"/projects/%d/clusters/%d/deployment-targets",
-			projectID, clusterID,
-		),
+		fmt.Sprintf("/projects/%d/targets", projectId),
+		req,
+		resp,
+	)
+
+	return resp, err
+}
+
+// ListDeploymentTargets retrieves all deployment targets in a project
+func (c *Client) ListDeploymentTargets(
+	ctx context.Context,
+	projectId uint,
+	includePreviews bool,
+) (*types.ListDeploymentTargetsResponse, error) {
+	resp := &types.ListDeploymentTargetsResponse{}
+
+	req := &types.ListDeploymentTargetsRequest{
+		Preview: includePreviews,
+	}
+
+	err := c.getRequest(
+		fmt.Sprintf("/projects/%d/targets", projectId),
 		req,
 		resp,
 	)

+ 16 - 17
api/server/handlers/deployment_target/create.go

@@ -30,34 +30,21 @@ func NewCreateDeploymentTargetHandler(
 	}
 }
 
-// CreateDeploymentTargetRequest is the request object for the /deployment-targets POST endpoint
-type CreateDeploymentTargetRequest struct {
-	// Deprecated: use name instead
-	Selector string `json:"selector"`
-	Name     string `json:"name,omitempty"`
-	Preview  bool   `json:"preview"`
-}
-
-// CreateDeploymentTargetResponse is the response object for the /deployment-targets POST endpoint
-type CreateDeploymentTargetResponse struct {
-	DeploymentTargetID string `json:"deployment_target_id"`
-}
-
 // ServeHTTP handles POST requests to create a new deployment target
 func (c *CreateDeploymentTargetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	ctx, span := telemetry.NewSpan(r.Context(), "serve-create-deployment-target")
 	defer span.End()
 
 	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
-	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
 
+	cluster, clusterOk := ctx.Value(types.ClusterScope).(*models.Cluster)
 	if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) {
 		err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled")
 		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
 		return
 	}
 
-	request := &CreateDeploymentTargetRequest{}
+	request := &types.CreateDeploymentTargetRequest{}
 	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))
@@ -69,6 +56,16 @@ func (c *CreateDeploymentTargetHandler) ServeHTTP(w http.ResponseWriter, r *http
 		return
 	}
 
+	clusterId := request.ClusterId
+	if clusterOk {
+		clusterId = cluster.ID
+	}
+	if clusterId == 0 {
+		err := telemetry.Error(ctx, span, nil, "cluster id is required")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
 	name := request.Name
 	if name == "" {
 		name = request.Selector
@@ -76,7 +73,7 @@ func (c *CreateDeploymentTargetHandler) ServeHTTP(w http.ResponseWriter, r *http
 
 	createReq := connect.NewRequest(&porterv1.CreateDeploymentTargetRequest{
 		ProjectId: int64(project.ID),
-		ClusterId: int64(cluster.ID),
+		ClusterId: int64(clusterId),
 		Name:      name,
 		Namespace: name,
 		IsPreview: request.Preview,
@@ -99,7 +96,9 @@ func (c *CreateDeploymentTargetHandler) ServeHTTP(w http.ResponseWriter, r *http
 		return
 	}
 
-	res := &CreateDeploymentTargetResponse{
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-target-id", Value: ccpResp.Msg.DeploymentTargetId})
+
+	res := &types.CreateDeploymentTargetResponse{
 		DeploymentTargetID: ccpResp.Msg.DeploymentTargetId,
 	}
 

+ 21 - 17
api/server/handlers/deployment_target/list.go

@@ -28,16 +28,6 @@ func NewListDeploymentTargetsHandler(
 	}
 }
 
-// ListDeploymentTargetsRequest is the request object for the /deployment-targets GET endpoint
-type ListDeploymentTargetsRequest struct {
-	Preview bool `json:"preview"`
-}
-
-// ListDeploymentTargetsResponse is the response object for the /deployment-targets GET endpoint
-type ListDeploymentTargetsResponse struct {
-	DeploymentTargets []types.DeploymentTarget `json:"deployment_targets"`
-}
-
 func (c *ListDeploymentTargetsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	ctx, span := telemetry.NewSpan(r.Context(), "serve-list-deployment-targets")
 	defer span.End()
@@ -45,27 +35,41 @@ func (c *ListDeploymentTargetsHandler) ServeHTTP(w http.ResponseWriter, r *http.
 	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
 
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "cluster-provided", Value: cluster != nil})
+
 	if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) {
 		err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled")
 		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
 		return
 	}
 
-	request := &ListDeploymentTargetsRequest{}
+	request := &types.ListDeploymentTargetsRequest{}
 	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
 	}
 
-	deploymentTargets, err := c.Repo().DeploymentTarget().List(project.ID, cluster.ID, request.Preview)
-	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error retrieving deployment targets")
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
+	var deploymentTargets []*models.DeploymentTarget
+	var err error
+
+	if cluster != nil {
+		deploymentTargets, err = c.Repo().DeploymentTarget().ListForCluster(project.ID, cluster.ID, request.Preview)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error retrieving deployment targets for cluster")
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	} else {
+		deploymentTargets, err = c.Repo().DeploymentTarget().List(project.ID, request.Preview)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error retrieving deployment targets for project")
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
 	}
 
-	response := ListDeploymentTargetsResponse{
+	response := types.ListDeploymentTargetsResponse{
 		DeploymentTargets: make([]types.DeploymentTarget, 0),
 	}
 

+ 58 - 0
api/server/router/project.go

@@ -3,6 +3,8 @@ package router
 import (
 	"fmt"
 
+	"github.com/porter-dev/porter/api/server/handlers/deployment_target"
+
 	"github.com/go-chi/chi/v5"
 	apiContract "github.com/porter-dev/porter/api/server/handlers/api_contract"
 	"github.com/porter-dev/porter/api/server/handlers/api_token"
@@ -1740,5 +1742,61 @@ func getProjectRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/targets -> deployment_target.ListDeploymentTargetHandler
+	listDeploymentTargetEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/targets", relPath),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listDeploymentTargetHandler := deployment_target.NewListDeploymentTargetsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listDeploymentTargetEndpoint,
+		Handler:  listDeploymentTargetHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/targets -> deployment_target.ListDeploymentTargetHandler
+	createDeploymentTargetEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/targets", relPath),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	createDeploymentTargetHandler := deployment_target.NewCreateDeploymentTargetHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: createDeploymentTargetEndpoint,
+		Handler:  createDeploymentTargetHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 25 - 0
api/types/deployment_target.go

@@ -19,3 +19,28 @@ type DeploymentTarget struct {
 	CreatedAtUTC time.Time `json:"created_at"`
 	UpdatedAtUTC time.Time `json:"updated_at"`
 }
+
+// CreateDeploymentTargetRequest is the request object for the /deployment-targets POST endpoint
+type CreateDeploymentTargetRequest struct {
+	// Deprecated: use name instead
+	Selector string `json:"selector"`
+	Name     string `json:"name,omitempty"`
+	Preview  bool   `json:"preview"`
+	// required if using the project-scoped endpoint
+	ClusterId uint `json:"cluster_id"`
+}
+
+// CreateDeploymentTargetResponse is the response object for the /deployment-targets POST endpoint
+type CreateDeploymentTargetResponse struct {
+	DeploymentTargetID string `json:"deployment_target_id"`
+}
+
+// ListDeploymentTargetsRequest is the request object for the /deployment-targets GET endpoint
+type ListDeploymentTargetsRequest struct {
+	Preview bool `json:"preview"`
+}
+
+// ListDeploymentTargetsResponse is the response object for the /deployment-targets GET endpoint
+type ListDeploymentTargetsResponse struct {
+	DeploymentTargets []DeploymentTarget `json:"deployment_targets"`
+}

+ 1 - 0
cli/cmd/commands/all.go

@@ -46,6 +46,7 @@ func RegisterCommands() (*cobra.Command, error) {
 	rootCmd.AddCommand(registerCommand_Run(cliConf))
 	rootCmd.AddCommand(registerCommand_Server(cliConf))
 	rootCmd.AddCommand(registerCommand_Stack(cliConf))
+	rootCmd.AddCommand(registerCommand_Target(cliConf))
 	rootCmd.AddCommand(registerCommand_Update(cliConf))
 	rootCmd.AddCommand(registerCommand_Version(cliConf))
 	rootCmd.AddCommand(registerCommand_Env(cliConf))

+ 135 - 0
cli/cmd/commands/target.go

@@ -0,0 +1,135 @@
+package commands
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"sort"
+	"text/tabwriter"
+
+	"github.com/fatih/color"
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
+	"github.com/spf13/cobra"
+)
+
+func registerCommand_Target(cliConf config.CLIConfig) *cobra.Command {
+	targetCmd := &cobra.Command{
+		Use:     "target",
+		Aliases: []string{"targets"},
+		Short:   "Commands that control Porter target settings",
+	}
+
+	createTargetCmd := &cobra.Command{
+		Use:   "create --name [name]",
+		Short: "Creates a deployment target",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, createTarget)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	var targetName string
+	createTargetCmd.Flags().StringVar(&targetName, "name", "", "Name of deployment target")
+	targetCmd.AddCommand(createTargetCmd)
+
+	listTargetCmd := &cobra.Command{
+		Use:   "list",
+		Short: "Lists the deployment targets for the logged in user",
+		Long: `Lists the deployment targets in the project
+
+The following columns are returned:
+* ID:          id of the deployment target
+* NAME:        name of the deployment target
+* CLUSTER-ID:  id of the cluster associated with the deployment target
+* DEFAULT:     whether the deployment target is the default target for the cluster
+
+If the --preview flag is set, only deployment targets for preview environments will be returned.
+`,
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, listTargets)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	var includePreviews bool
+	listTargetCmd.Flags().BoolVar(&includePreviews, "preview", false, "List preview environments")
+	targetCmd.AddCommand(listTargetCmd)
+
+	return targetCmd
+}
+
+func createTarget(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, cmd *cobra.Command, args []string) error {
+	targetName, err := cmd.Flags().GetString("name")
+	if err != nil {
+		return fmt.Errorf("error finding name flag: %w", err)
+	}
+
+	resp, err := client.CreateDeploymentTarget(ctx, cliConf.Project, &types.CreateDeploymentTargetRequest{
+		Name:      targetName,
+		ClusterId: cliConf.Cluster,
+	})
+	if err != nil {
+		return err
+	}
+
+	_, _ = color.New(color.FgGreen).Printf("Created target with name %s and id %s\n", targetName, resp.DeploymentTargetID)
+
+	return nil
+}
+
+func listTargets(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, cmd *cobra.Command, args []string) error {
+	includePreviews, err := cmd.Flags().GetBool("preview")
+	if err != nil {
+		return fmt.Errorf("error finding preview flag: %w", err)
+	}
+
+	resp, err := client.ListDeploymentTargets(ctx, cliConf.Project, includePreviews)
+	if err != nil {
+		return err
+	}
+	if resp == nil {
+		return nil
+	}
+
+	targets := *resp
+
+	sort.Slice(targets.DeploymentTargets, func(i, j int) bool {
+		if targets.DeploymentTargets[i].ClusterID != targets.DeploymentTargets[j].ClusterID {
+			return targets.DeploymentTargets[i].ClusterID < targets.DeploymentTargets[j].ClusterID
+		}
+		return targets.DeploymentTargets[i].Name < targets.DeploymentTargets[j].Name
+	})
+
+	w := new(tabwriter.Writer)
+	w.Init(os.Stdout, 3, 8, 0, '\t', tabwriter.AlignRight)
+
+	if includePreviews {
+		fmt.Fprintf(w, "%s\t%s\t%s\n", "ID", "NAME", "CLUSTER-ID")
+		for _, target := range targets.DeploymentTargets {
+			fmt.Fprintf(w, "%s\t%s\t%d\n", target.ID, target.Name, target.ClusterID)
+		}
+	} else {
+		fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", "ID", "NAME", "CLUSTER-ID", "DEFAULT")
+		for _, target := range targets.DeploymentTargets {
+			fmt.Fprintf(w, "%s\t%s\t%d\t%s\n", target.ID, target.Name, target.ClusterID, checkmark(target.IsDefault))
+		}
+	}
+
+	_ = w.Flush()
+
+	return nil
+}
+
+func checkmark(b bool) string {
+	if b {
+		return "✓"
+	}
+
+	return ""
+}

+ 6 - 1
cli/cmd/v2/apply.go

@@ -375,7 +375,12 @@ func deploymentTargetFromConfig(ctx context.Context, client api.Client, projectI
 			return deploymentTargetID, errors.New("branch name is empty. Please run apply in a git repository with access to the git CLI")
 		}
 
-		targetResp, err := client.CreateDeploymentTarget(ctx, projectID, clusterID, branchName, true)
+		targetResp, err := client.CreateDeploymentTarget(ctx, projectID, &types.CreateDeploymentTargetRequest{
+			Selector:  "",
+			Name:      branchName,
+			Preview:   true,
+			ClusterId: clusterID,
+		})
 		if err != nil {
 			return deploymentTargetID, fmt.Errorf("error calling create deployment target endpoint: %w", err)
 		}

+ 3 - 1
internal/repository/deployment_target.go

@@ -8,8 +8,10 @@ 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)
+	// ListForCluster returns all deployment targets for a project and cluster
+	ListForCluster(projectID uint, clusterID uint, preview bool) ([]*models.DeploymentTarget, error)
 	// List returns all deployment targets for a project
-	List(projectID uint, clusterID uint, preview bool) ([]*models.DeploymentTarget, error)
+	List(projectID uint, preview bool) ([]*models.DeploymentTarget, error)
 	// CreateDeploymentTarget creates a new deployment target
 	CreateDeploymentTarget(deploymentTarget *models.DeploymentTarget) (*models.DeploymentTarget, error)
 	// DeploymentTarget retrieves a deployment target by its id if a uuid is provided or by name

+ 12 - 2
internal/repository/gorm/deployment_target.go

@@ -32,8 +32,8 @@ func (repo *DeploymentTargetRepository) DeploymentTargetBySelectorAndSelectorTyp
 	return deploymentTarget, nil
 }
 
-// List finds all deployment targets for a given project
-func (repo *DeploymentTargetRepository) List(projectID uint, clusterID uint, preview bool) ([]*models.DeploymentTarget, error) {
+// ListForCluster finds all deployment targets for a given project
+func (repo *DeploymentTargetRepository) ListForCluster(projectID uint, clusterID uint, preview bool) ([]*models.DeploymentTarget, error) {
 	deploymentTargets := []*models.DeploymentTarget{}
 	if err := repo.db.Where("project_id = ? AND cluster_id = ? AND preview = ?", projectID, clusterID, preview).Find(&deploymentTargets).Error; err != nil {
 		return nil, err
@@ -42,6 +42,16 @@ func (repo *DeploymentTargetRepository) List(projectID uint, clusterID uint, pre
 	return deploymentTargets, nil
 }
 
+// List finds all deployment targets for a given project
+func (repo *DeploymentTargetRepository) List(projectID uint, preview bool) ([]*models.DeploymentTarget, error) {
+	deploymentTargets := []*models.DeploymentTarget{}
+	if err := repo.db.Where("project_id = ? AND preview = ?", projectID, preview).Find(&deploymentTargets).Error; err != nil {
+		return nil, err
+	}
+
+	return deploymentTargets, nil
+}
+
 // DeploymentTarget finds all deployment targets for a given project
 func (repo *DeploymentTargetRepository) DeploymentTarget(projectID uint, deploymentTargetIdentifier string) (*models.DeploymentTarget, error) {
 	if deploymentTargetIdentifier == "" {

+ 6 - 1
internal/repository/test/deployment_target.go

@@ -22,8 +22,13 @@ func (repo *DeploymentTargetRepository) DeploymentTargetBySelectorAndSelectorTyp
 	return nil, errors.New("cannot read database")
 }
 
+// ListForCluster returns all deployment targets for a project
+func (repo *DeploymentTargetRepository) ListForCluster(projectID uint, clusterID uint, preview bool) ([]*models.DeploymentTarget, error) {
+	return nil, errors.New("cannot read database")
+}
+
 // List returns all deployment targets for a project
-func (repo *DeploymentTargetRepository) List(projectID uint, clusterID uint, preview bool) ([]*models.DeploymentTarget, error) {
+func (repo *DeploymentTargetRepository) List(projectID uint, preview bool) ([]*models.DeploymentTarget, error) {
 	return nil, errors.New("cannot read database")
 }