Просмотр исходного кода

laying groundwork for github action

Feroze Mohideen 3 лет назад
Родитель
Сommit
577b7763cf

+ 186 - 0
api/server/handlers/stacks/open_stack_pr.go

@@ -0,0 +1,186 @@
+package stacks
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"strconv"
+	"strings"
+
+	"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/encryption"
+	"github.com/porter-dev/porter/internal/integrations/ci/actions"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/models/integrations"
+)
+
+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) {
+	ga, _ := r.Context().Value(types.GitInstallationScope).(*integrations.GithubAppInstallation)
+	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 {
+		return
+	}
+
+	// create the environment
+	request := &types.CreateEnvironmentRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	// create a random webhook id
+	webhookUID, err := encryption.GenerateRandomBytes(32)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error generating webhook UID for new stack: %w", err)))
+		return
+	}
+
+	env := &models.Environment{
+		ProjectID:           project.ID,
+		ClusterID:           cluster.ID,
+		GitInstallationID:   uint(ga.InstallationID),
+		Name:                request.Name,
+		GitRepoOwner:        owner,
+		GitRepoName:         name,
+		GitRepoBranches:     strings.Join(request.GitRepoBranches, ","),
+		Mode:                request.Mode,
+		WebhookID:           string(webhookUID),
+		NewCommentsDisabled: request.DisableNewComments,
+		GitDeployBranches:   strings.Join(request.GitDeployBranches, ","),
+	}
+
+	client, err := getGithubClient(c.Config(), ga.InstallationID)
+	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 {
+		_, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())
+
+		if deleteErr != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
+				http.StatusConflict, "error getting token for API while creating environment"))
+			return
+		}
+
+		_, deleteErr = c.Repo().Environment().DeleteEnvironment(env)
+
+		if deleteErr != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deleting created preview environment: %w",
+				deleteErr)))
+			return
+		}
+
+		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 {
+		_, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())
+
+		if deleteErr != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
+				http.StatusConflict, "error encoding token while creating environment"))
+			return
+		}
+
+		_, deleteErr = c.Repo().Environment().DeleteEnvironment(env)
+
+		if deleteErr != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deleting created preview environment: %w",
+				deleteErr)))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error encoding API token: %w", err)))
+		return
+	}
+
+	err = actions.SetupEnv(&actions.EnvOpts{
+		Client:            client,
+		ServerURL:         c.Config().ServerConf.ServerURL,
+		PorterToken:       encoded,
+		GitRepoOwner:      owner,
+		GitRepoName:       name,
+		ProjectID:         project.ID,
+		ClusterID:         cluster.ID,
+		GitInstallationID: uint(ga.InstallationID),
+		EnvironmentName:   request.Name,
+		InstanceName:      c.Config().ServerConf.InstanceName,
+	})
+
+	err = actions.OpenGithubPR(&actions.OpenGithubPROpts{
+		Client: client,
+	})
+
+	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
+}

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

@@ -110,5 +110,36 @@ func getStackRoutes(
 		Handler:  updateHandler,
 		Router:   r,
 	})
+
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{stack}/pr -> stacks.NewOpenStackPRHandler
+	openPREndpoint := 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,
+				types.GitInstallationScope,
+			},
+		},
+	)
+
+	openPRHandler := stacks.NewOpenStackPRHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: openPREndpoint,
+		Handler:  openPRHandler,
+		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"`
-}

+ 16 - 0
api/types/stack.go

@@ -0,0 +1,16 @@
+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"`
+}

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

@@ -0,0 +1,133 @@
+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
+}
+
+func OpenGithubPR(opts GithubPROpts) error {
+	// 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()
+
+	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_%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--------\nERROR: %w",
+			defaultBranch, string(applyWorkflowYAML), 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 nil
+}
+
+func getStackApplyActionYAML(opts *EnvOpts) ([]byte, error) {
+	gaSteps := []GithubActionYAMLStep{
+		getCheckoutCodeStep(),
+		getCreatePreviewEnvStep(
+			opts.ServerURL,
+			getPreviewEnvSecretName(opts.ProjectID, opts.ClusterID, opts.InstanceName),
+			opts.ProjectID,
+			opts.ClusterID,
+			opts.GitInstallationID,
+			opts.GitRepoOwner,
+			opts.GitRepoName,
+			"v0.2.1",
+		),
+	}
+
+	actionYAML := GithubActionYAML{
+		On: map[string]interface{}{
+			"workflow_dispatch": map[string]interface{}{
+				"inputs": map[string]interface{}{
+					"pr_number": map[string]interface{}{
+						"description": "Pull request number",
+						"type":        "string",
+						"required":    true,
+					},
+					"pr_title": map[string]interface{}{
+						"description": "Pull request title",
+						"type":        "string",
+						"required":    true,
+					},
+					"pr_branch_from": map[string]interface{}{
+						"description": "Pull request head branch",
+						"type":        "string",
+						"required":    true,
+					},
+					"pr_branch_into": map[string]interface{}{
+						"description": "Pull request base branch",
+						"type":        "string",
+						"required":    true,
+					},
+				},
+			},
+		},
+		Name: "Porter Preview Environment",
+		Jobs: map[string]GithubActionYAMLJob{
+			"porter-preview": {
+				RunsOn: "ubuntu-latest",
+				Concurrency: map[string]string{
+					"group": "${{ github.workflow }}-${{ github.event.inputs.pr_number }}",
+				},
+				Steps: gaSteps,
+			},
+		},
+	}
+
+	return yaml.Marshal(actionYAML)
+}