Kaynağa Gözat

add upgrade endpoint

Alexander Belanger 4 yıl önce
ebeveyn
işleme
f134435280

+ 196 - 0
api/server/handlers/release/ugprade.go

@@ -0,0 +1,196 @@
+package release
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+
+	"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/helm"
+	"github.com/porter-dev/porter/internal/helm/loader"
+	"github.com/porter-dev/porter/internal/integrations/slack"
+	"github.com/porter-dev/porter/internal/models"
+	"helm.sh/helm/v3/pkg/release"
+)
+
+type UpgradeReleaseHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewUpgradeReleaseHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *UpgradeReleaseHandler {
+	return &UpgradeReleaseHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *UpgradeReleaseHandler) 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.UpgradeReleaseRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	registries, err := c.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	conf := &helm.UpgradeReleaseConfig{
+		Name:       helmRelease.Name,
+		Cluster:    cluster,
+		Repo:       c.Repo(),
+		Registries: registries,
+	}
+
+	// if the chart version is set, load a chart from the repo
+	if request.ChartVersion != "" {
+		cache := c.Config().URLCache
+		chartRepoURL, foundFirst := cache.GetURL(helmRelease.Chart.Metadata.Name)
+
+		if !foundFirst {
+			cache.Update()
+
+			var found bool
+
+			chartRepoURL, found = cache.GetURL(helmRelease.Chart.Metadata.Name)
+
+			if !found {
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+					fmt.Errorf("chart not found"),
+					http.StatusBadRequest,
+				))
+
+				return
+			}
+		}
+
+		chart, err := loader.LoadChartPublic(
+			chartRepoURL,
+			helmRelease.Chart.Metadata.Name,
+			request.ChartVersion,
+		)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("chart not found"),
+				http.StatusBadRequest,
+			))
+
+			return
+		}
+
+		conf.Chart = chart
+	}
+
+	helmRelease, upgradeErr := helmAgent.UpgradeRelease(conf, request.Values, c.Config().DOConf)
+
+	slackInts, _ := c.Repo().SlackIntegration().ListSlackIntegrationsByProjectID(cluster.ProjectID)
+
+	rel, releaseErr := c.Repo().Release().ReadRelease(cluster.ID, helmRelease.Name, helmRelease.Namespace)
+
+	var notifConf *models.NotificationConfigExternal
+	notifConf = nil
+	if rel != nil && rel.NotificationConfig != 0 {
+		conf, err := c.Repo().NotificationConfig().ReadNotificationConfig(rel.NotificationConfig)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		notifConf = conf.Externalize()
+	}
+
+	notifier := slack.NewSlackNotifier(notifConf, slackInts...)
+
+	notifyOpts := &slack.NotifyOpts{
+		ProjectID:   cluster.ProjectID,
+		ClusterID:   cluster.ID,
+		ClusterName: cluster.Name,
+		Name:        helmRelease.Name,
+		Namespace:   helmRelease.Namespace,
+		URL: fmt.Sprintf(
+			"%s/applications/%s/%s/%s",
+			c.Config().ServerConf.ServerURL,
+			url.PathEscape(cluster.Name),
+			cluster.Name,
+			helmRelease.Name,
+		) + fmt.Sprintf("?project_id=%d", cluster.ProjectID),
+	}
+
+	if upgradeErr != nil {
+		notifyOpts.Status = slack.StatusFailed
+		notifyOpts.Info = upgradeErr.Error()
+
+		notifier.Notify(notifyOpts)
+
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			upgradeErr,
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	notifyOpts.Status = string(helmRelease.Info.Status)
+	notifyOpts.Version = helmRelease.Version
+
+	notifier.Notify(notifyOpts)
+
+	// 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" {
+		if releaseErr == 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
+				}
+			}
+		}
+	}
+}

+ 34 - 2
api/server/router/release.go

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

+ 4 - 0
api/server/shared/config/config.go

@@ -7,6 +7,7 @@ import (
 	"github.com/gorilla/websocket"
 	"github.com/porter-dev/porter/api/server/shared/apierrors/alerter"
 	"github.com/porter-dev/porter/internal/auth/token"
+	"github.com/porter-dev/porter/internal/helm/urlcache"
 	"github.com/porter-dev/porter/internal/logger"
 	"github.com/porter-dev/porter/internal/notifier"
 	"github.com/porter-dev/porter/internal/oauth"
@@ -53,6 +54,9 @@ type Config struct {
 
 	// WSUpgrader upgrades HTTP connections to websocket connections
 	WSUpgrader *websocket.Upgrader
+
+	// URLCache contains a cache of chart names to chart repos
+	URLCache *urlcache.ChartURLCache
 }
 
 type ConfigLoader interface {

+ 3 - 0
api/server/shared/config/loader/loader.go

@@ -10,6 +10,7 @@ import (
 	"github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/auth/sessionstore"
 	"github.com/porter-dev/porter/internal/auth/token"
+	"github.com/porter-dev/porter/internal/helm/urlcache"
 	"github.com/porter-dev/porter/internal/notifier"
 	"github.com/porter-dev/porter/internal/notifier/sendgrid"
 	"github.com/porter-dev/porter/internal/oauth"
@@ -138,5 +139,7 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 		},
 	}
 
+	res.URLCache = urlcache.Init(sc.DefaultApplicationHelmRepoURL, sc.DefaultAddonHelmRepoURL)
+
 	return res, nil
 }

