Procházet zdrojové kódy

add rollback handler

Alexander Belanger před 4 roky
rodič
revize
260859e643

+ 60 - 0
api/server/handlers/release/create.go

@@ -18,6 +18,7 @@ import (
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/registry"
 	"github.com/porter-dev/porter/internal/repository"
+	"gopkg.in/yaml.v2"
 	"helm.sh/helm/v3/pkg/release"
 )
 
@@ -263,3 +264,62 @@ func createGitAction(
 
 	return ga.ToGitActionConfigType(), workflowYAML, nil
 }
+
+type containerEnvConfig struct {
+	Container struct {
+		Env struct {
+			Normal map[string]string `yaml:"normal"`
+		} `yaml:"env"`
+	} `yaml:"container"`
+}
+
+func updateGitActionEnvSecret(
+	config *config.Config,
+	userID, projectID, clusterID uint,
+	ga *models.GitActionConfig,
+	name, namespace string,
+	release *models.Release,
+	helmRelease *release.Release,
+) error {
+	cEnv := &containerEnvConfig{}
+	rawValues, err := yaml.Marshal(helmRelease.Config)
+
+	if err != nil {
+		return err
+	}
+
+	err = yaml.Unmarshal(rawValues, cEnv)
+
+	if err != nil {
+		return err
+	}
+
+	repoSplit := strings.Split(ga.GitRepo, "/")
+
+	if len(repoSplit) != 2 {
+		return fmt.Errorf("invalid formatting of repo name")
+	}
+
+	// create the commit in the git repo
+	gaRunner := &actions.GithubActions{
+		ServerURL:              config.ServerConf.ServerURL,
+		GithubOAuthIntegration: nil,
+		BuildEnv:               cEnv.Container.Env.Normal,
+		GithubAppID:            config.GithubAppConf.AppID,
+		GithubAppSecretPath:    config.GithubAppConf.SecretPath,
+		GithubInstallationID:   ga.GitRepoID,
+		GitRepoName:            repoSplit[1],
+		GitRepoOwner:           repoSplit[0],
+		Repo:                   config.Repo,
+		ProjectID:              projectID,
+		ClusterID:              clusterID,
+		ReleaseName:            name,
+		GitBranch:              ga.GitBranch,
+		DockerFilePath:         ga.DockerfilePath,
+		FolderPath:             ga.FolderPath,
+		ImageRepoURL:           ga.ImageRepoURI,
+		Version:                "v0.1.0",
+	}
+
+	return gaRunner.CreateEnvSecret()
+}

+ 291 - 0
api/server/handlers/release/update_rollback.go

