Просмотр исходного кода

Merge pull request #48 from porter-dev/api-charts-history

Api charts history
jusrhee 5 лет назад
Родитель
Сommit
0167333317

+ 82 - 0
docs/API.md

@@ -15,6 +15,7 @@
   - [`DELETE /api/users/{id}`](#delete-apiusersid)
 - [`/api/charts`](#apicharts)
   - [`GET /api/charts`](#get-apicharts)
+  - [`GET /api/charts/{name}/history`](#get-apichartsnamehistory)
   - [`GET /api/charts/{name}/{revision}`](#get-apichartsnamerevision)
 - [`/api/k8s`](#apik8s)
   - [`GET /api/k8s/namespaces`](#get-apik8snamespaces)
@@ -480,6 +481,87 @@ User{
 
 **Errors:** TBD
 
+#### `GET /api/charts/{name}/history`
+
+**Description:** Gets a history of revisions for a given deployed chart based on the release `name`.
+
+**URL parameters:** 
+
+- `name` The name of the release.
+
+**Query parameters:** 
+
+```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")
+}
+```
+
+**Request Body**: N/A
+
+**Successful Response Body**: the full body is determined by the [release specification](https://pkg.go.dev/helm.sh/helm/v3@v3.3.4/pkg/release#Release): listed here is a subset of fields deemed to be most relevant. Note that all of the top-level fields are optional.
+
+```js
+[]Chart{
+  // Name is the name of the release
+  "name": String,
+  "info": Info{
+    // LastDeployed is when the release was last deployed.
+    "last_deployed": String,
+    // Deleted tracks when this object was deleted.
+    "deleted": String,
+    // Description is human-friendly "log entry" about this release.
+    "description": String,
+    // Status is the current state of the release
+    "status": String("unknown"|"deployed"|"uninstalled"|"superseded"|"failed"|"uninstalling"|"pending-install"|"pending-upgrade"|"pending-rollback")
+  },
+  "chart": Chart{
+    "metadata": Metadata{
+      // The name of the chart
+      "name": String,
+      // The URL to a relevant project page, git repo, or contact person
+      "home": String,
+      // Sources is a list of URLs to the source code of this chart
+      "sources": []String,
+      // A SemVer 2 conformant version string of the chart
+      "version": String,
+      // A one-sentence description of the chart
+      "description": String,
+      // The URL to an icon file.
+      "icon": String,
+      // The API Version of this chart.
+      "apiVersion": String,
+    },
+    "templates": []File{
+      // Name is the path-like name of the template.
+      "name": String,
+      // Data is the template as byte data.
+      "data": String
+    },
+    // Values are default config for this chart.
+    "values": Map[String]{}
+  },
+  // The set of extra Values added to the chart, which override the 
+  // default values inside of the chart
+  "config": Map[String]{},
+  // Manifest is the string representation of the rendered template
+  "manifest": String,
+  // Version is an int which represents the revision of the release.
+  "version": Number,
+  // Namespace is the kubernetes namespace of the release.
+  "namespace": String
+}
+```
+
+**Successful Status Code**: `200`
+
+**Errors:** TBD
+
 #### `GET /api/charts/{name}/{revision}`
 
 **Description:** Gets a single chart for a current context and a kubeconfig retrieved from the user's ID based on a **name** and **revision**. To retrieve the latest deployed chart, set **revision** to 0. 

+ 6 - 0
internal/forms/chart.go

@@ -84,3 +84,9 @@ type GetChartForm struct {
 	Name     string `json:"name" form:"required"`
 	Revision int    `json:"revision"`
 }
+
+// ListChartHistoryForm represents the accepted values for getting a single Helm chart
+type ListChartHistoryForm struct {
+	*ChartForm
+	Name string `json:"name" form:"required"`
+}

+ 9 - 0
internal/helm/agent.go

@@ -34,3 +34,12 @@ func (a *Agent) GetRelease(
 
 	return cmd.Run(name)
 }
+
+// GetReleaseHistory returns a list of charts for a specific release
+func (a *Agent) GetReleaseHistory(
+	name string,
+) ([]*release.Release, error) {
+	cmd := action.NewHistory(a.ActionConfig)
+
+	return cmd.Run(name)
+}

+ 34 - 0
internal/helm/agent_test.go

@@ -212,3 +212,37 @@ func TestGetReleases(t *testing.T) {
 		compareReleaseToStubs(t, []*release.Release{rel}, []releaseStub{tc.expRes})
 	}
 }
+
+var listReleaseHistoryTests = []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.StatusDeployed},
+		},
+	},
+}
+
+func TestListReleaseHistory(t *testing.T) {
+	for _, tc := range listReleaseHistoryTests {
+		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)
+
+		releases, err := agent.GetReleaseHistory("wordpress")
+
+		if err != nil {
+			t.Errorf("%v", err)
+		}
+
+		compareReleaseToStubs(t, releases, tc.expRes)
+	}
+}

+ 4 - 3
internal/kubernetes/kubeconfig.go

@@ -36,15 +36,16 @@ func GetRestrictedClientConfigFromBytes(
 	// put allowed clusters in a map
 	aContextMap := createAllowedContextMap(allowedContexts)
 
-	// discover all allowed clusters
-	for name, context := range rawConf.Contexts {
+	context, ok := rawConf.Contexts[contextName]
+
+	if ok {
 		userName := context.AuthInfo
 		clusterName := context.Cluster
 		authInfo, userFound := rawConf.AuthInfos[userName]
 		cluster, clusterFound := rawConf.Clusters[clusterName]
 
 		// make sure the cluster is "allowed"
-		_, isAllowed := aContextMap[name]
+		_, isAllowed := aContextMap[contextName]
 
 		if userFound && clusterFound && isAllowed {
 			copyConf.Clusters[clusterName] = cluster

+ 60 - 0
server/api/chart_handler.go

@@ -135,3 +135,63 @@ func (app *App) HandleGetChart(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 }
+
+// HandleListChartHistory retrieves a history of charts based on a chart name
+func (app *App) HandleListChartHistory(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.ListChartHistoryForm{
+		ChartForm: &forms.ChartForm{
+			Form: &helm.Form{},
+		},
+		Name: name,
+	}
+
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrChartDecode, w)
+		return
+	}
+
+	form.PopulateHelmOptionsFromQueryParams(vals)
+
+	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)
+	}
+
+	release, err := agent.GetReleaseHistory(form.Name)
+
+	if err != nil {
+		app.handleErrorFormValidation(err, ErrChartValidateFields, w)
+		return
+	}
+
+	if err := json.NewEncoder(w).Encode(release); err != nil {
+		app.handleErrorFormDecoding(err, ErrChartDecode, w)
+		return
+	}
+}

+ 43 - 0
server/api/chart_handler_test.go

@@ -137,6 +137,32 @@ func TestHandleGetChart(t *testing.T) {
 	testChartRequests(t, getChartTests, true)
 }
 
+var listChartHistoryTests = []*chartTest{
+	&chartTest{
+		initializers: []func(tester *tester){
+			initHistoryCharts,
+		},
+		msg:    "List chart history",
+		method: "GET",
+		endpoint: "/api/charts/wordpress/history?" + url.Values{
+			"namespace": []string{""},
+			"context":   []string{"context-test"},
+			"storage":   []string{"memory"},
+		}.Encode(),
+		body:      "",
+		expStatus: http.StatusOK,
+		expBody:   releaseStubsToChartJSON(historyReleaseStubs),
+		useCookie: true,
+		validators: []func(c *chartTest, tester *tester, t *testing.T){
+			chartReleaseBodyValidator,
+		},
+	},
+}
+
+func TestHandleListChartHistory(t *testing.T) {
+	testChartRequests(t, listChartHistoryTests, true)
+}
+
 // ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
 
 func initDefaultCharts(tester *tester) {
@@ -151,12 +177,29 @@ func initDefaultCharts(tester *tester) {
 	agent.ActionConfig.Releases.Driver.(*driver.Memory).SetNamespace("")
 }
 
+func initHistoryCharts(tester *tester) {
+	initUserDefault(tester)
+
+	agent := tester.app.TestAgents.HelmAgent
+
+	makeReleases(agent, historyReleaseStubs)
+
+	// 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("")
+}
+
 var sampleReleaseStubs = []releaseStub{
 	releaseStub{"airwatch", "default", 1, "1.0.0", release.StatusDeployed},
 	releaseStub{"not-in-default-namespace", "other", 1, "1.0.1", release.StatusDeployed},
 	releaseStub{"wordpress", "default", 1, "1.0.2", release.StatusDeployed},
 }
 
+var historyReleaseStubs = []releaseStub{
+	releaseStub{"wordpress", "default", 1, "1.0.1", release.StatusSuperseded},
+	releaseStub{"wordpress", "default", 2, "1.0.2", release.StatusDeployed},
+}
+
 func releaseStubsToChartJSON(rels []releaseStub) string {
 	releases := make([]*release.Release, 0)
 

+ 1 - 0
server/router/router.go

@@ -29,6 +29,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("GET", "/charts/{name}/{revision}", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleGetChart, l)))
 
 		// /api/k8s routes