Jelajahi Sumber

add functionality for preview deletion, change update behavior

Alexander Belanger 4 tahun lalu
induk
melakukan
04cbc3a42b

+ 34 - 0
api/client/environment.go

@@ -45,6 +45,25 @@ func (c *Client) GetDeployment(
 	return resp, err
 }
 
+func (c *Client) UpdateDeployment(
+	ctx context.Context,
+	projID, gitInstallationID, clusterID uint,
+	req *types.UpdateDeploymentRequest,
+) (*types.Deployment, error) {
+	resp := &types.Deployment{}
+
+	err := c.postRequest(
+		fmt.Sprintf(
+			"/projects/%d/gitrepos/%d/clusters/%d/deployment/update",
+			projID, gitInstallationID, clusterID,
+		),
+		req,
+		resp,
+	)
+
+	return resp, err
+}
+
 func (c *Client) FinalizeDeployment(
 	ctx context.Context,
 	projID, gitInstallationID, clusterID uint,
@@ -63,3 +82,18 @@ func (c *Client) FinalizeDeployment(
 
 	return resp, err
 }
+
+func (c *Client) DeleteDeployment(
+	ctx context.Context,
+	projID, gitInstallationID, clusterID uint,
+	req *types.DeleteDeploymentRequest,
+) error {
+	return c.deleteRequest(
+		fmt.Sprintf(
+			"/projects/%d/gitrepos/%d/clusters/%d/deployment",
+			projID, gitInstallationID, clusterID,
+		),
+		req,
+		nil,
+	)
+}

+ 1 - 1
api/server/handlers/environment/create.go

@@ -81,7 +81,7 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	_, err = actions.SetupEnv(&actions.EnvOpts{
+	err = actions.SetupEnv(&actions.EnvOpts{
 		Client:            client,
 		ServerURL:         c.Config().ServerConf.ServerURL,
 		PorterToken:       encoded,

+ 44 - 35
api/server/handlers/environment/create_deployment.go

@@ -59,6 +59,46 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		return
 	}
 
+	ghDeployment, err := createDeployment(client, env, request.CreateGHDeploymentRequest)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// create the deployment
+	depl, err := c.Repo().Environment().CreateDeployment(&models.Deployment{
+		EnvironmentID:      env.ID,
+		Namespace:          request.Namespace,
+		Status:             "creating",
+		PullRequestID:      request.PullRequestID,
+		GitHubDeploymentID: ghDeployment.GetID(),
+	})
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// create the backing namespace
+	agent, err := c.GetAgent(r, cluster, "")
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	_, err = agent.CreateNamespace(depl.Namespace)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, depl.ToDeploymentType())
+}
+
+func createDeployment(client *github.Client, env *models.Environment, request *types.CreateGHDeploymentRequest) (*github.Deployment, error) {
 	branch := request.Branch
 	envName := "Preview"
 	automerge := false
@@ -79,8 +119,7 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	)
 
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
+		return nil, err
 	}
 
 	depID := deployment.GetID()
@@ -88,7 +127,7 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	// Create Deployment Status to indicate it's in progress
 
 	state := "in_progress"
-	log_url := fmt.Sprintf("https://github.com/%s/%s/actions/%d", env.GitRepoOwner, env.GitRepoName, request.ActionID)
+	log_url := fmt.Sprintf("https://github.com/%s/%s/runs/%d", env.GitRepoOwner, env.GitRepoName, request.ActionID)
 
 	deploymentStatusRequest := github.DeploymentStatusRequest{
 		State:  &state,
@@ -104,38 +143,8 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	)
 
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
+		return nil, err
 	}
 
-	// create the deployment
-	depl, err := c.Repo().Environment().CreateDeployment(&models.Deployment{
-		EnvironmentID:      env.ID,
-		Namespace:          request.Namespace,
-		Status:             "creating",
-		PullRequestID:      request.PullRequestID,
-		GitHubDeploymentID: depID,
-	})
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	// create the backing namespace
-	agent, err := c.GetAgent(r, cluster, "")
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	_, err = agent.CreateNamespace(depl.Namespace)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	c.WriteResult(w, r, depl.ToDeploymentType())
+	return deployment, nil
 }

+ 0 - 6
api/server/handlers/environment/delete.go

@@ -32,12 +32,6 @@ func (c *DeleteEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	request := &types.FinalizeDeploymentRequest{}
-
-	if ok := c.DecodeAndValidate(w, r, request); !ok {
-		return
-	}
-
 	// read the environment to get the environment id
 	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID))
 