@@ -0,0 +1,291 @@
+package release
+
+import (
+	"fmt"
+	"net/http"
+
+	"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/types"
+	"github.com/porter-dev/porter/internal/models"
+	"helm.sh/helm/v3/pkg/release"
+)
+
+type RollbackReleaseHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewRollbackReleaseHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *RollbackReleaseHandler {
+	return &RollbackReleaseHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *RollbackReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+	helmRelease, _ := r.Context().Value(types.ReleaseScope).(*release.Release)
+
+	helmAgent, err := c.GetHelmAgent(r, cluster)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	request := &types.RollbackReleaseRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	err = helmAgent.RollbackRelease(helmRelease.Name, request.Revision)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("error rolling back release: %s", err.Error()),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	// update the github actions env if the release exists and is built from source
+	if cName := helmRelease.Chart.Metadata.Name; cName == "job" || cName == "web" || cName == "worker" {
+		rel, err := c.Repo().Release().ReadRelease(cluster.ID, helmRelease.Name, helmRelease.Namespace)
+
+		if err == nil && rel != nil {
+			err = updateReleaseRepo(c.Config(), rel, helmRelease)
+
+			if err != nil {
+				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				return
+			}
+
+			gitAction := rel.GitActionConfig
+
+			if gitAction != nil && gitAction.ID != 0 {
+				err = updateGitActionEnvSecret(
+					c.Config(),
+					user.ID,
+					cluster.ProjectID,
+					cluster.ID,
+					rel.GitActionConfig,
+					helmRelease.Name,
+					helmRelease.Namespace,
+					rel,
+					helmRelease,
+				)
+
+				if err != nil {
+					c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+					return
+				}
+			}
+		}
+	}
+}
+
+func updateReleaseRepo(config *config.Config, release *models.Release, helmRelease *release.Release) error {
+	repository := helmRelease.Config["image"].(map[string]interface{})["repository"]
+	repoStr, ok := repository.(string)
+
+	if !ok {
+		return fmt.Errorf("Could not find field repository in config")
+	}
+
+	if repoStr != release.ImageRepoURI {
+		release.ImageRepoURI = repoStr
+		_, err := config.Repo.Release().UpdateRelease(release)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// // HandleRollbackRelease rolls a release back to a specified revision
+// func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
+// 	name := chi.URLParam(r, "name")
+
+// 	vals, err := url.ParseQuery(r.URL.RawQuery)
+
+// 	if err != nil {
+// 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+// 		return
+// 	}
+
+// 	form := &forms.RollbackReleaseForm{
+// 		ReleaseForm: &forms.ReleaseForm{
+// 			Form: &helm.Form{
+// 				Repo:              app.Repo,
+// 				DigitalOceanOAuth: app.DOConf,
+// 			},
+// 		},
+// 		Name: name,
+// 	}
+
+// 	form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
+// 		vals,
+// 		app.Repo.Cluster(),
+// 	)
+
+// 	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+// 		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+// 		return
+// 	}
+
+// 	agent, err := app.getAgentFromReleaseForm(
+// 		w,
+// 		r,
+// 		form.ReleaseForm,
+// 	)
+
+// 	// errors are handled in app.getAgentFromBodyParams
+// 	if err != nil {
+// 		return
+// 	}
+
+// 	err = agent.RollbackRelease(form.Name, form.Revision)
+
+// 	if err != nil {
+// 		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
+// 			Code:   ErrReleaseDeploy,
+// 			Errors: []string{"error rolling back release " + err.Error()},
+// 		}, w)
+
+// 		return
+// 	}
+
+// 	// get the full release data for GHA updating
+// 	rel, err := agent.GetRelease(form.Name, form.Revision)
+
+// 	if err != nil {
+// 		app.sendExternalError(err, http.StatusNotFound, HTTPError{
+// 			Code:   ErrReleaseReadData,
+// 			Errors: []string{"release not found"},
+// 		}, w)
+
+// 		return
+// 	}
+
+// 	// update the github actions env if the release exists and is built from source
+// 	if cName := rel.Chart.Metadata.Name; cName == "job" || cName == "web" || cName == "worker" {
+// 		clusterID, err := strconv.ParseUint(vals["cluster_id"][0], 10, 64)
+
+// 		if err != nil {
+// 			app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
+// 				Code:   ErrReleaseReadData,
+// 				Errors: []string{"release not found"},
+// 			}, w)
+
+// 			return
+// 		}
+
+// 		release, err := app.Repo.Release().ReadRelease(uint(clusterID), name, rel.Namespace)
+
+// 		if release != nil {
+// 			// update image repo uri if changed
+// 			repository := rel.Config["image"].(map[string]interface{})["repository"]
+// 			repoStr, ok := repository.(string)
+
+// 			if !ok {
+// 				app.handleErrorInternal(fmt.Errorf("Could not find field repository in config"), w)
+// 				return
+// 			}
+
+// 			if repoStr != release.ImageRepoURI {
+// 				release, err = app.Repo.Release().UpdateRelease(release)
+
+// 				if err != nil {
+// 					app.handleErrorInternal(err, w)
+// 					return
+// 				}
+// 			}
+
+// 			gitAction := release.GitActionConfig
+
+// 			if gitAction.ID != 0 {
+// 				// parse env into build env
+// 				cEnv := &ContainerEnvConfig{}
+// 				rawValues, err := yaml.Marshal(rel.Config)
+
+// 				if err != nil {
+// 					app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
+// 						Code:   ErrReleaseReadData,
+// 						Errors: []string{"could not get values of previous revision"},
+// 					}, w)
+// 				}
+
+// 				yaml.Unmarshal(rawValues, cEnv)
+
+// 				gr, err := app.Repo.GitRepo().ReadGitRepo(gitAction.GitRepoID)
+
+// 				if err != nil {
+// 					if err != gorm.ErrRecordNotFound {
+// 						app.handleErrorInternal(err, w)
+// 						return
+// 					}
+// 					gr = nil
+// 				}
+
+// 				repoSplit := strings.Split(gitAction.GitRepo, "/")
+
+// 				projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+// 				if err != nil || projID == 0 {
+// 					app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+// 					return
+// 				}
+
+// 				gaRunner := &actions.GithubActions{
+// 					ServerURL:              app.ServerConf.ServerURL,
+// 					GithubOAuthIntegration: gr,
+// 					GithubInstallationID:   gitAction.GithubInstallationID,
+// 					GithubAppID:            app.GithubAppConf.AppID,
+// 					GithubAppSecretPath:    app.GithubAppConf.SecretPath,
+// 					GitRepoName:            repoSplit[1],
+// 					GitRepoOwner:           repoSplit[0],
+// 					Repo:                   app.Repo,
+// 					GithubConf:             app.GithubProjectConf,
+// 					ProjectID:              uint(projID),
+// 					ReleaseName:            name,
+// 					GitBranch:              gitAction.GitBranch,
+// 					DockerFilePath:         gitAction.DockerfilePath,
+// 					FolderPath:             gitAction.FolderPath,
+// 					ImageRepoURL:           gitAction.ImageRepoURI,
+// 					BuildEnv:               cEnv.Container.Env.Normal,
+// 					ClusterID:              release.ClusterID,
+// 					Version:                gitAction.Version,
+// 				}
+
+// 				actionVersion, err := semver.NewVersion(gaRunner.Version)
+// 				if err != nil {
+// 					app.handleErrorInternal(err, w)
+// 				}
+
+// 				if createEnvSecretConstraint.Check(actionVersion) {
+// 					if err := gaRunner.CreateEnvSecret(); err != nil {
+// 						app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
+// 							Code:   ErrReleaseReadData,
+// 							Errors: []string{"could not update github secret"},
+// 						}, w)
+// 					}
+// 				}
+// 			}
+// 		}
+// 	}
+
+// 	w.WriteHeader(http.StatusOK)
+// }

+ 32 - 0
api/server/router/release.go

@@ -380,5 +380,37 @@ func getReleaseRoutes(
 		Router:   r,
 	})
 
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/releases/{name}/{version}/rollback ->
+	// release.NewCreateAddonHandler
+	rollbackEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/rollback",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+				types.NamespaceScope,
+				types.ReleaseScope,
+			},
+		},
+	)
+
+	rollbackHandler := release.NewRollbackReleaseHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: rollbackEndpoint,
+		Handler:  rollbackHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 4 - 0
api/types/release.go

