Răsfoiți Sursa

merged with rollback

Alexander Belanger 5 ani în urmă
părinte
comite
37e5ef38be

+ 1 - 1
.gitignore

@@ -2,4 +2,4 @@
 .env
 app
 *.db
-
+test.yaml

+ 151 - 0
dashboard/src/components/Selector.tsx

@@ -0,0 +1,151 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+type PropsType = {
+  activeValue: string,
+  options: { value: string, label: string }[],
+  setActiveValue: (x: string) => void,
+  dropdownLabel: string
+};
+
+type StateType = {
+};
+
+export default class Selector extends Component<PropsType, StateType> {
+  state = {
+    expanded: false
+  }
+
+  renderOptionList = () => {
+    let { options, activeValue, setActiveValue } = this.props;
+    return options.map((option: { value: string, label: string }, i: number) => {
+      return (
+        <Option
+          key={i}
+          selected={option.value === activeValue}
+          onClick={() => setActiveValue(option.value)}
+        >
+          {option.label}
+        </Option>
+      );
+    });
+  }
+
+  renderDropdown = () => {
+    if (this.state.expanded) {
+      return (
+        <div>
+          <CloseOverlay onClick={() => this.setState({ expanded: false })}/>
+          <Dropdown>
+            <DropdownLabel>
+              {this.props.dropdownLabel}
+            </DropdownLabel>
+            {this.renderOptionList()}
+          </Dropdown>
+        </div>
+      )
+    }
+  }
+
+  render() {
+    let { activeValue } = this.props;
+    return (
+      <StyledSelector>
+        <MainSelector
+          onClick={() => this.setState({ expanded: !this.state.expanded })}
+          expanded={this.state.expanded}
+        >
+          <TextWrap>
+            {activeValue === '' ? 'All' : activeValue}
+          </TextWrap>
+          <i className="material-icons">arrow_drop_down</i>
+        </MainSelector>
+        {this.renderDropdown()}
+      </StyledSelector>
+    );
+  }
+}
+
+const TextWrap = styled.div`
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const DropdownLabel = styled.div`
+  font-size: 13px;
+  color: #ffffff44;
+  font-weight: 500;
+  margin: 10px 13px;
+`;
+
+const Option = styled.div` 
+  width: 100%;
+  border-bottom: 1px solid #ffffff10;
+  height: 35px;
+  font-size: 13px;
+  padding-top: 9px;
+  align-items: center;
+  padding-left: 15px;
+  cursor: pointer;
+  padding-right: 10px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  background: ${(props: { selected: boolean }) => props.selected ? '#ffffff11' : ''};
+
+  :hover {
+    background: #ffffff22;
+  }
+`;
+
+const CloseOverlay = styled.div`
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  z-index: 999;
+`;
+
+const Dropdown = styled.div`
+  position: absolute;
+  right: 0;
+  top: calc(100% + 5px);
+  background: #26282f;
+  width: calc(100% + 80px);
+  max-height: 300px;
+  padding-bottom: 20px;
+  border-radius: 3px;
+  z-index: 999;
+  overflow-y: auto;
+  box-shadow: 0 8px 20px 0px #00000055;
+`;
+
+const StyledSelector = styled.div`
+  position: relative;
+`;
+
+const MainSelector = styled.div`
+  width: 150px;
+  height: 30px;
+  border: 1px solid #ffffff66;
+  font-size: 13px;
+  padding: 5px 10px;
+  padding-left: 12px;
+  border-radius: 3px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  cursor: pointer;
+  background: ${(props: { expanded: boolean }) => props.expanded ? '#ffffff33' : '#ffffff11'};
+
+  :hover {
+    background: ${(props: { expanded: boolean }) => props.expanded ? '#ffffff33' : '#ffffff22'};
+  }
+
+  > i {
+    font-size: 20px;
+    transform: ${(props: { expanded: boolean }) => props.expanded ? 'rotate(180deg)' : ''};
+  }
+`;

+ 4 - 1
dashboard/src/main/Main.tsx

@@ -114,6 +114,9 @@ const GlobalStyle = createGlobalStyle`
     box-sizing: border-box;
     font-family: 'Work Sans', sans-serif;
   }