+ 5 - 3
api/server/handlers/environment/delete_deployment.go

@@ -35,7 +35,7 @@ func (c *DeleteDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	request := &types.FinalizeDeploymentRequest{}
+	request := &types.DeleteDeploymentRequest{}
 
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
 		return
@@ -83,8 +83,10 @@ func (c *DeleteDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	// 	return
 	// }
 
-	// delete the deployment
-	depl, err = c.Repo().Environment().DeleteDeployment(depl)
+	depl.Status = "inactive"
+
+	// update the deployment to mark it inactive
+	depl, err = c.Repo().Environment().UpdateDeployment(depl)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 90 - 0
api/server/handlers/environment/update_deployment.go

@@ -0,0 +1,90 @@
+package environment
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"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/models/integrations"
+)
+
+type UpdateDeploymentHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewUpdateDeploymentHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *UpdateDeploymentHandler {
+	return &UpdateDeploymentHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *UpdateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ga, _ := r.Context().Value(types.GitInstallationScope).(*integrations.GithubAppInstallation)
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	request := &types.UpdateDeploymentRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	// read the environment to get the environment id
+	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID))
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// read the deployment
+	depl, err := c.Repo().Environment().ReadDeployment(env.ID, request.Namespace)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// create deployment on GitHub API
+	client, err := getGithubClientFromEnvironment(c.Config(), env)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	ghDeployment, err := createDeployment(client, env, request.CreateGHDeploymentRequest)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	depl.GitHubDeploymentID = ghDeployment.GetID()
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// create the deployment
+	depl, err = c.Repo().Environment().UpdateDeployment(depl)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, depl.ToDeploymentType())
+}

+ 31 - 0
api/server/router/git_installation.go

@@ -238,6 +238,37 @@ func getGitInstallationRoutes(
 		Router:   r,
 	})
 