+ 5 - 0
api/types/release.go

@@ -48,3 +48,8 @@ type CreateAddonRequest struct {
 type RollbackReleaseRequest struct {
 	Revision int `json:"revision" form:"required"`
 }
+
+type UpgradeReleaseRequest struct {
+	Values       string `json:"values" form:"required"`
+	ChartVersion string `json:"version"`
+}

+ 2 - 3
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -250,12 +250,12 @@ const ExpandedChart: React.FC<Props> = (props) => {
       await api.upgradeChartValues(
         "<token>",
         {
-          namespace: currentChart.namespace,
           storage: StorageType.Secret,
           values: valuesYaml,
         },
         {
           id: currentProject.id,
+          namespace: currentChart.namespace,
           name: currentChart.name,
           cluster_id: currentCluster.id,
         }
@@ -304,13 +304,12 @@ const ExpandedChart: React.FC<Props> = (props) => {
         await api.upgradeChartValues(
           "<token>",
           {
-            namespace: currentChart.namespace,
-            storage: StorageType.Secret,
             values: valuesYaml,
             version: version,
           },
           {
             id: currentProject.id,
+            namespace: currentChart.namespace,
             name: currentChart.name,
             cluster_id: currentCluster.id,
           }

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

@@ -344,13 +344,13 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
       .upgradeChartValues(
         "<token>",
         {
-          namespace: this.state.currentChart.namespace,
           storage: StorageType.Secret,
           values: conf,
         },
         {
           id: currentProject.id,
           name: this.state.currentChart.name,
+          namespace: this.state.currentChart.namespace,
           cluster_id: currentCluster.id,
         }
       )

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

@@ -118,13 +118,12 @@ const SettingsSection: React.FC<PropsType> = ({
       await api.upgradeChartValues(
         "<token>",
         {
-          namespace: currentChart?.namespace,
-          storage: StorageType.Secret,
           values: conf,
         },
         {
           id: currentProject.id,
           name: currentChart?.name,
+          namespace: currentChart?.namespace,
           cluster_id: currentCluster.id,
         }
       );

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

@@ -63,14 +63,13 @@ export default class ValuesYaml extends Component<PropsType, StateType> {
       .upgradeChartValues(
         "<token>",
         {
-          namespace: this.props.currentChart.namespace,
-          storage: StorageType.Secret,
           values: valuesString,
         },
         {
           id: currentProject.id,
           name: this.props.currentChart.name,
           cluster_id: currentCluster.id,
+          namespace: this.props.currentChart.namespace,
         }
       )
       .then((res) => {

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

@@ -851,19 +851,18 @@ const updateUser = baseApi<
 
 const upgradeChartValues = baseApi<
   {
-    namespace: string;
-    storage: StorageType;
     values: string;
     version?: string;
   },
   {
     id: number;
     name: string;
+    namespace: string;
     cluster_id: number;
   }
 >("POST", (pathParams) => {
-  let { id, name, cluster_id } = pathParams;
-  return `/api/projects/${id}/releases/${name}/upgrade?cluster_id=${cluster_id}`;
+  let { id, name, cluster_id, namespace } = pathParams;
+  return `/api/projects/${id}/clusters/${cluster_id}/namespaces/${namespace}/releases/${name}/0/upgrade`;
 });
 
 const listConfigMaps = baseApi<

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

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

+ 46 - 0
internal/helm/urlcache/urlcache.go

@@ -0,0 +1,46 @@
+package urlcache
+
+import "github.com/porter-dev/porter/internal/helm/loader"
+
+// ChartLookupURLs contains an in-memory store of Porter chart names matched with
+// a repo URL, so that finding a chart does not involve multiple lookups to our
+// chart repo's index.yaml file
+type ChartURLCache struct {
+	cache map[string]string
+	urls  []string
+}
+
+func Init(urls ...string) *ChartURLCache {
+	res := &ChartURLCache{
+		cache: make(map[string]string),
+		urls:  urls,
+	}
+
+	res.Update()
+
+	return res
+}
+
+func (c *ChartURLCache) Update() {
+	newCharts := make(map[string]string)
+
+	for _, chartRepo := range c.urls {
+		indexFile, err := loader.LoadRepoIndexPublic(chartRepo)
+
+		if err != nil {
+			continue
+		}
+
+		for chartName := range indexFile.Entries {
+			newCharts[chartName] = chartRepo
+		}
+	}
+
+	c.cache = newCharts
+}
+
+func (c *ChartURLCache) GetURL(chartName string) (string, bool) {
+	res, ok := c.cache[chartName]
+
+	return res, ok
+}