Jelajahi Sumber

update api endpoints and cli to use github metadata

Alexander Belanger 4 tahun lalu
induk
melakukan
f20c43371c

+ 19 - 0
api/client/environment.go

@@ -26,6 +26,25 @@ func (c *Client) CreateDeployment(
 	return resp, err
 }
 
+func (c *Client) GetDeployment(
+	ctx context.Context,
+	projID, gitInstallationID, clusterID uint,
+	req *types.GetDeploymentRequest,
+) (*types.Deployment, error) {
+	resp := &types.Deployment{}
+
+	err := c.getRequest(
+		fmt.Sprintf(
+			"/projects/%d/gitrepos/%d/clusters/%d/deployment",
+			projID, gitInstallationID, clusterID,
+		),
+		req,
+		resp,
+	)
+
+	return resp, err
+}
+
 func (c *Client) FinalizeDeployment(
 	ctx context.Context,
 	projID, gitInstallationID, clusterID uint,

+ 6 - 1
api/server/authz/git_installation.go

@@ -2,11 +2,13 @@ package authz
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net/http"
 
 	"github.com/google/go-github/github"
 	"golang.org/x/oauth2"
+	"gorm.io/gorm"
 
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
@@ -45,7 +47,10 @@ func (p *GitInstallationScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *ht
 
 	gitInstallation, err := p.config.Repo.GithubAppInstallation().ReadGithubAppInstallationByInstallationID(gitInstallationID)
 
-	if err != nil {
+	if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
+		apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrForbidden(err), true)
+		return
+	} else if err != nil {
 		apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err), true)
 		return
 	}

+ 36 - 0
api/server/handlers/environment/create.go

@@ -2,7 +2,10 @@ package environment
 
 import (
 	"net/http"
+	"strconv"
 
+	"github.com/bradleyfalzon/ghinstallation"
+	"github.com/google/go-github/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"
@@ -43,6 +46,8 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		ClusterID:         cluster.ID,
 		GitInstallationID: uint(ga.InstallationID),
 		Name:              request.Name,
+		GitRepoOwner:      request.GitRepoOwner,
+		GitRepoName:       request.GitRepoName,
 	})
 
 	if err != nil {
@@ -50,5 +55,36 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
+	// TODO: upon environment creation, write Github actions files to the repo
+	// client, err := getGithubClientFromEnvironment(c.Config(), env)
+
+	// if err != nil {
+	// 	c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+	// 	return
+	// }
+
 	c.WriteResult(w, r, env.ToEnvironmentType())
 }
+
+func getGithubClientFromEnvironment(config *config.Config, env *models.Environment) (*github.Client, error) {
+	// get the github app client
+	ghAppId, err := strconv.Atoi(config.ServerConf.GithubAppID)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// authenticate as github app installation
+	itr, err := ghinstallation.NewKeyFromFile(
+		http.DefaultTransport,
+		int64(ghAppId),
+		int64(env.GitInstallationID),
+		config.ServerConf.GithubAppSecretPath,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return github.NewClient(&http.Client{Transport: itr}), nil
+}

+ 1 - 0
api/server/handlers/environment/create_deployment.go

@@ -50,6 +50,7 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		EnvironmentID: env.ID,
 		Namespace:     request.Namespace,
 		Status:        "creating",
+		PullRequestID: request.PullRequestID,
 	})
 
 	if err != nil {

+ 28 - 1
api/server/handlers/environment/finalize_deployment.go

@@ -1,8 +1,10 @@
 package environment
 
 import (
+	"context"
 	"net/http"
 
+	"github.com/google/go-github/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"
@@ -64,7 +66,32 @@ func (c *FinalizeDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 		return
 	}
 
-	// TODO: call Github client here using the env.GitInstallationID which is the installation ID
+	client, err := getGithubClientFromEnvironment(c.Config(), env)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// write comment in PR
+	commentBody := "(TENTATIVE) Porter is deploying this pull request..."
+	prComment := github.IssueComment{
+		Body: &commentBody,
+		User: &github.User{},
+	}
+
+	_, _, err = client.Issues.CreateComment(
+		context.Background(),
+		env.GitRepoOwner,
+		env.GitRepoName,
+		int(depl.PullRequestID),
+		&prComment,
+	)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
 
 	c.WriteResult(w, r, depl.ToDeploymentType())
 }

+ 71 - 0
api/server/handlers/environment/get_deployment.go

@@ -0,0 +1,71 @@
+package environment
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"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"
+	"gorm.io/gorm"
+)
+
+type GetDeploymentHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewGetDeploymentHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetDeploymentHandler {
+	return &GetDeploymentHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *GetDeploymentHandler) 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.GetDeploymentRequest{}
+
+	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 && errors.Is(err, gorm.ErrRecordNotFound) {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("environment not found: is the environment enabled for this git installation?"),
+			http.StatusNotFound,
+		))
+		return
+	} else if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	depl, err := c.Repo().Environment().ReadDeployment(env.ID, request.Namespace)
+
+	if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("deployment not found"),
+			http.StatusNotFound,
+		))
+		return
+	} else 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

