| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- package environment
- import (
- "context"
- "errors"
- "fmt"
- "net/http"
- "strings"
- "sync"
- "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"
- "gorm.io/gorm"
- )
- type CreateEnvironmentHandler struct {
- handlers.PorterHandlerReadWriter
- }
- func NewCreateEnvironmentHandler(
- config *config.Config,
- decoderValidator shared.RequestDecoderValidator,
- writer shared.ResultWriter,
- ) *CreateEnvironmentHandler {
- return &CreateEnvironmentHandler{
- PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
- }
- }
- func (c *CreateEnvironmentHandler) 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 preview "+
- "environment: %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, ","),
- }
- if len(request.NamespaceLabels) > 0 {
- var labels []string
- for k, v := range request.NamespaceLabels {
- labels = append(labels, fmt.Sprintf("%s=%s", k, v))
- }
- env.NamespaceLabels = []byte(strings.Join(labels, ","))
- }
- // write Github actions files to the repo
- client, err := getGithubClientFromEnvironment(c.Config(), env)
- if err != nil {
- c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
- return
- }
- webhookURL := getGithubWebhookURLFromUID(c.Config().ServerConf.ServerURL, string(webhookUID))
- // create incoming webhook
- hook, _, err := client.Repositories.CreateHook(
- context.Background(), owner, name, &github.Hook{
- Config: map[string]interface{}{
- "url": webhookURL,
- "content_type": "json",
- "secret": c.Config().ServerConf.GithubIncomingWebhookSecret,
- },
- Events: []string{"pull_request", "push"},
- Active: github.Bool(true),
- },
- )
- if err != nil && !strings.Contains(err.Error(), "already exists") {
- c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, err),
- http.StatusConflict))
- return
- }
- env.GithubWebhookID = hook.GetID()
- env, err = c.Repo().Environment().CreateEnvironment(env)
- 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 creating environment"))
- return
- }
- c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating environment: %w", 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,
- })
- 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
- }
- }
- envType := env.ToEnvironmentType()
- if len(envType.GitDeployBranches) > 0 && c.Config().ServerConf.EnableAutoPreviewBranchDeploy {
- errs := autoDeployBranch(env, c.Config(), envType.GitDeployBranches, false)
- if len(errs) > 0 {
- errString := errs[0].Error()
- for _, e := range errs {
- errString += ": " + e.Error()
- }
- c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
- fmt.Errorf("error auto deploying preview branches: %s", errString), http.StatusConflict),
- )
- return
- }
- }
- c.WriteResult(w, r, envType)
- }
- func getGithubWebhookURLFromUID(serverURL, webhookUID string) string {
- return fmt.Sprintf("%s/api/github/incoming_webhook/%s", serverURL, string(webhookUID))
- }
- func autoDeployBranch(
- env *models.Environment,
- config *config.Config,
- branches []string,
- onlyNewDeployments bool,
- ) []error {
- var (
- errs []error
- wg sync.WaitGroup
- )
- for _, branch := range branches {
- wg.Add(1)
- go func(errs []error, branch string) {
- defer wg.Done()
- errs = append(errs, createWorkflowDispatchForBranch(env, config, onlyNewDeployments, branch)...)
- }(errs, branch)
- }
- wg.Wait()
- return errs
- }
- func createWorkflowDispatchForBranch(
- env *models.Environment,
- config *config.Config,
- onlyNewDeployments bool,
- branch string,
- ) []error {
- var errs []error
- client, err := getGithubClientFromEnvironment(config, env)
- if err != nil {
- errs = append(errs, err)
- return errs
- }
- var deplID uint
- depl, err := config.Repo.Environment().ReadDeploymentForBranch(env.ID, env.GitRepoOwner, env.GitRepoName, branch)
- if err == nil {
- if onlyNewDeployments {
- return errs
- }
- deplID = depl.ID
- } else {
- if errors.Is(err, gorm.ErrRecordNotFound) {
- depl, err := config.Repo.Environment().CreateDeployment(&models.Deployment{
- EnvironmentID: env.ID,
- Status: types.DeploymentStatusCreating,
- PRName: fmt.Sprintf("Deployment for branch %s", branch),
- RepoName: env.GitRepoName,
- RepoOwner: env.GitRepoOwner,
- PRBranchFrom: branch,
- PRBranchInto: branch,
- })
- if err != nil {
- errs = append(errs, fmt.Errorf("error creating deployment for branch %s: %w", branch, err))
- return errs
- }
- deplID = depl.ID
- } else {
- errs = append(errs, fmt.Errorf("error reading deployment for branch %s: %w", branch, err))
- return errs
- }
- }
- if deplID == 0 {
- errs = append(errs, fmt.Errorf("deployment id is 0 for branch %s", branch))
- return errs
- }
- _, err = client.Actions.CreateWorkflowDispatchEventByFileName(
- context.Background(), env.GitRepoOwner, env.GitRepoName, fmt.Sprintf("porter_%s_env.yml", env.Name),
- github.CreateWorkflowDispatchEventRequest{
- Ref: branch,
- Inputs: map[string]interface{}{
- "pr_number": fmt.Sprintf("%d", deplID),
- "pr_title": fmt.Sprintf("Deployment for branch %s", branch),
- "pr_branch_from": branch,
- "pr_branch_into": branch,
- },
- },
- )
- if err != nil {
- errs = append(errs, err)
- }
- return errs
- }
|