+  body {
+    background: #202227;
+  }
 `;
 
 const StyledMain = styled.div`
@@ -122,6 +125,6 @@ const StyledMain = styled.div`
   position: fixed;
   top: 0;
   left: 0;
-  background: #24272a;
+  background: #202227;
   color: white;
 `;

+ 43 - 23
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -5,14 +5,20 @@ import gradient from '../../../assets/gradient.jpg';
 import { Context } from '../../../shared/Context';
 
 import ChartList from './chart/ChartList';
+import NamespaceSelector from './NamespaceSelector';
 
 type PropsType = {
 };
 
 type StateType = {
+  namespace: string
 };
 
 export default class Dashboard extends Component<PropsType, StateType> {
+  state = {
+    namespace: ''
+  }
+
   render() {
     let { currentCluster } = this.context;
 
@@ -37,8 +43,22 @@ export default class Dashboard extends Component<PropsType, StateType> {
         </InfoSection>
 
         <LineBreak />
-
-        <ChartList currentCluster={currentCluster} />
+        
+        <ControlRow>
+          <Button disabled={true}>
+            <i className="material-icons">add</i> Add a Chart
+          </Button>
+          <NamespaceSelector
+            setNamespace={(namespace) => this.setState({ namespace })}
+            namespace={this.state.namespace}
+            currentCluster={currentCluster}
+          />
+        </ControlRow>
+
+        <ChartList
+          currentCluster={currentCluster}
+          namespace={this.state.namespace}
+        />
       </div>
     );
   }
@@ -46,6 +66,14 @@ export default class Dashboard extends Component<PropsType, StateType> {
 
 Dashboard.contextType = Context;
 
+const ControlRow = styled.div`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 35px;
+  padding-left: 0px;
+`;
+
 const TopRow = styled.div`
   display: flex;
   align-items: center;
@@ -75,43 +103,35 @@ const InfoLabel = styled.div`
 const InfoSection = styled.div`
   margin-top: 20px;
   font-family: 'Work Sans', sans-serif;
-  margin-left: 7px;
+  margin-left: 0px;
   margin-bottom: 35px;
 `;
 
-const ButtonWrap = styled.div`
-  display: flex;
-  align-items: center;
-  font-size: 18px;
-  margin-top: 2px;
-  margin-bottom: 25px;
-  color: #00000020;
-`;
-
 const Button = styled.div`
-  min-width: 145px;
-  max-width: 145px;
   display: flex;
-  flex: 1;
   flex-direction: row;
   align-items: center;
   justify-content: space-between;
   font-size: 13px;
   cursor: pointer;
   font-family: 'Work Sans', sans-serif;
-  margin-left: 5px;
   border-radius: 20px;
   color: white;
-  padding: 6px 8px;
+  height: 30px;
+  padding: 0px 8px;
+  padding-bottom: 1px;
   margin-right: 10px;
-  padding-right: 13px;
+  font-weight: 500;
+  padding-right: 15px;
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
+  box-shadow: 0 5px 8px 0px #00000010;
+  cursor: not-allowed;
 
-  background: #616FEEcc;
+  background: ${(props: { disabled: boolean }) => props.disabled ? '#aaaabbee' :'#616FEEcc'};
   :hover {
-    background: #505edddd;
+    background: ${(props: { disabled: boolean }) => props.disabled ? '' : '#505edddd'};
   }
 
   > i {
@@ -122,7 +142,7 @@ const Button = styled.div`
     border-radius: 20px;
     display: flex;
     align-items: center;
-    margin-top: -1px;
+    margin-right: 8px;
     justify-content: center;
   }
 `;
@@ -196,7 +216,7 @@ const Title = styled.div`
   font-size: 20px;
   font-weight: 500;
   font-family: 'Work Sans', sans-serif;
-  margin-left: 20px;
+  margin-left: 18px;
   color: #ffffff;
   white-space: nowrap;
   overflow: hidden;
@@ -210,7 +230,7 @@ const TitleSection = styled.div`
   display: flex;
   flex-direction: row;
   align-items: center;
-  padding-left: 17px;
+  padding-left: 0px;
 
   > i {
     margin-left: 10px;

+ 87 - 0
dashboard/src/main/home/dashboard/NamespaceSelector.tsx

@@ -0,0 +1,87 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import { Context } from '../../../shared/Context';
+import api from '../../../shared/api';
+
+import Selector from '../../../components/Selector';
+
+type PropsType = {
+  setNamespace: (x: string) => void,
+  currentCluster: string,
+  namespace: string
+};
+
+type StateType = {
+  namespaceOptions: { label: string, value: string }[]
+};
+
+
+// TODO: display selected in option dropdown and actually filter!
+
+export default class NamespaceSelector extends Component<PropsType, StateType> {
+  state = {
+    namespaceOptions: [] as { label: string, value: string }[]
+  }
+
+  updateOptions = () => {
+    let { currentCluster, setCurrentError } = this.context;
+
+    api.getNamespaces('<token>', { context: currentCluster }, {}, (err: any, res: any) => {
+      if (err) {
+        setCurrentError('Could not read clusters: ' + JSON.stringify(err));
+      } else {
+        let namespaceOptions: { label: string, value: string }[] = [];
+        res.data.items.forEach((x: { metadata: { name: string }}, i: number) => {
+          namespaceOptions.push({ label: x.metadata.name, value: x.metadata.name });
+        })
+        this.setState({ namespaceOptions });
+      }
+    });
+  }
+
+  componentDidMount() {
+    this.updateOptions();
+  }
+
+  componentDidUpdate(prevProps: PropsType) {
+    if (prevProps !== this.props) {
+      this.updateOptions();
+    }
+  }
+
+  render() {
+    return ( 
+      <StyledNamespaceSelector>
+        <Label>
+          <i className="material-icons">filter_alt</i> Filter
+        </Label>
+        <Selector
+          activeValue={this.props.namespace}
+          setActiveValue={(namespace) => this.props.setNamespace(namespace)}
+          options={this.state.namespaceOptions}
+          dropdownLabel='Namespace:'
+        />
+      </StyledNamespaceSelector>
+    );
+  }
+}
+
+NamespaceSelector.contextType = Context;
+
+const Label = styled.div`
+  display: flex;
+  align-items: center;
+  margin-right: 12px;
+
+  > i {
+    margin-right: 8px;
+    font-size: 18px;
+  }
+`;
+
+const StyledNamespaceSelector = styled.div`
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+`;

+ 3 - 2
dashboard/src/main/home/dashboard/chart/ChartList.tsx

@@ -9,7 +9,8 @@ import Chart from './Chart';
 import Loading from '../../../../components/Loading';
 
 type PropsType = {
-  currentCluster: string
+  currentCluster: string,
+  namespace: string
 };
 
 type StateType = {
@@ -28,7 +29,7 @@ export default class ChartList extends Component<PropsType, StateType> {
     
     this.setState({ loading: true });
     api.getCharts('<token>', {
-      namespace: '',
+      namespace: this.props.namespace,
       context: currentCluster,
       storage: 'secret',
       limit: 20,

+ 5 - 0
dashboard/src/shared/api.tsx

@@ -50,6 +50,10 @@ const getCharts = baseApi<{
   statusFilter: string[]
 }>('GET', '/api/charts');
 
+const getNamespaces = baseApi<{
+  context: string
+}>('GET', '/api/k8s/namespaces');
+
 // Bundle export to allow default api import (api.<method> is more readable)
 export default {
   checkAuth,
@@ -60,4 +64,5 @@ export default {
   updateUser,
   getContexts,
   getCharts,
+  getNamespaces
 }

+ 52 - 2
docs/API.md

@@ -17,6 +17,7 @@
   - [`GET /api/charts`](#get-apicharts)
   - [`GET /api/charts/{name}/history`](#get-apichartsnamehistory)
   - [`GET /api/charts/{name}/{revision}`](#get-apichartsnamerevision)
+  - [`POST /api/charts/rollback/{name}/{revision}`](#post-apichartsrollbacknamerevision)
 - [`/api/k8s`](#apik8s)
   - [`GET /api/k8s/namespaces`](#get-apik8snamespaces)
 
@@ -208,7 +209,16 @@ User{
 }
 ```
 
-**Successful Response Body**: N/A
+**Successful Response Body**:
+User object with only the id field. Other fields are empty - with values in parantheses.
+```js
+{
+  "id": Int,
+  "email": String ("")
+  "contexts": []String (NULL)
+  "rawKubeConfig": String ("")
+}
+```
 
 **Successful Status Code**: `201`
 
@@ -261,7 +271,16 @@ User{
 }
 ```
 
-**Successful Response Body**: N/A
+**Successful Response Body**:
+User object with only the id field. Other fields are empty - with values in parantheses.
+```js
+{
+  "id": Int,
+  "email": String ("")
+  "contexts": []String (NULL)
+  "rawKubeConfig": String ("")
+}
+```
 
 **Successful Status Code**: `200`
 
@@ -644,6 +663,37 @@ Chart{
 
 **Errors:** TBD
 
+#### `POST /api/charts/rollback/{name}/{revision}`
+
+**Description:** Rolls a release back to a specified revision. 
+
+**URL parameters:** 
+
+- `name` The name of the release.
+- `revision` The number 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")
+}
+```
+
+
+**Successful Response Body**: N/A
+
+**Successful Status Code**: `200`
+
+**Errors:** TBD
+
 ### `/api/k8s`
 
 #### `GET /api/k8s/namespaces`

+ 1 - 0
go.sum

@@ -1249,6 +1249,7 @@ gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
 gorm.io/gorm v1.20.2 h1:bZzSEnq7NDGsrd+n3evOOedDrY5oLM5QPlCjZJUK2ro=
 gorm.io/gorm v1.20.2/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
 gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
+helm.sh/helm v1.2.1 h1:Jrn7kKQqQ/hnFWZEX+9pMFvYqFexkzrBnGqYBmIph7c=
 helm.sh/helm v2.16.12+incompatible h1:nQfifk10KcpAGD1RJaNZVW/fWiqluV0JMuuDwdba4rw=
 helm.sh/helm v2.16.12+incompatible/go.mod h1:0Xbc6ErzwWH9qC55X1+hE3ZwhM3atbhCm/NbFZw5i+4=
 helm.sh/helm/v3 v3.3.4 h1:tbad6WQVMxEw1HlVBvI2rQqOblmI5lgXOrWAMwJ198M=

+ 5 - 1
internal/forms/chart.go

@@ -53,7 +53,11 @@ type ListChartForm struct {
 // url.Values (the parsed query params). It calls the underlying
 // PopulateHelmOptionsFromQueryParams
 func (lcf *ListChartForm) PopulateListFromQueryParams(vals url.Values) {
-	lcf.PopulateHelmOptionsFromQueryParams(vals)
+	lcf.ChartForm.PopulateHelmOptionsFromQueryParams(vals)
+
+	if namespace, ok := vals["namespace"]; ok && len(namespace) == 1 {
+		lcf.ListFilter.Namespace = namespace[0]
+	}
 
 	if limit, ok := vals["limit"]; ok && len(limit) == 1 {
 		if limitInt, err := strconv.ParseInt(limit[0], 10, 64); err == nil {

+ 10 - 0
internal/helm/agent.go

@@ -76,3 +76,13 @@ func (a *Agent) UpgradeChart(
 
 	return res, nil
 }
+
+// RollbackRelease rolls a release back to a specified revision/version
+func (a *Agent) RollbackRelease(
+	name string,
+	version int,
+) error {
+	cmd := action.NewRollback(a.ActionConfig)
+	cmd.Version = version
+	return cmd.Run(name)
+}

+ 60 - 1
internal/helm/agent_test.go

@@ -129,6 +129,26 @@ var listReleaseTests = []listReleaseTest{
 			releaseStub{"wordpress", "default", 1, "1.0.1", release.StatusDeployed},
 		},
 	},
+	listReleaseTest{
+		name:      "simple test only default namespace",
+		namespace: "default",
+		filter: &helm.ListFilter{
+			Namespace:    "",
+			Limit:        20,
+			Skip:         0,
+			ByDate:       false,
+			StatusFilter: []string{"deployed"},
+		},
+		releases: []releaseStub{
+			releaseStub{"airwatch", "default", 1, "1.0.0", release.StatusDeployed},
+			releaseStub{"wordpress", "default", 1, "1.0.1", release.StatusDeployed},
+			releaseStub{"not-in-default-namespace", "other", 1, "1.0.2", release.StatusDeployed},
+		},
+		expRes: []releaseStub{
+			releaseStub{"airwatch", "default", 1, "1.0.0", release.StatusDeployed},
+			releaseStub{"wordpress", "default", 1, "1.0.1", release.StatusDeployed},
+		},
+	},
 	listReleaseTest{
 		name:      "simple test limit",
 		namespace: "",
@@ -189,7 +209,7 @@ var getReleaseTests = []getReleaseTest{
 			releaseStub{"not-in-default-namespace", "other", 1, "1.0.2", release.StatusDeployed},
 		},
 		getName:    "airwatch",
-		getVersion: 0,
+		getVersion: 1,
 		expRes:     releaseStub{"airwatch", "default", 1, "1.0.0", release.StatusDeployed},
 	},
 }
@@ -283,3 +303,42 @@ func TestUpgradeChart(t *testing.T) {
 		compareReleaseToStubs(t, releases, tc.expRes)
 	}
 }
+
+var rollbackReleaseTests = []getReleaseTest{
+	getReleaseTest{
+		name:      "simple rollback test",
+		namespace: "default",
+		releases: []releaseStub{
+			releaseStub{"wordpress", "default", 2, "1.0.2", release.StatusDeployed},
+			releaseStub{"wordpress", "default", 1, "1.0.1", release.StatusSuperseded},
+		},
+		getName:    "wordpress",
+		getVersion: 3,
+		expRes:     releaseStub{"wordpress", "default", 3, "1.0.1", release.StatusDeployed},
+	},
+}
+
+func TestRollbackRelease(t *testing.T) {
+	for _, tc := range rollbackReleaseTests {
+		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)
+
+		err := agent.RollbackRelease("wordpress", 1)
+
+		if err != nil {
+			t.Errorf("%v", err)
+		}
+
+		rel, err := agent.GetRelease(tc.getName, tc.getVersion)
+
+		if err != nil {
+			t.Errorf("%v", err)
+		}
+
+		compareReleaseToStubs(t, []*release.Release{rel}, []releaseStub{tc.expRes})
+	}
+}

+ 61 - 0
server/api/chart_handler.go

@@ -2,6 +2,7 @@ package api
 
 import (
 	"encoding/json"
+	"fmt"
 	"net/http"
 	"net/url"
 	"strconv"
@@ -255,3 +256,63 @@ func (app *App) HandleListChartHistory(w http.ResponseWriter, r *http.Request) {
 // 		return
 // 	}
 // }
+
+// 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)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrChartDecode, w)
+		return
+	}
+
+	name := chi.URLParam(r, "name")
+	revision, err := strconv.ParseUint(chi.URLParam(r, "revision"), 0, 64)
+
+	// get the filter options
+	form := &forms.GetChartForm{
+		ChartForm: &forms.ChartForm{
+			Form: &helm.Form{},
+		},
+		Name:     name,
+		Revision: int(revision),
+	}
+
+	// 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.RollbackRelease(form.Name, form.Revision)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	release, err := agent.GetRelease("wordpress", 3)
+
+	fmt.Println("RELEASE IS", release)
+
+	w.WriteHeader(http.StatusOK)
+}

+ 123 - 7
server/api/chart_handler_test.go

@@ -2,7 +2,9 @@ package api_test
 
 import (
 	"encoding/json"
+	"fmt"
 	"net/http"
+	"net/http/httptest"
 	"net/url"
 	"reflect"
 	"strings"
@@ -27,6 +29,7 @@ type releaseStub struct {
 
 type chartTest struct {
 	initializers []func(tester *tester)
+	namespace    string
 	msg          string
 	method       string
 	endpoint     string
@@ -47,6 +50,8 @@ func testChartRequests(t *testing.T, tests []*chartTest, canQuery bool) {
 			init(tester)
 		}
 
+		tester.app.TestAgents.HelmAgent.ActionConfig.Releases.Driver.(*driver.Memory).SetNamespace(c.namespace)
+
 		req, err := http.NewRequest(
 			c.method,
 			c.endpoint,
@@ -102,7 +107,34 @@ var listChartsTests = []*chartTest{
 		expBody:   releaseStubsToChartJSON(sampleReleaseStubs),
 		useCookie: true,
 		validators: []func(c *chartTest, tester *tester, t *testing.T){
-			chartReleaseBodyValidator,
+			chartReleaseArrBodyValidator,
+		},
+	},
+	&chartTest{
+		initializers: []func(tester *tester){
+			initDefaultCharts,
+		},
+		msg:       "List charts",
+		method:    "GET",
+		namespace: "default",
+		endpoint: "/api/charts?" + url.Values{
+			"namespace":    []string{"default"},
+			"context":      []string{"context-test"},
+			"storage":      []string{"memory"},
+			"limit":        []string{"20"},
+			"skip":         []string{"0"},
+			"byDate":       []string{"false"},
+			"statusFilter": []string{"deployed"},
+		}.Encode(),
+		body:      "",
+		expStatus: http.StatusOK,
+		expBody: releaseStubsToChartJSON([]releaseStub{
+			sampleReleaseStubs[0],
+			sampleReleaseStubs[2],
+		}),
+		useCookie: true,
+		validators: []func(c *chartTest, tester *tester, t *testing.T){
+			chartReleaseArrBodyValidator,
 		},
 	},
 }
@@ -116,9 +148,10 @@ var getChartTests = []*chartTest{
 		initializers: []func(tester *tester){
 			initDefaultCharts,
 		},
-		msg:    "Get charts",
-		method: "GET",
-		endpoint: "/api/charts/airwatch/0?" + url.Values{
+		msg:       "Get charts",
+		method:    "GET",
+		namespace: "default",
+		endpoint: "/api/charts/airwatch/1?" + url.Values{
 			"namespace": []string{""},
 			"context":   []string{"context-test"},
 			"storage":   []string{"memory"},
@@ -142,8 +175,9 @@ var listChartHistoryTests = []*chartTest{
 		initializers: []func(tester *tester){
 			initHistoryCharts,
 		},
-		msg:    "List chart history",
-		method: "GET",
+		msg:       "List chart history",
+		method:    "GET",
+		namespace: "default",
 		endpoint: "/api/charts/wordpress/history?" + url.Values{
 			"namespace": []string{""},
 			"context":   []string{"context-test"},
@@ -154,7 +188,7 @@ var listChartHistoryTests = []*chartTest{
 		expBody:   releaseStubsToChartJSON(historyReleaseStubs),
 		useCookie: true,
 		validators: []func(c *chartTest, tester *tester, t *testing.T){
-			chartReleaseBodyValidator,
+			chartReleaseArrBodyValidator,
 		},
 	},
 }
@@ -163,6 +197,75 @@ func TestHandleListChartHistory(t *testing.T) {
 	testChartRequests(t, listChartHistoryTests, true)
 }
 
+var rollbackChartTests = []*chartTest{
+	&chartTest{
+		initializers: []func(tester *tester){
+			initHistoryCharts,
+		},
+		msg:       "Rollback relase",
+		method:    "POST",
+		namespace: "default",
+		endpoint:  "/api/charts/rollback/wordpress/1",
+		body: `
+			{
+				"namespace": "default",
+				"context": "context-test",
+				"storage": "memory"
+			}
+		`,
+		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.1", release.StatusDeployed})
+
+				fmt.Println(rr2.Body.String())
+
+				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)
+				}
+			},
+		},
+	},
+}
+
+func TestRollbackChart(t *testing.T) {
+	testChartRequests(t, rollbackChartTests, true)
+}
+
 // ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
 
 func initDefaultCharts(tester *tester) {
@@ -250,6 +353,19 @@ func makeReleases(agent *helm.Agent, rels []releaseStub) {
 }
 
 func chartReleaseBodyValidator(c *chartTest, tester *tester, t *testing.T) {
+	gotBody := &release.Release{}
+	expBody := &release.Release{}
+
+	json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
+	json.Unmarshal([]byte(c.expBody), expBody)
+
+	if !reflect.DeepEqual(gotBody, expBody) {
+		t.Errorf("%s, handler returned wrong body: got %v want %v",
+			c.msg, gotBody, expBody)
+	}
+}
+
+func chartReleaseArrBodyValidator(c *chartTest, tester *tester, t *testing.T) {
 	gotBody := &[]release.Release{}
 	expBody := &[]release.Release{}
 

+ 6 - 7
server/api/user_handler.go

@@ -50,12 +50,12 @@ func (app *App) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
 		session.Values["user_id"] = user.ID
 		session.Save(r, w)
 
+		w.WriteHeader(http.StatusCreated)
+
 		if err := app.sendUserID(w, user.ID); err != nil {
 			app.handleErrorFormDecoding(err, ErrUserDecode, w)
 			return
 		}
-
-		w.WriteHeader(http.StatusCreated)
 	}
 }
 
@@ -69,12 +69,12 @@ func (app *App) HandleAuthCheck(w http.ResponseWriter, r *http.Request) {
 
 	userID, _ := session.Values["user_id"].(uint)
 
+	w.WriteHeader(http.StatusOK)
+
 	if err := app.sendUserID(w, userID); err != nil {
 		app.handleErrorFormDecoding(err, ErrUserDecode, w)
 		return
 	}
-
-	w.WriteHeader(http.StatusOK)
 }
 
 // HandleLoginUser checks the request header for cookie and validates the user.
@@ -119,12 +119,12 @@ func (app *App) HandleLoginUser(w http.ResponseWriter, r *http.Request) {
 		app.logger.Warn().Err(err)
 	}
 
+	w.WriteHeader(http.StatusOK)
+
 	if err := app.sendUserID(w, storedUser.ID); err != nil {
 		app.handleErrorFormDecoding(err, ErrUserDecode, w)
 		return
 	}
-
-	w.WriteHeader(http.StatusOK)
 }
 
 // HandleLogoutUser detaches the user from the session
@@ -343,7 +343,6 @@ func (app *App) sendUserID(w http.ResponseWriter, userID uint) error {
 	resUser := &models.UserExternal{
 		ID: userID,
 	}
-
 	if err := json.NewEncoder(w).Encode(resUser); err != nil {
 		return err
 	}

+ 40 - 4
server/api/user_handler_test.go

@@ -70,6 +70,42 @@ func testUserRequests(t *testing.T, tests []*userTest, canQuery bool) {
 
 // ------------------------- TEST FIXTURES AND FUNCTIONS  ------------------------- //
 
+var authCheckTests = []*userTest{
+	&userTest{
+		initializers: []func(tester *tester){
+			initUserDefault,
+		},
+		msg:       "Auth check successful. User is logged in.",
+		method:    "GET",
+		endpoint:  "/api/auth/check",
+		expStatus: http.StatusOK,
+		body:      "",
+		expBody:   `{"id":1,"email":"","contexts":null,"rawKubeConfig":""}`,
+		useCookie: true,
+		validators: []func(c *userTest, tester *tester, t *testing.T){
+			userBasicBodyValidator,
+		},
+	},
+	&userTest{
+		initializers: []func(tester *tester){
+			initUserDefault,
+		},
+		msg:       "Auth check failure. User is not logged in.",
+		method:    "GET",
+		endpoint:  "/api/auth/check",
+		body:      "",
+		expStatus: http.StatusForbidden,
+		expBody:   http.StatusText(http.StatusForbidden) + "\n",
+		validators: []func(c *userTest, tester *tester, t *testing.T){
+			userBasicBodyValidator,
+		},
+	},
+}
+
+func TestHandleAuthCheck(t *testing.T) {
+	testUserRequests(t, authCheckTests, true)
+}
+
 var createUserTests = []*userTest{
 	&userTest{
 		msg:      "Create user",
@@ -80,7 +116,7 @@ var createUserTests = []*userTest{
 			"password": "hello"
 		}`,
 		expStatus: http.StatusCreated,
-		expBody:   "",
+		expBody:   `{"id":1,"email":"","contexts":null,"rawKubeConfig":""}`,
 	},
 	&userTest{
 		msg:      "Create user invalid email",
@@ -180,7 +216,7 @@ var loginUserTests = []*userTest{
 			"password": "hello"
 		}`,
 		expStatus: http.StatusOK,
-		expBody:   ``,
+		expBody:   `{"id":1,"email":"","contexts":null,"rawKubeConfig":""}`,
 		validators: []func(c *userTest, tester *tester, t *testing.T){
 			userBasicBodyValidator,
 		},
@@ -197,7 +233,7 @@ var loginUserTests = []*userTest{
 			"password": "hello"
 		}`,
 		expStatus: http.StatusOK,
-		expBody:   ``,
+		expBody:   `{"id":1,"email":"","contexts":null,"rawKubeConfig":""}`,
 		useCookie: true,
 		validators: []func(c *userTest, tester *tester, t *testing.T){
 			userBasicBodyValidator,
@@ -670,7 +706,7 @@ func initUserWithContexts(tester *tester) {
 }
 
 func userBasicBodyValidator(c *userTest, tester *tester, t *testing.T) {
-	if body := tester.rr.Body.String(); body != c.expBody {
+	if body := tester.rr.Body.String(); strings.TrimSpace(body) != strings.TrimSpace(c.expBody) {
 		t.Errorf("%s, handler returned wrong body: got %v want %v",
 			c.msg, body, c.expBody)
 	}

+ 1 - 0
server/router/router.go

@@ -31,6 +31,7 @@ func New(a *api.App, store sessions.Store, cookieName string) *chi.Mux {
 		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)))
+		r.Method("POST", "/charts/rollback/{name}/{revision}", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleRollbackChart, l)))
 
 		// /api/k8s routes
 		r.Method("GET", "/k8s/namespaces", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleListNamespaces, l)))

Fișier diff suprimat deoarece este prea mare
+ 0 - 124
test.yaml


Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff