Browse Source

Merge pull request #2112 from porter-dev/nafees/protected-branch

[POR-523] Return a descriptive error in case of a protected branch
abelanger5 3 năm trước cách đây
mục cha
commit
8350f51d2d

+ 20 - 3
api/server/handlers/environment/create.go

@@ -1,6 +1,7 @@
 package environment
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -134,8 +135,18 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	})
 
 	if err != nil {
-		c.deleteEnvAndReportError(w, r, env, err)
-		return
+		unwrappedErr := errors.Unwrap(err)
+
+		if unwrappedErr != nil {
+			if errors.Is(unwrappedErr, actions.ErrProtectedBranch) {
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
+			} else if errors.Is(unwrappedErr, actions.ErrCreatePRForProtectedBranch) {
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusPreconditionFailed))
+			}
+		} else {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
 	}
 
 	c.WriteResult(w, r, env.ToEnvironmentType())
@@ -144,7 +155,13 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 func (c *CreateEnvironmentHandler) deleteEnvAndReportError(
 	w http.ResponseWriter, r *http.Request, env *models.Environment, err error,
 ) {
-	c.Repo().Environment().DeleteEnvironment(env)
+	_, delErr := c.Repo().Environment().DeleteEnvironment(env)
+
+	if delErr != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(delErr))
+		return
+	}
+
 	c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 }
 

+ 26 - 16
api/server/handlers/environment/delete.go

@@ -1,6 +1,8 @@
 package environment
 
 import (
+	"errors"
+	"fmt"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz"
@@ -58,22 +60,6 @@ func (c *DeleteEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	err = actions.DeleteEnv(&actions.EnvOpts{
-		Client:            client,
-		ServerURL:         c.Config().ServerConf.ServerURL,
-		GitRepoOwner:      env.GitRepoOwner,
-		GitRepoName:       env.GitRepoName,
-		ProjectID:         project.ID,
-		ClusterID:         cluster.ID,
-		GitInstallationID: uint(ga.InstallationID),
-		EnvironmentName:   env.Name,
-	})
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
 	// delete all corresponding deployments
 	agent, err := c.GetAgent(r, cluster, "")
 
@@ -101,5 +87,29 @@ func (c *DeleteEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
+	err = actions.DeleteEnv(&actions.EnvOpts{
+		Client:            client,
+		ServerURL:         c.Config().ServerConf.ServerURL,
+		GitRepoOwner:      env.GitRepoOwner,
+		GitRepoName:       env.GitRepoName,
+		ProjectID:         project.ID,
+		ClusterID:         cluster.ID,
+		GitInstallationID: uint(ga.InstallationID),
+		EnvironmentName:   env.Name,
+	})
+
+	if err != nil {
+		if errors.Is(err, actions.ErrProtectedBranch) {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("We were unable to delete the Porter Preview Environment workflow files for this "+
+					"repository as the default branch is protected. Please manually delete them."), http.StatusConflict,
+			))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
 	c.WriteResult(w, r, env.ToEnvironmentType())
 }

+ 26 - 19
api/server/handlers/release/create.go

@@ -2,6 +2,7 @@ package release
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"net/http"
 	"strings"
@@ -163,6 +164,15 @@ func (c *CreateReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		}
 	}
 
+	if request.BuildConfig != nil {
+		_, err = createBuildConfig(c.Config(), release, request.BuildConfig)
+	}
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
 	if request.GitActionConfig != nil {
 		_, _, err := createGitAction(
 			c.Config(),
@@ -176,20 +186,21 @@ func (c *CreateReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		)
 
 		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-			return
+			unwrappedErr := errors.Unwrap(err)
+
+			if unwrappedErr != nil {
+				if errors.Is(unwrappedErr, actions.ErrProtectedBranch) {
+					c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
+				} else if errors.Is(unwrappedErr, actions.ErrCreatePRForProtectedBranch) {
+					c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusPreconditionFailed))
+				}
+			} else {
+				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				return
+			}
 		}
 	}
 