@@ -44,3 +44,7 @@ type CreateReleaseRequest struct {
 type CreateAddonRequest struct {
 	*CreateReleaseBaseRequest
 }
+
+type RollbackReleaseRequest struct {
+	Revision int `json:"revision" form:"required"`
+}

+ 1 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx

@@ -165,13 +165,12 @@ class RevisionSection extends Component<PropsType, StateType> {
       .rollbackChart(
         "<token>",
         {
-          namespace: this.props.chart.namespace,
-          storage: StorageType.Secret,
           revision: revisionNumber,
         },
         {
           id: currentProject.id,
           name: this.props.chart.name,
+          namespace: this.props.chart.namespace,
           cluster_id: currentCluster.id,
         }
       )

+ 3 - 4
dashboard/src/shared/api.tsx

@@ -812,18 +812,17 @@ const registerUser = baseApi<{
 
 const rollbackChart = baseApi<
   {
-    namespace: string;
-    storage: StorageType;
     revision: number;
   },
   {
     id: number;
     name: string;
+    namespace: string;
     cluster_id: number;
   }
 >("POST", (pathParams) => {
-  let { id, name, cluster_id } = pathParams;
-  return `/api/projects/${id}/releases/${name}/rollback?cluster_id=${cluster_id}`;
+  let { id, name, cluster_id, namespace } = pathParams;
+  return `/api/projects/${id}/clusters/${cluster_id}/namespaces/${namespace}/releases/${name}/0/rollback`;
 });
 
 const uninstallTemplate = baseApi<

+ 1 - 1
docs/developing/backend-refactor-status.md

@@ -128,7 +128,7 @@
 | <li>- [X] `GET /api/projects/{project_id}/releases/{name}/history`                                                          | AB          |                 |             |                  |
 | <li>- [X] `POST /api/projects/{project_id}/releases/{name}/notifications`                                                   | AB          |                 |             |                  |
 | <li>- [X] `GET /api/projects/{project_id}/releases/{name}/notifications`                                                    | AB          |                 |             |                  |
-| <li>- [ ] `POST /api/projects/{project_id}/releases/{name}/rollback`                                                        |             |                 |             |                  |
+| <li>- [X] `POST /api/projects/{project_id}/releases/{name}/rollback`                                                        | AB          |                 |             |                  |
 | <li>- [ ] `POST /api/projects/{project_id}/releases/{name}/upgrade`                                                         |             |                 |             |                  |
 | <li>- [X] `GET /api/projects/{project_id}/releases/{name}/webhook_token`                                                    | AB          |                 |             |                  |
 | <li>- [X] `POST /api/projects/{project_id}/releases/{name}/webhook_token`                                                   | AB          |                 |             |                  |

+ 1 - 0
go.mod

@@ -6,6 +6,7 @@ require (
 	cloud.google.com/go v0.65.0
 	github.com/AlecAivazis/survey/v2 v2.2.9
 	github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect
+	github.com/Masterminds/semver v1.5.0
 	github.com/Masterminds/semver/v3 v3.1.1
 	github.com/aws/aws-sdk-go v1.35.4
 	github.com/bradleyfalzon/ghinstallation v1.1.1