Преглед на файлове

delete preview deployment targets on pr close

Ian Edwards преди 2 години
родител
ревизия
70c8a37c8b

+ 68 - 0
api/server/handlers/deployment_target/delete.go

@@ -0,0 +1,68 @@
+package deployment_target
+
+import (
+	"net/http"
+
+	"connectrpc.com/connect"
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+	"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/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+type DeleteDeploymentTargetHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewDeleteDeploymentTargetHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *DeleteDeploymentTargetHandler {
+	return &DeleteDeploymentTargetHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *DeleteDeploymentTargetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "server-delete-deployment-target-by-id")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	deploymentTargetID, reqErr := requestutils.GetURLParamString(r, types.URLParamDeploymentTargetID)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, reqErr, "error parsing deployment target id")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	if deploymentTargetID == "" {
+		err := telemetry.Error(ctx, span, nil, "deployment target id cannot be empty")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	deleteReq := connect.NewRequest(&porterv1.DeleteDeploymentTargetRequest{
+		ProjectId:          int64(project.ID),
+		ClusterId:          int64(cluster.ID),
+		DeploymentTargetId: deploymentTargetID,
+	})
+
+	_, err := c.Config().ClusterControlPlaneClient.DeleteDeploymentTarget(ctx, deleteReq)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error deleting deployment target")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	c.WriteResult(w, r, nil)
+}

+ 12 - 0
api/server/handlers/porter_app/create_app_template.go

@@ -3,6 +3,7 @@ package porter_app
 import (
 	"context"
 	"encoding/base64"
+	"fmt"
 	"net/http"
 	"time"
 
@@ -207,6 +208,17 @@ func (c *CreateAppTemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
+	webhookURL := fmt.Sprintf("%s/api/webhooks/github/%d/%d/%s", c.Config().ServerConf.ServerURL, project.ID, cluster.ID, appName)
+	err = porter_app.SetRepoWebhook(ctx, porter_app.SetRepoWebhookInput{
+		PorterAppName:       appName,
+		ClusterID:           cluster.ID,
+		GithubAppSecret:     c.Config().ServerConf.GithubAppSecret,
+		GithubAppID:         c.Config().ServerConf.GithubAppID,
+		GithubWebhookSecret: c.Config().ServerConf.GithubIncomingWebhookSecret,
+		WebhookURL:          webhookURL,
+		PorterAppRepository: c.Repo().PorterApp(),
+	})
+
 	res := &CreateAppTemplateResponse{
 		AppTemplateID: updatedAppTemplate.ID.String(),
 	}

+ 154 - 0
api/server/handlers/webhook/app_v2_github.go

@@ -0,0 +1,154 @@
+package webhook
+
+import (
+	"net/http"
+
+	"connectrpc.com/connect"
+	"github.com/google/go-github/v41/github"
+	"github.com/google/uuid"
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+	"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/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+type GithubWebhookHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewGithubWebhookHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GithubWebhookHandler {
+	return &GithubWebhookHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *GithubWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-github-webhook")
+	defer span.End()
+
+	payload, err := github.ValidatePayload(r, []byte(c.Config().ServerConf.GithubIncomingWebhookSecret))
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "could not validate payload")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	event, err := github.ParseWebHook(github.WebHookType(r), payload)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "could not parse webhook")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, nil, "error parsing porter app name")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "application-name", Value: appName})
+
+	clusterID, reqErr := requestutils.GetURLParamUint(r, types.URLParamClusterID)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, nil, "error parsing cluster id")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	projectID, reqErr := requestutils.GetURLParamUint(r, types.URLParamProjectID)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, nil, "error parsing project id")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "project-id", Value: projectID})
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "cluster-id", Value: clusterID})
+
+	porterApps, err := c.Repo().PorterApp().ReadPorterAppsByProjectIDAndName(projectID, appName)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting porter app from repo")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+	if len(porterApps) == 0 {
+		err := telemetry.Error(ctx, span, err, "error getting porter app from repo")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+	if len(porterApps) > 1 {
+		err := telemetry.Error(ctx, span, err, "multiple porter apps returned; unable to determine which one to use")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	app := porterApps[0]
+	if app.ID == 0 {
+		err := telemetry.Error(ctx, span, err, "porter app id is missing")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+	if app.ClusterID != clusterID {
+		err := telemetry.Error(ctx, span, err, "porter app cluster id does not match")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-app-id", Value: app.ID})
+
+	switch event := event.(type) {
+	case *github.PullRequestEvent:
+		if event.GetAction() != "closed" {
+			c.WriteResult(w, r, nil)
+			return
+		}
+
+		branch := event.GetPullRequest().GetHead().GetRef()
+		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "event-branch", Value: branch})
+
+		deploymentTarget, err := c.Repo().DeploymentTarget().DeploymentTargetBySelectorAndSelectorType(
+			projectID,
+			clusterID,
+			branch,
+			string(models.DeploymentTargetSelectorType_Namespace),
+		)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error getting deployment target")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+
+		if deploymentTarget.ID == uuid.Nil || !deploymentTarget.Preview {
+			c.WriteResult(w, r, nil)
+			return
+		}
+		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-target-id", Value: deploymentTarget.ID.String()})
+
+		deleteTargetReq := connect.NewRequest(&porterv1.DeleteDeploymentTargetRequest{
+			ProjectId:          int64(projectID),
+			ClusterId:          int64(clusterID),
+			DeploymentTargetId: deploymentTarget.ID.String(),
+		})
+
+		_, err = c.Config().ClusterControlPlaneClient.DeleteDeploymentTarget(ctx, deleteTargetReq)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error deleting deployment target")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+	}
+
+	c.WriteResult(w, r, nil)
+}

+ 25 - 0
api/server/router/base.go

@@ -540,6 +540,31 @@ func GetBaseRoutes(
 			Handler:  githubIncomingWebhookHandler,
 			Router:   r,
 		})