-	if request.BuildConfig != nil {
-		_, err = createBuildConfig(c.Config(), release, request.BuildConfig)
-	}
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
 	c.Config().AnalyticsClient.Track(analytics.ApplicationLaunchSuccessTrack(
 		&analytics.ApplicationLaunchSuccessTrackOpts{
 			ApplicationScopedTrackOpts: analytics.GetApplicationScopedTrackOpts(
@@ -344,11 +355,11 @@ func createGitAction(
 		// need to call Setup() in order to get the workflow file before writing the
 		// action config, in the case of a dry run, since the dry run does not create
 		// a git action config.
-		workflowYAML, githubErr := gaRunner.Setup()
+		workflowYAML, gitErr = gaRunner.Setup()
 
 		if gaRunner.DryRun {
-			if githubErr != nil {
-				return nil, nil, githubErr
+			if gitErr != nil {
+				return nil, nil, gitErr
 			}
 
 			return nil, workflowYAML, nil
@@ -382,11 +393,7 @@ func createGitAction(
 		return nil, nil, err
 	}
 
-	if gitErr != nil {
-		return nil, nil, gitErr
-	}
-
-	return ga.ToGitActionConfigType(), workflowYAML, nil
+	return ga.ToGitActionConfigType(), workflowYAML, gitErr
 }
 
 func getToken(

+ 62 - 2
internal/integrations/ci/actions/actions.go

@@ -3,6 +3,7 @@ package actions
 import (
 	"context"
 	"encoding/base64"
+	"errors"
 	"fmt"
 	"net/http"
 
@@ -20,6 +21,11 @@ import (
 	"gopkg.in/yaml.v2"
 )
 
+var (
+	ErrProtectedBranch            = errors.New("protected branch")
+	ErrCreatePRForProtectedBranch = errors.New("unable to create PR to merge workflow files into protected branch")
+)
+
 type GithubActions struct {
 	ServerURL    string
 	InstanceName string
@@ -97,9 +103,63 @@ func (g *GithubActions) Setup() ([]byte, error) {
 			branch = g.defaultBranch
 		}
 
+		// check if the branch is protected
+		githubBranch, _, err := client.Repositories.GetBranch(
+			context.Background(),
+			g.GitRepoOwner,
+			g.GitRepoName,
+			branch,
+			true,
+		)
+
+		if err != nil {
+			return nil, err
+		}
+
 		isOAuth := g.GithubOAuthIntegration != nil
 
-		_, err = commitGithubFile(client, g.getPorterYMLFileName(), workflowYAML, g.GitRepoOwner, g.GitRepoName, branch, isOAuth)
+		if githubBranch.GetProtected() {
+			err = createNewBranch(client, g.GitRepoOwner, g.GitRepoName, branch, "porter-setup")
+
+			if err != nil {
+				return nil, fmt.Errorf(
+					"Unable to create PR to merge workflow files into protected branch: %s.\n"+
+						"To enable automatic deployments to Porter, please create a Github workflow "+
+						"file in this branch with the following contents:\n"+
+						"--------\n%s--------\nERROR: %w", branch, string(workflowYAML), ErrCreatePRForProtectedBranch,
+				)
+			}
+
+			_, err = commitWorkflowFile(client, g.getPorterYMLFileName(), workflowYAML, g.GitRepoOwner,
+				g.GitRepoName, "porter-setup", isOAuth)
+
+			if err != nil {
+				return nil, fmt.Errorf(
+					"Unable to create PR to merge workflow files into protected branch: %s.\n"+
+						"To enable automatic deployments to Porter, please create a Github workflow "+
+						"file in this branch with the following contents:\n"+
+						"--------\n%s--------\nERROR: %w", branch, string(workflowYAML), ErrCreatePRForProtectedBranch,
+				)
+			}
+
+			pr, _, err := client.PullRequests.Create(
+				context.Background(), g.GitRepoOwner, g.GitRepoName, &github.NewPullRequest{
+					Title: github.String("Enable Porter automatic deployments"),
+					Base:  github.String(branch),
+					Head:  github.String("porter-setup"),
+				},
+			)
+
+			if err != nil {
+				return nil, err
+			}
+
+			return nil, fmt.Errorf("Please merge %s to enable automatic deployments on Porter.\nERROR: %w",
+				pr.GetHTMLURL(), ErrProtectedBranch)
+		}
+
+		_, err = commitWorkflowFile(client, g.getPorterYMLFileName(), workflowYAML, g.GitRepoOwner,
+			g.GitRepoName, branch, isOAuth)
 		if err != nil {
 			return workflowYAML, err
 		}
@@ -382,7 +442,7 @@ func getPorterTokenSecretName(projectID uint) string {
 	return fmt.Sprintf("PORTER_TOKEN_%d", projectID)
 }
 
-func commitGithubFile(
+func commitWorkflowFile(
 	client *github.Client,
 	filename string,
 	contents []byte,

+ 117 - 24
internal/integrations/ci/actions/preview.go

@@ -79,7 +79,78 @@ func SetupEnv(opts *EnvOpts) error {
 		return err
 	}
 
-	_, err = commitGithubFile(
+	githubBranch, _, err := opts.Client.Repositories.GetBranch(
+		context.Background(), opts.GitRepoOwner, opts.GitRepoName, defaultBranch, true,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	if githubBranch.GetProtected() {
+		err = createNewBranch(opts.Client, opts.GitRepoOwner, opts.GitRepoName, defaultBranch, "porter-preview")
+
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to create PR to merge workflow files into protected branch: %s.\n"+
+					"To enable Porter Preview Environment deployments, please create Github workflow "+
+					"files in this branch with the following contents:\n"+
+					"--------\n%s--------\n--------\n%s--------\nERROR: %w",
+				defaultBranch, string(applyWorkflowYAML), string(deleteWorkflowYAML), ErrCreatePRForProtectedBranch,
+			)
+		}
+
+		_, err = commitWorkflowFile(
+			opts.Client,
+			fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
+			applyWorkflowYAML, opts.GitRepoOwner,
+			opts.GitRepoName, "porter-preview", false,
+		)
+
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to create PR to merge workflow files into protected branch: %s.\n"+
+					"To enable Porter Preview Environment deployments, please create Github workflow "+
+					"files in this branch with the following contents:\n"+
+					"--------\n%s--------\n--------\n%s--------\nERROR: %w",
+				defaultBranch, string(applyWorkflowYAML), string(deleteWorkflowYAML), ErrCreatePRForProtectedBranch,
+			)
+		}
+
+		_, err = commitWorkflowFile(
+			opts.Client,
+			fmt.Sprintf("porter_%s_delete_env.yml", strings.ToLower(opts.EnvironmentName)),
+			deleteWorkflowYAML, opts.GitRepoOwner,
+			opts.GitRepoName, "porter-preview", false,
+		)
+
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to create PR to merge workflow files into protected branch: %s.\n"+
+					"To enable Porter Preview Environment deployments, please create a Github workflow "+
+					"file in this branch with the following contents:\n"+
+					"--------\n%s--------\nERROR: %w",
+				defaultBranch, string(deleteWorkflowYAML), ErrCreatePRForProtectedBranch,
+			)
+		}
+
+		pr, _, err := opts.Client.PullRequests.Create(
+			context.Background(), opts.GitRepoOwner, opts.GitRepoName, &github.NewPullRequest{
+				Title: github.String("Enable Porter Preview Environment deployments"),
+				Base:  github.String(defaultBranch),
+				Head:  github.String("porter-preview"),
+			},
+		)
+
+		if err != nil {
+			return err
+		}
+
+		return fmt.Errorf("Please merge %s to enable Porter Preview Environment deployments.\nERROR: %w",
+			pr.GetHTMLURL(), ErrProtectedBranch)
+	}
+
+	_, err = commitWorkflowFile(
 		opts.Client,
 		fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
 		applyWorkflowYAML,
@@ -92,13 +163,13 @@ func SetupEnv(opts *EnvOpts) error {
 	if err != nil {
 		if strings.Contains(err.Error(), "409 Could not create file") {
 			// possibly a write-protected branch
-			err = createPorterPreviewBranch(opts, defaultBranch)
+			err = createNewBranch(opts.Client, opts.GitRepoOwner, opts.GitRepoName, defaultBranch, "porter-preview")
 
 			if err != nil {
 				return fmt.Errorf("write-protected branch %s. Error creating porter-preview branch: %w", defaultBranch, err)
 			}
 
-			_, err = commitGithubFile(
+			_, err = commitWorkflowFile(
 				opts.Client,
 				fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
 				applyWorkflowYAML,
@@ -112,7 +183,7 @@ func SetupEnv(opts *EnvOpts) error {
 				return fmt.Errorf("write-protected branch %s. Error committing to porter-preview branch: %w", defaultBranch, err)
 			}
 
-			_, err = commitGithubFile(
+			_, err = commitWorkflowFile(
 				opts.Client,
 				fmt.Sprintf("porter_%s_delete_env.yml", strings.ToLower(opts.EnvironmentName)),
 				deleteWorkflowYAML,
@@ -144,7 +215,7 @@ func SetupEnv(opts *EnvOpts) error {
 		return err
 	}
 
-	_, err = commitGithubFile(
+	_, err = commitWorkflowFile(
 		opts.Client,
 		fmt.Sprintf("porter_%s_delete_env.yml", strings.ToLower(opts.EnvironmentName)),
 		deleteWorkflowYAML,
@@ -197,6 +268,18 @@ func DeleteEnv(opts *EnvOpts) error {
 		}
 	}
 
+	githubBranch, _, err := opts.Client.Repositories.GetBranch(
+		context.Background(), opts.GitRepoOwner, opts.GitRepoName, defaultBranch, true,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	if githubBranch.GetProtected() {
+		return ErrProtectedBranch
+	}
+
 	err = deleteGithubFile(
 		opts.Client,
 		fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
@@ -310,36 +393,46 @@ func getPreviewDeleteActionYAML(opts *EnvOpts) ([]byte, error) {
 	return yaml.Marshal(actionYAML)
 }
 
-func createPorterPreviewBranch(opts *EnvOpts, defaultBranch string) error {
-	_, resp, err := opts.Client.Repositories.GetBranch(
-		context.Background(), opts.GitRepoOwner, opts.GitRepoName, "porter-preview", false,
+func createNewBranch(
+	client *github.Client,
+	gitRepoOwner, gitRepoName, baseBranch, headBranch string,
+) error {
+	_, resp, err := client.Repositories.GetBranch(
+		context.Background(), gitRepoOwner, gitRepoName, headBranch, true,
 	)
 
-	if resp.StatusCode == http.StatusNotFound {
-		branch, _, err := opts.Client.Repositories.GetBranch(
-			context.Background(), opts.GitRepoOwner, opts.GitRepoName, defaultBranch, false,
-		)
-
-		if err != nil {
-			return err
-		}
+	headBranchRef := fmt.Sprintf("refs/heads/%s", headBranch)
 
-		_, _, err = opts.Client.Git.CreateRef(
-			context.Background(), opts.GitRepoOwner, opts.GitRepoName, &github.Reference{
-				Ref: github.String("refs/heads/porter-preview"),
-				Object: &github.GitObject{
-					SHA: branch.Commit.SHA,
-				},
-			},
+	if err == nil {
+		// delete the stale branch
+		_, err := client.Git.DeleteRef(
+			context.Background(), gitRepoOwner, gitRepoName, headBranchRef,
 		)
 
 		if err != nil {
 			return err
 		}
+	} else if resp.StatusCode != http.StatusNotFound {
+		return err
+	}
+
+	base, _, err := client.Repositories.GetBranch(
+		context.Background(), gitRepoOwner, gitRepoName, baseBranch, true,
+	)
 
-		return nil
+	if err != nil {
+		return err
 	}
 
+	_, _, err = client.Git.CreateRef(
+		context.Background(), gitRepoOwner, gitRepoName, &github.Reference{
+			Ref: github.String(headBranchRef),
+			Object: &github.GitObject{
+				SHA: base.Commit.SHA,
+			},
+		},
+	)
+
 	if err != nil {
 		return err
 	}