+	// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/clusters/{cluster_id}/deployment/update ->
+	// environment.NewFinalizeDeploymentHandler
+	updateDeploymentEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/clusters/{cluster_id}/deployment/update",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.GitInstallationScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	updateDeploymentHandler := environment.NewUpdateDeploymentHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: updateDeploymentEndpoint,
+		Handler:  updateDeploymentHandler,
+		Router:   r,
+	})
+
 	// DELETE /api/projects/{project_id}/gitrepos/{git_installation_id}/clusters/{cluster_id}/environment ->
 	// environment.NewDeleteEnvironmentHandler
 	deleteEnvironmentEndpoint := factory.NewAPIEndpoint(

+ 17 - 2
api/types/environment.go

@@ -27,11 +27,16 @@ type Deployment struct {
 	GitHubDeploymentID int64  `json:"github_deployment_id"`
 }
 
+type CreateGHDeploymentRequest struct {
+	Branch   string `json:"branch" form:"required"`
+	ActionID uint   `json:"action_id" form:"required"`
+}
+
 type CreateDeploymentRequest struct {
+	*CreateGHDeploymentRequest
+
 	Namespace     string `json:"namespace" form:"required"`
 	PullRequestID uint   `json:"pull_request_id" form:"required"`
-	Branch        string `json:"branch" form:"required"`
-	ActionID      uint   `json:"action_id" form:"required"`
 }
 
 type FinalizeDeploymentRequest struct {
@@ -39,6 +44,16 @@ type FinalizeDeploymentRequest struct {
 	Subdomain string `json:"subdomain"`
 }
 
+type UpdateDeploymentRequest struct {
+	*CreateGHDeploymentRequest
+
+	Namespace string `json:"namespace" form:"required"`
+}
+
+type DeleteDeploymentRequest struct {
+	Namespace string `json:"namespace" form:"required"`
+}
+
 type GetDeploymentRequest struct {
 	Namespace string `schema:"namespace" form:"required"`
 }

+ 33 - 6
cli/cmd/apply.go

@@ -666,17 +666,44 @@ func NewDeploymentHook(client *api.Client, resourceGroup *switchboardTypes.Resou
 }
 
 func (t *DeploymentHook) PreApply() error {
-	_, err := t.client.CreateDeployment(
+	// attempt to read the deployment -- if it doesn't exist, create it
+	_, err := t.client.GetDeployment(
 		context.Background(),
 		t.projectID, t.gitInstallationID, t.clusterID,
-		&types.CreateDeploymentRequest{
-			Namespace:     t.namespace,
-			PullRequestID: t.prID,
-			Branch:        t.branch,
-			ActionID:      t.actionID,
+		&types.GetDeploymentRequest{
+			Namespace: t.namespace,
 		},
 	)
 
+	// TODO: case this on the response status code rather than text
+	if err != nil && strings.Contains(err.Error(), "deployment not found") {
+		// in this case, create the deployment
+		_, err = t.client.CreateDeployment(
+			context.Background(),
+			t.projectID, t.gitInstallationID, t.clusterID,
+			&types.CreateDeploymentRequest{
+				Namespace:     t.namespace,
+				PullRequestID: t.prID,
+				CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
+					Branch:   t.branch,
+					ActionID: t.actionID,
+				},
+			},
+		)
+	} else if err == nil {
+		_, err = t.client.UpdateDeployment(
+			context.Background(),
+			t.projectID, t.gitInstallationID, t.clusterID,
+			&types.UpdateDeploymentRequest{
+				Namespace: t.namespace,
+				CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
+					Branch:   t.branch,
+					ActionID: t.actionID,
+				},
+			},
+		)
+	}
+
 	return err
 }
 

+ 89 - 0
cli/cmd/delete.go

@@ -0,0 +1,89 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"strconv"
+
+	"github.com/fatih/color"
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/spf13/cobra"
+)
+
+// deleteCmd represents the "porter delete" base command
+var deleteCmd = &cobra.Command{
+	Use:   "delete",
+	Short: "Deletes a deployment",
+	Long: fmt.Sprintf(`
+%s
+
+Destroys a deployment, which is read based on env variables.
+
+  %s
+
+The following are the environment variables that can be used to set certain values while
+deleting a configuration:
+  PORTER_CLUSTER              Cluster ID that contains the project
+  PORTER_PROJECT              Project ID that contains the application
+  PORTER_GIT_INSTALLATION_ID  The Github installation ID that this deployment is associated with.
+  PORTER_NAMESPACE            The namespace associated with the deployment.
+	`,
+		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter delete\":"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter delete"),
+	),
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, delete)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(deleteCmd)
+}
+
+func delete(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+	projectID := config.Project
+
+	if projectID == 0 {
+		return fmt.Errorf("project id must be set")
+	}
+
+	clusterID := config.Cluster
+
+	if clusterID == 0 {
+		return fmt.Errorf("cluster id must be set")
+	}
+
+	deplNamespace := os.Getenv("PORTER_NAMESPACE")
+
+	if deplNamespace == "" {
+		return fmt.Errorf("namespace must be set by PORTER_NAMESPACE")
+	}
+
+	var ghID uint
+
+	if ghIDStr := os.Getenv("PORTER_GIT_INSTALLATION_ID"); ghIDStr != "" {
+		ghIDInt, err := strconv.Atoi(ghIDStr)
+
+		if err != nil {
+			return err
+		}
+
+		ghID = uint(ghIDInt)
+	} else if ghIDStr == "" {
+		return fmt.Errorf("Git installation ID must be defined, set by PORTER_GIT_INSTALLATION_ID")
+	}
+
+	return client.DeleteDeployment(
+		context.Background(),
+		projectID, clusterID, ghID,
+		&types.DeleteDeploymentRequest{
+			Namespace: deplNamespace,
+		},
+	)
+}

+ 63 - 11
internal/integrations/ci/actions/preview.go

@@ -20,7 +20,7 @@ type EnvOpts struct {
 	ProjectID, ClusterID, GitInstallationID uint
 }
 