+
+		// POST /api/webhooks/github/{project_id}/{cluster_id}/{porter_app_name} -> webhook.NewGithubWebhookHandler
+		githubWebhookEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbCreate,
+				Method: types.HTTPVerbPost,
+				Path: &types.Path{
+					Parent:       basePath,
+					RelativePath: fmt.Sprintf("/webhooks/github/{%s}/{%s}/{%s}", types.URLParamProjectID, types.URLParamClusterID, types.URLParamPorterAppName),
+				},
+				Scopes: []types.PermissionScope{},
+			},
+		)
+
+		githubWebhookHandler := webhook.NewGithubWebhookHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &router.Route{
+			Endpoint: githubWebhookEndpoint,
+			Handler:  githubWebhookHandler,
+			Router:   r,
+		})
 	}
 
 	return routes

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

@@ -1,6 +1,8 @@
 package router
 
 import (
+	"fmt"
+
 	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/deployment_target"
 	"github.com/porter-dev/porter/api/server/shared"
@@ -114,5 +116,34 @@ func getDeploymentTargetRoutes(
 		Router:   r,
 	})
 
+	// DELETE /api/projects/{project_id}/clusters/{cluster_id}/deployment-targets/{deployment_target_id} -> deployment_target.DeleteDeploymentTargetHandler
+	deleteDeploymentTargetEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbDelete,
+			Method: types.HTTPVerbDelete,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamDeploymentTargetID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	deleteDeploymentTargetHandler := deployment_target.NewDeleteDeploymentTargetHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: deleteDeploymentTargetEndpoint,
+		Handler:  deleteDeploymentTargetHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 1 - 0
api/types/request.go

