Selaa lähdekoodia

Merge branch 'simplified-view' of github.com:porter-dev/porter into simplified-view

Justin Rhee 3 vuotta sitten
vanhempi
sitoutus
933721bcad

+ 124 - 0
api/server/handlers/stacks/create_secret_and_open_pr.go

@@ -0,0 +1,124 @@
+package stacks
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"github.com/bradleyfalzon/ghinstallation/v2"
+	"github.com/google/go-github/v41/github"
+	"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/commonutils"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/auth/token"
+	"github.com/porter-dev/porter/internal/integrations/ci/actions"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type OpenStackPRHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewOpenStackPRHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *OpenStackPRHandler {
+	return &OpenStackPRHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *OpenStackPRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	gaid := c.Config().GithubAppConf.AppID
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
+	if !ok {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("unable to get github owner and name params")))
+		return
+	}
+
+	// create the environment
+	request := &types.CreateSecretAndOpenGitHubPullRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	client, err := getGithubClient(c.Config(), gaid)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// generate porter jwt token
+	jwt, err := token.GetTokenForAPI(user.ID, project.ID)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting token for API: %w", err)))
+		return
+	}
+	encoded, err := jwt.EncodeToken(c.Config().TokenConf)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error encoding API token: %w", err)))
+		return
+	}
+
+	if request.OpenPr {
+		err = actions.OpenGithubPR(&actions.GithubPROpts{
+			Client:       client,
+			GitRepoOwner: owner,
+			GitRepoName:  name,
+			StackName:    request.StackName,
+			ProjectID:    project.ID,
+			ClusterID:    cluster.ID,
+			PorterToken:  encoded,
+			ServerURL:    c.Config().ServerConf.ServerURL,
+		})
+	}
+
+	if err != nil {
+		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(fmt.Errorf("error setting up preview environment in the github "+
+				"repo: %w", err)))
+			return
+		}
+	}
+
+	w.WriteHeader(http.StatusCreated)
+}
+
+func getGithubClient(config *config.Config, gitInstallationId int64) (*github.Client, error) {
+	// get the github app client
+	ghAppId, err := strconv.Atoi(config.ServerConf.GithubAppID)
+	if err != nil {
+		return nil, fmt.Errorf("malformed GITHUB_APP_ID in server configuration: %w", err)
+	}
+
+	// authenticate as github app installation
+	itr, err := ghinstallation.New(
+		http.DefaultTransport,
+		int64(ghAppId),
+		gitInstallationId,
+		config.ServerConf.GithubAppSecret,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error in creating github client for stack: %w", err)
+	}
+
+	return github.NewClient(&http.Client{Transport: itr}), nil
+}

+ 30 - 0
api/server/router/stack.go

@@ -139,5 +139,35 @@ func getStackRoutes(
 		Handler:  updateHandler,
 		Router:   r,
 	})
+
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{stack}/pr -> stacks.NewOpenStackPRHandler
+	createSecretAndOpenGitHubPullRequestEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/{stack}/pr",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	createSecretAndOpenGitHubPullRequestHandler := stacks.NewOpenStackPRHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: createSecretAndOpenGitHubPullRequestEndpoint,
+		Handler:  createSecretAndOpenGitHubPullRequestHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 0 - 15
api/types/release.go

