Procházet zdrojové kódy

Merge pull request #55 from porter-dev/api-charts-upgrade

Api charts upgrade
sunguroku před 5 roky
rodič
revize
f5ea2ae1ae

+ 66 - 0
docs/API.md

@@ -18,6 +18,7 @@
   - [`GET /api/charts/{name}/history`](#get-apichartsnamehistory)
   - [`GET /api/charts/{name}/{revision}`](#get-apichartsnamerevision)
   - [`POST /api/charts/rollback/{name}/{revision}`](#post-apichartsrollbacknamerevision)
+  - [`POST /api/charts/{name}/upgrade`](#post-apichartsnameupgrade)
 - [`/api/k8s`](#apik8s)
   - [`GET /api/k8s/namespaces`](#get-apik8snamespaces)
 
@@ -688,6 +689,71 @@ Chart{
 ```
 
 
+**Successful Response Body**: N/A
+
+**Successful Status Code**: `200`
+
+**Errors:** TBD
+
+### `/api/k8s`
+
+#### `GET /api/k8s/namespaces`
+
+**Description:** 
+
+**URL parameters:** N/A
+
+**Query parameters:** N/A
+
+```js
+// The name of the context in the kubeconfig being used
+"context": String,
+```
+
+**Request Body**: N/A
+
+**Successful Response Body**: the full body is determined by the [namespace specification](https://pkg.go.dev/k8s.io/api/core/v1#NamespaceList), but we're primarily only interested in namespace `name`:
+
+```js
+{
+  "metadata": {},
+  "items": []Namespace{
+    "metadata": {
+      "name": String
+    }
+  }
+}
+```
+
+**Successful Status Code**: `200`
+
+**Errors:** TBD
+
+#### `POST /api/charts/{name}/upgrade`
+
+**Description:** Upgrades a chart with new `values.yaml`. 
+
+**URL parameters:** 
+
+- `name` The name of the release.
+
+**Query parameters:** N/A
+
+**Request Body**:
+
+```js
+{
+  // The namespace of the cluster to be used
+  "namespace": String,
+  // The name of the context in the kubeconfig being used
+  "context": String,
+  // The Helm storage option to use
+  "storage": String("secret"|"configmap"|"memory"),
+  // The string of values to use
+  "values": String
+}
+```
+
 **Successful Response Body**: N/A
 
 **Successful Status Code**: `200`

+ 2 - 1
go.mod

@@ -5,6 +5,7 @@ go 1.14
 require (
 	github.com/Azure/go-autorest/autorest v0.11.1 // indirect
 	github.com/DATA-DOG/go-sqlmock v1.5.0
+	github.com/Masterminds/semver v1.5.0 // indirect
 	github.com/cosmtrek/air v1.21.2 // indirect
 	github.com/creack/pty v1.1.11 // indirect
 	github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 // indirect
@@ -49,7 +50,7 @@ require (
 	k8s.io/cli-runtime v0.18.8
 	k8s.io/client-go v0.18.8
 	k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac // indirect
-	k8s.io/helm v2.16.12+incompatible // indirect
+	k8s.io/helm v2.16.12+incompatible
 	k8s.io/klog v1.0.0 // indirect
 	k8s.io/klog/v2 v2.2.0 // indirect
 	k8s.io/utils v0.0.0-20200912215256-4140de9c8800 // indirect

+ 3 - 0
go.sum

@@ -65,6 +65,8 @@ github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp
 github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
 github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
 github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
+github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
 github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
 github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
 github.com/Masterminds/sprig/v3 v3.1.0 h1:j7GpgZ7PdFqNsmncycTHsLmVPf5/3wJtlgW9TNDYD9Y=
@@ -1277,6 +1279,7 @@ k8s.io/component-base v0.18.8/go.mod h1:00frPRDas29rx58pPCxNkhUfPbwajlyyvu8ruNgS
 k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
+k8s.io/helm v2.16.12+incompatible h1:K2zhF8+B85Ya1n7n3eH34xwwp5qNUM42TBFENDZJT7w=
 k8s.io/helm v2.16.12+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI=
 k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=

+ 14 - 0
internal/forms/chart.go

@@ -94,3 +94,17 @@ type ListChartHistoryForm struct {
 	*ChartForm
 	Name string `json:"name" form:"required"`
 }
+
+// RollbackChartForm represents the accepted values for getting a single Helm chart
+type RollbackChartForm struct {
+	*ChartForm
+	Name     string `json:"name" form:"required"`
+	Revision int    `json:"revision" form:"required"`
+}
+
+// UpgradeChartForm represents the accepted values for updating a Helm chart
+type UpgradeChartForm struct {
+	*ChartForm
+	Name   string `json:"name" form:"required"`
+	Values string `json:"values" form:"required"`
+}

+ 33 - 0
internal/helm/agent.go

@@ -1,8 +1,11 @@
 package helm
 
 import (
+	"fmt"
+
 	"helm.sh/helm/v3/pkg/action"
 	"helm.sh/helm/v3/pkg/release"
+	"k8s.io/helm/pkg/chartutil"
 )
 
 // Agent is a Helm agent for performing helm operations
@@ -44,6 +47,36 @@ func (a *Agent) GetReleaseHistory(
 	return cmd.Run(name)
 }
 
+// UpgradeChart upgrades a specific chart using a string of values.yaml
+func (a *Agent) UpgradeChart(
+	name string,
+	values string,
+) (*release.Release, error) {
+	// grab the latest release
+	rel, err := a.GetRelease(name, 0)
+
+	if err != nil {
+		return nil, err
+	}
+
+	ch := rel.Chart
+
+	cmd := action.NewUpgrade(a.ActionConfig)
+	valuesYaml, err := chartutil.ReadValues([]byte(values))
+
+	if err != nil {
+		return nil, fmt.Errorf("Unable to upgrade the release because values could not be parsed: %v", err)
+	}
+
+	res, err := cmd.Run(name, ch, valuesYaml)
+
+	if err != nil {
+		return nil, fmt.Errorf("Unable to upgrade the release: %v", err)
+	}
+
+	return res, nil
+}
+
 // RollbackRelease rolls a release back to a specified revision/version
 func (a *Agent) RollbackRelease(
 	name string,

+ 37 - 0
internal/helm/agent_test.go

@@ -267,6 +267,43 @@ func TestListReleaseHistory(t *testing.T) {
 	}
 }
 
+var upgradeTests = []listReleaseTest{
+	listReleaseTest{
+		name:      "simple history test",
+		namespace: "default",
+		releases: []releaseStub{
+			releaseStub{"wordpress", "default", 2, "1.0.2", release.StatusDeployed},
+			releaseStub{"wordpress", "default", 1, "1.0.1", release.StatusSuperseded},
+		},
+		expRes: []releaseStub{
+			releaseStub{"wordpress", "default", 1, "1.0.1", release.StatusSuperseded},
+			releaseStub{"wordpress", "default", 2, "1.0.2", release.StatusSuperseded},
+			releaseStub{"wordpress", "default", 3, "1.0.2", release.StatusDeployed},
+		},
+	},
+}
+
+func TestUpgradeChart(t *testing.T) {
+	for _, tc := range upgradeTests {
+		agent := newAgentFixture(t, tc.namespace)
+		makeReleases(t, agent, tc.releases)
+
+		// calling agent.ActionConfig.Releases.Create in makeReleases will automatically set the
+		// namespace, so we have to reset the namespace of the storage driver
+		agent.ActionConfig.Releases.Driver.(*driver.Memory).SetNamespace(tc.namespace)
+
+		agent.UpgradeChart("wordpress", "")
+
+		releases, err := agent.GetReleaseHistory("wordpress")
+
+		if err != nil {
+			t.Errorf("%v", err)
+		}
+
+		compareReleaseToStubs(t, releases, tc.expRes)
+	}
+}
+
 var rollbackReleaseTests = []getReleaseTest{
 	getReleaseTest{
 		name:      "simple rollback test",

+ 54 - 5
server/api/chart_handler.go

@@ -2,7 +2,6 @@ package api
 
 import (
 	"encoding/json"
-	"fmt"
 	"net/http"
 	"net/url"
 	"strconv"
@@ -197,6 +196,60 @@ func (app *App) HandleListChartHistory(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
+// HandleUpgradeChart upgrades a chart with new values.yaml
+func (app *App) HandleUpgradeChart(w http.ResponseWriter, r *http.Request) {
+	session, err := app.store.Get(r, app.cookieName)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrChartDecode, w)
+		return
+	}
+
+	name := chi.URLParam(r, "name")
+
+	// get the filter options
+	form := &forms.UpgradeChartForm{
+		ChartForm: &forms.ChartForm{
+			Form: &helm.Form{},
+		},
+		Name: name,
+	}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+		return
+	}
+
+	if sessID, ok := session.Values["user_id"].(uint); ok {
+		form.PopulateHelmOptionsFromUserID(sessID, app.repo.User)
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrChartValidateFields, w)
+		return
+	}
+
+	// create a new agent
+	var agent *helm.Agent
+
+	if app.testing {
+		agent = app.TestAgents.HelmAgent
+	} else {
+		agent, err = helm.GetAgentOutOfClusterConfig(form.ChartForm.Form, app.logger)
+	}
+
+	_, err = agent.UpgradeChart(form.Name, form.Values)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+}
+
 // HandleRollbackChart rolls a release back to a specified revision
 func (app *App) HandleRollbackChart(w http.ResponseWriter, r *http.Request) {
 	session, err := app.store.Get(r, app.cookieName)
@@ -250,9 +303,5 @@ func (app *App) HandleRollbackChart(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	release, err := agent.GetRelease("wordpress", 3)
-
-	fmt.Println("RELEASE IS", release)
-
 	w.WriteHeader(http.StatusOK)
 }

+ 77 - 0
server/api/chart_handler_test.go

@@ -197,6 +197,83 @@ func TestHandleListChartHistory(t *testing.T) {
 	testChartRequests(t, listChartHistoryTests, true)
 }
 
+var upgradeChartTests = []*chartTest{
+	&chartTest{
+		initializers: []func(tester *tester){
+			initHistoryCharts,
+		},
+		msg:       "Upgrade relase",
+		method:    "POST",
+		namespace: "default",
+		endpoint:  "/api/charts/wordpress/upgrade",
+		body: `
+			{
+				"namespace": "default",
+				"context": "context-test",
+				"storage": "memory",
+				"values": "\nfoo: bar\n"
+			}
+		`,
+		expStatus: http.StatusOK,
+		expBody:   ``,
+		useCookie: true,
+		validators: []func(c *chartTest, tester *tester, t *testing.T){
+			func(c *chartTest, tester *tester, t *testing.T) {
+				req, err := http.NewRequest(
+					"GET",
+					"/api/charts/wordpress/3?"+url.Values{
+						"namespace": []string{"default"},
+						"context":   []string{"context-test"},
+						"storage":   []string{"memory"},
+					}.Encode(),
+					strings.NewReader(""),
+				)
+
+				req.AddCookie(tester.cookie)
+
+				if err != nil {
+					t.Fatal(err)
+				}
+
+				rr2 := httptest.NewRecorder()
+				tester.router.ServeHTTP(rr2, req)
+
+				gotBody := &release.Release{}
+				expBody := &release.Release{}
+
+				expBodyJSON := releaseStubToChartJSON(releaseStub{"wordpress", "default", 3, "1.0.2", release.StatusDeployed})
+
+				json.Unmarshal(rr2.Body.Bytes(), gotBody)
+				json.Unmarshal([]byte(expBodyJSON), expBody)
+
+				// just check name and version match, other items will be different
+				if gotBody.Name != expBody.Name {
+					t.Errorf("%s, validation wrong body: got %v want %v",
+						c.msg, gotBody.Name, expBody.Name)
+				}
+
+				if gotBody.Version != expBody.Version {
+					t.Errorf("%s, validation wrong body: got %v want %v",
+						c.msg, gotBody.Version, expBody.Version)
+				}
+
+				expConfig := map[string]interface{}{
+					"foo": "bar",
+				}
+
+				if !reflect.DeepEqual(gotBody.Config, expConfig) {
+					t.Errorf("%s, validation wrong config: got %v want %v",
+						c.msg, gotBody.Config, expConfig)
+				}
+			},
+		},
+	},
+}
+
+func TestUpgradeChart(t *testing.T) {
+	testChartRequests(t, upgradeChartTests, true)
+}
+
 var rollbackChartTests = []*chartTest{
 	&chartTest{
 		initializers: []func(tester *tester){

+ 1 - 0
server/router/router.go

@@ -30,6 +30,7 @@ func New(a *api.App, store sessions.Store, cookieName string) *chi.Mux {
 		// /api/charts routes
 		r.Method("GET", "/charts", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleListCharts, l)))
 		r.Method("GET", "/charts/{name}/history", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleListChartHistory, l)))
+		r.Method("POST", "/charts/{name}/upgrade", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleUpgradeChart, l)))
 		r.Method("GET", "/charts/{name}/{revision}", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleGetChart, l)))
 		r.Method("POST", "/charts/rollback/{name}/{revision}", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleRollbackChart, l)))