@@ -145,6 +145,37 @@ func getGitInstallationRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/gitrepos/{git_installation_id}/clusters/{cluster_id}/deployment ->
+	// environment.NewCreateDeploymentHandler
+	getDeploymentEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/clusters/{cluster_id}/deployment",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.GitInstallationScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getDeploymentHandler := environment.NewGetDeploymentHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: getDeploymentEndpoint,
+		Handler:  getDeploymentHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/clusters/{cluster_id}/deployment/finalize ->
 	// environment.NewFinalizeDeploymentHandler
 	finalizeDeploymentEndpoint := factory.NewAPIEndpoint(

+ 16 - 6
api/types/environment.go

@@ -1,16 +1,20 @@
 package types
 
 type Environment struct {
-	ID                uint `json:"id"`
-	ProjectID         uint `json:"project_id"`
-	ClusterID         uint `json:"cluster_id"`
-	GitInstallationID uint `json:"git_installation_id"`
+	ID                uint   `json:"id"`
+	ProjectID         uint   `json:"project_id"`
+	ClusterID         uint   `json:"cluster_id"`
+	GitInstallationID uint   `json:"git_installation_id"`
+	GitRepoOwner      string `json:"git_repo_owner"`
+	GitRepoName       string `json:"git_repo_name"`
 
 	Name string `json:"name"`
 }
 
 type CreateEnvironmentRequest struct {
-	Name string `json:"name" form:"required"`
+	Name         string `json:"name" form:"required"`
+	GitRepoOwner string `json:"git_repo_owner" form:"required"`
+	GitRepoName  string `json:"git_repo_name" form:"required"`
 }
 
 type Deployment struct {
@@ -19,13 +23,19 @@ type Deployment struct {
 	Namespace     string `json:"namespace"`
 	Status        string `json:"status"`
 	Subdomain     string `json:"subdomain"`
+	PullRequestID uint   `json:"pull_request_id"`
 }
 
 type CreateDeploymentRequest struct {
-	Namespace string `json:"namespace" form:"required"`
+	Namespace     string `json:"namespace" form:"required"`
+	PullRequestID uint   `json:"pull_request_id" form:"required"`
 }
 
 type FinalizeDeploymentRequest struct {
 	Namespace string `json:"namespace" form:"required"`
 	Subdomain string `json:"subdomain"`
 }
+
+type GetDeploymentRequest struct {
+	Namespace string `schema:"namespace" form:"required"`
+}

+ 120 - 0
cli/cmd/apply.go

@@ -7,6 +7,7 @@ import (
 	"os"
 	"path/filepath"
 	"strconv"
+	"strings"
 
 	"github.com/cli/cli/git"
 	"github.com/fatih/color"
@@ -95,6 +96,20 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []str
 	worker.RegisterDriver("porter.deploy", NewPorterDriver)
 	worker.SetDefaultDriver("porter.deploy")
 
+	deplNamespace := os.Getenv("PORTER_NAMESPACE")
+
+	if deplNamespace == "" {
+		return fmt.Errorf("namespace must be set by PORTER_NAMESPACE")
+	}
+
+	deploymentHook, err := NewDeploymentHook(client, resGroup, deplNamespace)
+
+	if err != nil {
+		return err
+	}
+
+	worker.RegisterHook("deployment", deploymentHook)
+
 	return worker.Apply(resGroup, &switchboardTypes.ApplyOpts{
 		BasePath: basePath,
 	})
@@ -167,6 +182,8 @@ func (d *Driver) ShouldApply(resource *models.Resource) bool {
 }
 
 func (d *Driver) Apply(resource *models.Resource) (*models.Resource, error) {
+	// TODO: call driver ConstructConfig
+
 	client := GetAPIClient(config)
 
 	if resource.Name == "" {
@@ -471,3 +488,106 @@ func existsInRepo(name, version, url string) (map[string]interface{}, error) {
 	}
 	return chart.Values, nil
 }
+
+type DeploymentHook struct {
+	client                                        *api.Client
+	resourceGroup                                 *switchboardTypes.ResourceGroup
+	gitInstallationID, projectID, clusterID, prID uint
+	namespace                                     string
+}
+
+func NewDeploymentHook(client *api.Client, resourceGroup *switchboardTypes.ResourceGroup, namespace string) (*DeploymentHook, error) {
+	res := &DeploymentHook{
+		client:        client,
+		resourceGroup: resourceGroup,
+		namespace:     namespace,
+	}
+
+	if ghIDStr := os.Getenv("PORTER_GIT_INSTALLATION_ID"); ghIDStr != "" {
+		ghID, err := strconv.Atoi(ghIDStr)
+
+		if err != nil {
+			return nil, err
+		}
+
+		res.gitInstallationID = uint(ghID)
+	} else if ghIDStr == "" {
+		return nil, fmt.Errorf("Git installation ID must be defined, set by PORTER_GIT_INSTALLATION_ID")
+	}
+
+	if prIDStr := os.Getenv("PORTER_PULL_REQUEST_ID"); prIDStr != "" {
+		prID, err := strconv.Atoi(prIDStr)
+
+		if err != nil {
+			return nil, err
+		}
+
+		res.prID = uint(prID)
+	} else if prIDStr == "" {
+		return nil, fmt.Errorf("Pull request ID must be defined, set by PORTER_PULL_REQUEST_ID")
+	}
+
+	res.projectID = config.Project
+
+	if res.projectID == 0 {
+		return nil, fmt.Errorf("project id must be set")
+	}
+
+	res.clusterID = config.Cluster
+
+	if res.clusterID == 0 {
+		return nil, fmt.Errorf("cluster id must be set")
+	}
+
+	return res, nil
+}
+
+func (t *DeploymentHook) PreApply() error {
+	// 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.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,
+			},
+		)
+
+		return err
+	}
+
+	return err
+}
+
+func (t *DeploymentHook) DataQueries() map[string]interface{} {
+	// TODO: use the resource group to find all web applications that can have an exposed subdomain
+	return map[string]interface{}{
+		"first": "{ .test-deployment.spec.replicas }",
+	}
+}
+
+func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
+	// finalize the deployment
+	_, err := t.client.FinalizeDeployment(
+		context.Background(),
+		t.projectID, t.gitInstallationID, t.clusterID,
+		&types.FinalizeDeploymentRequest{
+			Namespace: t.namespace,
+			// TODO: populate the subdomain based on the query
+			Subdomain: "google.com",
+		},
+	)
+
+	return err
+}