@@ -218,18 +218,3 @@ type UpdateGitActionConfigRequest struct {
 type UpdateCanonicalNameRequest struct {
 	CanonicalName string `json:"canonical_name"`
 }
-
-type CreateStackReleaseRequest struct {
-	// The Helm values for this release
-	Values map[string]interface{} `json:"values"`
-	// Used to construct the Chart.yaml
-	Dependencies []Dependency `json:"dependencies form:"required"`
-	StackName    string       `json:"stack_name" form:"required,dns1123"`
-}
-
-type Dependency struct {
-	Name       string `json:"name" form:"required"`
-	Alias      string `json:"alias" form:"required"`
-	Version    string `json:"version" form:"required"`
-	Repository string `json:"repository" form:"required"`
-}

+ 21 - 0
api/types/stack.go

@@ -0,0 +1,21 @@
+package types
+
+type CreateStackReleaseRequest struct {
+	// The Helm values for this release
+	Values map[string]interface{} `json:"values"`
+	// Used to construct the Chart.yaml
+	Dependencies []Dependency `json:"dependencies" form:"required"`
+	StackName    string       `json:"stack_name" form:"required,dns1123"`
+}
+
+type Dependency struct {
+	Name       string `json:"name" form:"required"`
+	Alias      string `json:"alias" form:"required"`
+	Version    string `json:"version" form:"required"`
+	Repository string `json:"repository" form:"required"`
+}
+
+type CreateSecretAndOpenGitHubPullRequest struct {
+	OpenPr    bool `json:"open_pr"`
+	StackName string
+}

+ 145 - 0
internal/integrations/ci/actions/stack.go

@@ -0,0 +1,145 @@
+package actions
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/google/go-github/v41/github"
+	"gopkg.in/yaml.v2"
+)
+
+type GithubPROpts struct {
+	Client                    *github.Client
+	GitRepoOwner, GitRepoName string
+	ApplyWorkflowYAML         string
+	StackName                 string
+	ProjectID, ClusterID      uint
+	PorterToken               string
+	ServerURL                 string
+}
+
+type GetStackApplyActionYAMLOpts struct {
+	ServerURL            string
+	StackName            string
+	ProjectID, ClusterID uint
+	DefaultBranch        string
+	SecretName           string
+}
+
+func OpenGithubPR(opts *GithubPROpts) error {
+	// create porter secret
+	secretName := fmt.Sprintf("PORTER_STACK_%d_%d", opts.ProjectID, opts.ClusterID)
+	err := createGithubSecret(
+		opts.Client,
+		secretName,
+		opts.PorterToken,
+		opts.GitRepoOwner,
+		opts.GitRepoName,
+	)
+	if err != nil {
+		return err
+	}
+
+	// get the repository to find the default branch
+	repo, _, err := opts.Client.Repositories.Get(
+		context.TODO(),
+		opts.GitRepoOwner,
+		opts.GitRepoName,
+	)
+	if err != nil {
+		return err
+	}
+
+	defaultBranch := repo.GetDefaultBranch()
+
+	applyWorkflowYAML, err := getStackApplyActionYAML(&GetStackApplyActionYAMLOpts{
+		ServerURL:     opts.ServerURL,
+		ClusterID:     opts.ClusterID,
+		ProjectID:     opts.ProjectID,
+		StackName:     opts.StackName,
+		DefaultBranch: defaultBranch,
+		SecretName:    secretName,
+	})
+	if err != nil {
+		return err
+	}
+
+	err = createNewBranch(opts.Client,
+		opts.GitRepoOwner,
+		opts.GitRepoName,
+		defaultBranch,
+		"porter-stack")
+	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--------\nERROR: %w",
+			defaultBranch, string(applyWorkflowYAML), ErrCreatePRForProtectedBranch,
+		)
+	}
+
+	_, err = commitWorkflowFile(
+		opts.Client,
+		fmt.Sprintf("porter_stack_%s.yml", strings.ToLower(opts.StackName)),
+		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--------\nERROR: %w",
+			defaultBranch, string(applyWorkflowYAML), ErrCreatePRForProtectedBranch,
+		)
+	}
+
+	_, _, 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 nil
+}
+
+func getStackApplyActionYAML(opts *GetStackApplyActionYAMLOpts) ([]byte, error) {
+	gaSteps := []GithubActionYAMLStep{
+		getCheckoutCodeStep(),
+		getSetTagStep(),
+		getDeployStackStep(
+			opts.ServerURL,
+			opts.SecretName,
+			opts.StackName,
+			"v0.1.0",
+			opts.ProjectID,
+			opts.ClusterID,
+		),
+	}
+
+	actionYAML := GithubActionYAML{
+		On: GithubActionYAMLOnPush{
+			Push: GithubActionYAMLOnPushBranches{
+				Branches: []string{
+					opts.DefaultBranch,
+				},
+			},
+		},
+		Name: "Deploy to Porter",
+		Jobs: map[string]GithubActionYAMLJob{
+			"porter-deploy": {
+				RunsOn: "ubuntu-latest",
+				Steps:  gaSteps,
+			},
+		},
+	}
+
+	return yaml.Marshal(actionYAML)
+}

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

@@ -8,6 +8,7 @@ import (
 const (
 	updateAppActionName     = "porter-dev/porter-update-action"
 	createPreviewActionName = "porter-dev/porter-preview-action"
+	cliActionName           = "porter-dev/porter-cli-action"
 )
 
 func getCheckoutCodeStep() GithubActionYAMLStep {
@@ -69,3 +70,25 @@ func getCreatePreviewEnvStep(
 		Timeout: 30,
 	}
 }
+
+func getDeployStackStep(
+	serverURL, porterTokenSecretName, stackName, actionVersion string,
+	projectID, clusterID uint,
+) GithubActionYAMLStep {
+	return GithubActionYAMLStep{
+		Name: "Deploy stack",
+		Uses: fmt.Sprintf("%s@%s", cliActionName, actionVersion),
+		With: map[string]string{
+			"command": "apply -f porter.yaml",
+		},
+		Env: map[string]string{
+			"PORTER_CLUSTER":    fmt.Sprintf("%d", clusterID),
+			"PORTER_HOST":       serverURL,
+			"PORTER_PROJECT":    fmt.Sprintf("%d", projectID),
+			"PORTER_TOKEN":      fmt.Sprintf("${{ secrets.%s }}", porterTokenSecretName),
+			"PORTER_TAG":        "${{ steps.vars.outputs.sha_short }}",
+			"PORTER_STACK_NAME": stackName,
+		},
+		Timeout: 30,
+	}
+}