@@ -53,6 +53,7 @@ const (
 	URLParamPorterAppName         URLParam = "porter_app_name"
 	URLParamPorterAppEventID      URLParam = "porter_app_event_id"
 	URLParamAppRevisionID         URLParam = "app_revision_id"
+	URLParamDeploymentTargetID    URLParam = "deployment_target_id"
 )
 
 type Path struct {

+ 138 - 0
internal/porter_app/github.go

@@ -0,0 +1,138 @@
+package porter_app
+
+import (
+	"context"
+	"net/http"
+	"strconv"
+	"strings"
+
+	"github.com/bradleyfalzon/ghinstallation/v2"
+	"github.com/google/go-github/v39/github"
+	"github.com/porter-dev/porter/internal/repository"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+type SetRepoWebhookInput struct {
+	PorterAppName       string
+	ClusterID           uint
+	GithubAppSecret     []byte
+	GithubAppID         string
+	GithubWebhookSecret string
+	WebhookURL          string
+
+	PorterAppRepository repository.PorterAppRepository
+}
+
+func SetRepoWebhook(ctx context.Context, inp SetRepoWebhookInput) error {
+	ctx, span := telemetry.NewSpan(ctx, "porter-app-set-repo-webhook")
+	defer span.End()
+
+	if inp.PorterAppName == "" {
+		return telemetry.Error(ctx, span, nil, "porter app name is empty")
+	}
+	if inp.ClusterID == 0 {
+		return telemetry.Error(ctx, span, nil, "cluster id is empty")
+	}
+	if inp.GithubAppSecret == nil {
+		return telemetry.Error(ctx, span, nil, "github app secret is nil")
+	}
+	if inp.GithubAppID == "" {
+		return telemetry.Error(ctx, span, nil, "github app id is empty")
+	}
+	if inp.GithubWebhookSecret == "" {
+		return telemetry.Error(ctx, span, nil, "github webhook secret is empty")
+	}
+	if inp.PorterAppRepository == nil {
+		return telemetry.Error(ctx, span, nil, "porter app repository is nil")
+	}
+
+	porterApp, err := inp.PorterAppRepository.ReadPorterAppByName(inp.ClusterID, inp.PorterAppName)
+	if err != nil {
+		return telemetry.Error(ctx, span, err, "could not read porter app by name")
+	}
+	if porterApp.ID == 0 {
+		return telemetry.Error(ctx, span, nil, "porter app not found")
+	}
+	if porterApp.GitRepoID == 0 {
+		return telemetry.Error(ctx, span, nil, "porter app git repo id is empty")
+	}
+
+	githubClient, err := getGithubClientByRepoID(ctx, porterApp.GitRepoID, inp.GithubAppSecret, inp.GithubAppID)
+	if err != nil {
+		return telemetry.Error(ctx, span, err, "error creating github client")
+	}
+
+	repoDetails := strings.Split(porterApp.RepoName, "/")
+	if len(repoDetails) != 2 {
+		return telemetry.Error(ctx, span, nil, "repo name is not in the format <org>/<repo>")
+	}
+	if _, _, err := githubClient.Repositories.Get(ctx, repoDetails[0], repoDetails[1]); err != nil {
+		return telemetry.Error(ctx, span, err, "error getting github repo")
+	}
+
+	hook := &github.Hook{
+		Config: map[string]interface{}{
+			"url":          inp.WebhookURL,
+			"content_type": "json",
+			"secret":       inp.GithubWebhookSecret,
+		},
+		Events: []string{"pull_request", "push"},
+		Active: github.Bool(true),
+	}
+
+	if porterApp.GithubWebhookID != 0 {
+		_, _, err := githubClient.Repositories.EditHook(
+			context.Background(), repoDetails[0], repoDetails[1], porterApp.GithubWebhookID, hook,
+		)
+		if err != nil {
+			return telemetry.Error(ctx, span, err, "could not edit hook")
+		}
+
+		return nil
+	}
+
+	hook, _, err = githubClient.Repositories.CreateHook(
+		context.Background(), repoDetails[0], repoDetails[1], hook,
+	)
+	if err != nil {
+		return telemetry.Error(ctx, span, err, "could not create hook")
+	}
+
+	porterApp.GithubWebhookID = hook.GetID()
+
+	_, err = inp.PorterAppRepository.UpdatePorterApp(porterApp)
+	if err != nil {
+		return telemetry.Error(ctx, span, err, "could not update porter app")
+	}
+
+	return nil
+}
+
+func getGithubClientByRepoID(ctx context.Context, repoID uint, githubAppSecret []byte, githubAppID string) (*github.Client, error) {
+	ctx, span := telemetry.NewSpan(ctx, "get-github-client-by-repo-id")
+	defer span.End()
+
+	if githubAppSecret == nil {
+		return nil, telemetry.Error(ctx, span, nil, "github app secret is nil")
+	}
+	if githubAppID == "" {
+		return nil, telemetry.Error(ctx, span, nil, "github app id is empty")
+	}
+
+	appID, err := strconv.Atoi(githubAppID)
+	if err != nil {
+		return nil, telemetry.Error(ctx, span, err, "could not convert github app id to int")
+	}
+
+	itr, err := ghinstallation.New(
+		http.DefaultTransport,
+		int64(appID),
+		int64(repoID),
+		githubAppSecret,
+	)
+	if err != nil {
+		return nil, telemetry.Error(ctx, span, err, "could not create github app client")
+	}
+
+	return github.NewClient(&http.Client{Transport: itr}), nil
+}