+ 2 - 0
cmd/migrate/keyrotate/helpers_test.go

@@ -61,6 +61,8 @@ func setupTestEnv(tester *tester, t *testing.T) {
 		&models.GitRepo{},
 		&models.Registry{},
 		&models.Release{},
+		&models.Environment{},
+		&models.Deployment{},
 		&models.HelmRepo{},
 		&models.Cluster{},
 		&models.ClusterCandidate{},

+ 1 - 1
go.mod

@@ -44,7 +44,7 @@ require (
 	github.com/opencontainers/image-spec v1.0.1
 	github.com/pelletier/go-toml v1.9.4 // indirect
 	github.com/pkg/errors v0.9.1
-	github.com/porter-dev/switchboard v0.0.0-20211206152649-b6792e4dc331
+	github.com/porter-dev/switchboard v0.0.0-20211208133739-316acab6516d
 	github.com/rs/zerolog v1.26.0
 	github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 // indirect
 	github.com/sendgrid/rest v2.6.3+incompatible // indirect

+ 2 - 0
go.sum

@@ -1175,6 +1175,8 @@ github.com/porter-dev/switchboard v0.0.0-20211203120508-691813a27c1b h1:xcM7l5Bz
 github.com/porter-dev/switchboard v0.0.0-20211203120508-691813a27c1b/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/porter-dev/switchboard v0.0.0-20211206152649-b6792e4dc331 h1:winqj0G5++gjklyo/lZtxqEMtQbj2eiIrwOzEESaFGI=
 github.com/porter-dev/switchboard v0.0.0-20211206152649-b6792e4dc331/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
+github.com/porter-dev/switchboard v0.0.0-20211208133739-316acab6516d h1:IKyljDrQPhCOzfnmp5r9KeY67X7KQo70aH8NNEfSomY=
+github.com/porter-dev/switchboard v0.0.0-20211208133739-316acab6516d/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
 github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=

+ 6 - 0
internal/models/deployment.go

@@ -11,6 +11,8 @@ type Environment struct {
 	ProjectID         uint
 	ClusterID         uint
 	GitInstallationID uint
+	GitRepoOwner      string
+	GitRepoName       string
 
 	Name string
 }
@@ -21,6 +23,8 @@ func (e *Environment) ToEnvironmentType() *types.Environment {
 		ProjectID:         e.ProjectID,
 		ClusterID:         e.ClusterID,
 		GitInstallationID: e.GitInstallationID,
+		GitRepoOwner:      e.GitRepoOwner,
+		GitRepoName:       e.GitRepoName,
 		Name:              e.Name,
 	}
 }
@@ -32,6 +36,7 @@ type Deployment struct {
 	Namespace     string
 	Status        string
 	Subdomain     string
+	PullRequestID uint
 }
 
 func (d *Deployment) ToDeploymentType() *types.Deployment {
@@ -41,5 +46,6 @@ func (d *Deployment) ToDeploymentType() *types.Deployment {
 		Namespace:     d.Namespace,
 		Status:        d.Status,
 		Subdomain:     d.Subdomain,
+		PullRequestID: d.PullRequestID,
 	}
 }

+ 1 - 1
internal/repository/gorm/environment.go

@@ -56,7 +56,7 @@ func (repo *EnvironmentRepository) UpdateDeployment(deployment *models.Deploymen
 
 func (repo *EnvironmentRepository) ReadDeployment(environmentID uint, namespace string) (*models.Deployment, error) {
 	depl := &models.Deployment{}
-	if err := repo.db.Order("id desc").Where("environment_id = ? AND namespace = ?", environmentID).First(&depl).Error; err != nil {
+	if err := repo.db.Order("id desc").Where("environment_id = ? AND namespace = ?", environmentID, namespace).First(&depl).Error; err != nil {
 		return nil, err
 	}
 	return depl, nil

+ 2 - 0
internal/repository/gorm/helpers_test.go

@@ -59,6 +59,8 @@ func setupTestEnv(tester *tester, t *testing.T) {
 		&models.GitRepo{},
 		&models.Registry{},
 		&models.Release{},
+		&models.Environment{},
+		&models.Deployment{},
 		&models.HelmRepo{},
 		&models.Cluster{},
 		&models.ClusterCandidate{},

+ 2 - 0
internal/repository/gorm/migrate.go

@@ -13,6 +13,8 @@ func AutoMigrate(db *gorm.DB) error {
 		&models.Role{},
 		&models.User{},
 		&models.Release{},
+		&models.Environment{},
+		&models.Deployment{},
 		&models.Session{},
 		&models.GitRepo{},
 		&models.Registry{},