-func SetupEnv(opts *EnvOpts) ([]byte, error) {
+func SetupEnv(opts *EnvOpts) error {
 	// create Github environment if it does not exist
 	_, resp, err := opts.Client.Repositories.GetEnvironment(
 		context.Background(),
@@ -39,10 +39,10 @@ func SetupEnv(opts *EnvOpts) ([]byte, error) {
 		)
 
 		if err != nil {
-			return nil, err
+			return err
 		}
 	} else if err != nil {
-		return nil, err
+		return err
 	}
 
 	// create porter token secret
@@ -55,7 +55,7 @@ func SetupEnv(opts *EnvOpts) ([]byte, error) {
 	)
 
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	// get the repository to find the default branch
@@ -66,21 +66,41 @@ func SetupEnv(opts *EnvOpts) ([]byte, error) {
 	)
 
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	defaultBranch := repo.GetDefaultBranch()
 
-	workflowYAML, err := getPreviewActionYAML(opts)
+	applyWorkflowYAML, err := getPreviewApplyActionYAML(opts)
 
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	_, err = commitGithubFile(
 		opts.Client,
 		fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
-		workflowYAML,
+		applyWorkflowYAML,
+		opts.GitRepoOwner,
+		opts.GitRepoName,
+		defaultBranch,
+		false,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	deleteWorkflowYAML, err := getPreviewApplyActionYAML(opts)
+
+	if err != nil {
+		return err
+	}
+
+	_, err = commitGithubFile(
+		opts.Client,
+		fmt.Sprintf("porter_%s_delete_env.yml", strings.ToLower(opts.EnvironmentName)),
+		deleteWorkflowYAML,
 		opts.GitRepoOwner,
 		opts.GitRepoName,
 		defaultBranch,
@@ -88,10 +108,10 @@ func SetupEnv(opts *EnvOpts) ([]byte, error) {
 	)
 
 	if err != nil {
-		return workflowYAML, err
+		return err
 	}
 
-	return workflowYAML, err
+	return err
 }
 
 func DeleteEnv(opts *EnvOpts) error {
@@ -118,7 +138,7 @@ func DeleteEnv(opts *EnvOpts) error {
 	)
 }
 
-func getPreviewActionYAML(opts *EnvOpts) ([]byte, error) {
+func getPreviewApplyActionYAML(opts *EnvOpts) ([]byte, error) {
 	gaSteps := []GithubActionYAMLStep{
 		getCheckoutCodeStep(),
 		getCreatePreviewEnvStep(
@@ -146,3 +166,35 @@ func getPreviewActionYAML(opts *EnvOpts) ([]byte, error) {
 
 	return yaml.Marshal(actionYAML)
 }
+
+func getPreviewDeleteActionYAML(opts *EnvOpts) ([]byte, error) {
+	gaSteps := []GithubActionYAMLStep{
+		getDeletePreviewEnvStep(
+			opts.ServerURL,
+			getPorterTokenSecretName(opts.ProjectID),
+			opts.ProjectID,
+			opts.ClusterID,
+			opts.GitInstallationID,
+			opts.GitRepoName,
+			// TODO: change to actual release version
+			"master",
+		),
+	}
+
+	actionYAML := GithubActionYAML{
+		On: map[string]interface{}{
+			"pull_request": map[string]interface{}{
+				"types": []string{"closed"},
+			},
+		},
+		Name: "Porter Preview Environment",
+		Jobs: map[string]GithubActionYAMLJob{
+			"porter-delete-preview": {
+				RunsOn: "ubuntu-latest",
+				Steps:  gaSteps,
+			},
+		},
+	}
+
+	return yaml.Marshal(actionYAML)
+}

+ 17 - 0
internal/integrations/ci/actions/steps.go

@@ -6,6 +6,7 @@ import (
 
 const updateAppActionName = "porter-dev/porter-update-action"
 const createPreviewActionName = "porter-dev/porter-preview-action"
+const deletePreviewActionName = "porter-dev/porter-delete-preview-action"
 
 func getCheckoutCodeStep() GithubActionYAMLStep {
 	return GithubActionYAMLStep{
@@ -57,3 +58,19 @@ func getCreatePreviewEnvStep(serverURL, porterTokenSecretName string, projectID,
 		Timeout: 30,
 	}
 }
+
+func getDeletePreviewEnvStep(serverURL, porterTokenSecretName string, projectID, clusterID, gitInstallationID uint, repoName, actionVersion string) GithubActionYAMLStep {
+	return GithubActionYAMLStep{
+		Name: "Delete Porter preview env",
+		Uses: fmt.Sprintf("%s@%s", deletePreviewActionName, actionVersion),
+		With: map[string]string{
+			"cluster":         fmt.Sprintf("%d", clusterID),
+			"host":            serverURL,
+			"project":         fmt.Sprintf("%d", projectID),
+			"token":           fmt.Sprintf("${{ secrets.%s }}", porterTokenSecretName),
+			"namespace":       fmt.Sprintf("pr-${{ github.event.pull_request.number }}-%s", repoName),
+			"installation_id": fmt.Sprintf("%d", gitInstallationID),
+		},
+		Timeout: 30,
+	}
+}