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

Merge branch 'api-charts' of https://github.com/porter-dev/porter into frontend-integration

jusrhee 5 лет назад
Родитель
Сommit
9add7d9011

+ 2 - 0
.gitignore

@@ -1,3 +1,5 @@
 .DS_Store
 .DS_Store
 .env
 .env
 app
 app
+*.db
+

+ 5 - 2
cmd/app/main.go

@@ -5,6 +5,7 @@ import (
 	"log"
 	"log"
 	"net/http"
 	"net/http"
 
 
+	"github.com/gorilla/sessions"
 	"github.com/porter-dev/porter/internal/repository/gorm"
 	"github.com/porter-dev/porter/internal/repository/gorm"
 
 
 	"github.com/porter-dev/porter/server/api"
 	"github.com/porter-dev/porter/server/api"
@@ -30,11 +31,13 @@ func main() {
 
 
 	repo := gorm.NewRepository(db)
 	repo := gorm.NewRepository(db)
 
 
-	store, _ := sessionstore.NewStore(repo, appConf.Server)
+	// declare as Store interface (methods Get, New, Save)
+	var store sessions.Store
+	store, _ = sessionstore.NewStore(repo, appConf.Server)
 
 
 	validator := vr.New()
 	validator := vr.New()
 
 
-	a := api.New(logger, repo, validator, store, &appConf.Helm, appConf.Server.CookieName)
+	a := api.New(logger, repo, validator, store, appConf.Server.CookieName, false)
 
 
 	appRouter := router.New(a, store, appConf.Server.CookieName)
 	appRouter := router.New(a, store, appConf.Server.CookieName)
 
 

+ 1 - 1
dashboard/src/main/Login.tsx

@@ -40,7 +40,7 @@ export default class Login extends Component<PropsType, StateType> {
         password: password
         password: password
       }, {}, (err: any, res: any) => {
       }, {}, (err: any, res: any) => {
         // TODO: case and set credential error
         // TODO: case and set credential error
-        err ? setCurrentError(JSON.stringify(err)) : authenticate();
+        err ? setCurrentError(err.response.data.errors[0]) : authenticate();
       });
       });
     }
     }
   }
   }

+ 1 - 1
dashboard/src/main/Register.tsx

@@ -48,7 +48,7 @@ export default class Register extends Component<PropsType, StateType> {
         email: email,
         email: email,
         password: password
         password: password
       }, {}, (err: any, res: any) => {
       }, {}, (err: any, res: any) => {
-        err ? setCurrentError(JSON.stringify(err)) : authenticate();
+        err ? setCurrentError(err.response.data.errors[0]) : authenticate();
       });
       });
     } 
     } 
   };
   };

+ 2 - 2
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -84,8 +84,8 @@ export default class Sidebar extends Component<PropsType, StateType> {
     api.logOutUser('<token>', {}, {}, (err: any, res: any) => {
     api.logOutUser('<token>', {}, {}, (err: any, res: any) => {
       // TODO: case and set logout error
       // TODO: case and set logout error
       
       
-      err ? setCurrentError(JSON.stringify(err)) : logOut();
-    });
+      err ? setCurrentError(err.response.data.errors[0]) : logOut();
+    }); 
   }
   }
 
 
   // SidebarBg is separate to cover retracted drawer
   // SidebarBg is separate to cover retracted drawer

+ 1 - 1
dashboard/src/shared/Context.tsx

@@ -51,7 +51,7 @@ class ContextProvider extends Component {
   };
   };
 
 
   componentDidMount() {
   componentDidMount() {
-    this.setState({ userId: 1 });
+    this.setState({ userId: 3 });
   }
   }
 
 
   render() {
   render() {

+ 0 - 1
dashboard/src/shared/baseApi.tsx

@@ -39,7 +39,6 @@ export const baseApi = <T extends {}, S = {}>(requestType: string, endpoint: ((p
       });
       });
     } else {
     } else {
       axios.get(endpointString, {
       axios.get(endpointString, {
-        withCredentials: true,
         params
         params
       })
       })
       .then(res => {
       .then(res => {

+ 3 - 1
docker/.env

@@ -10,4 +10,6 @@ DB_PORT=5432
 DB_USER=porter
 DB_USER=porter
 DB_PASS=porter
 DB_PASS=porter
 DB_NAME=porter
 DB_NAME=porter
-COOKIE_SECRETS=secret
+COOKIE_SECRETS=secret
+
+QUICK_START=false

+ 1 - 1
docker/dev.Dockerfile

@@ -16,4 +16,4 @@ RUN go build -ldflags '-w -s' -a -o ./bin/migrate ./cmd/migrate \
 # for live reloading of go container
 # for live reloading of go container
 RUN go get github.com/cosmtrek/air
 RUN go get github.com/cosmtrek/air
 
 
-CMD air -c .air.toml
+CMD /porter/bin/migrate; air -c .air.toml

+ 220 - 0
docs/API.md

@@ -13,6 +13,12 @@
   - [`POST /api/logout`](#post-apilogout)
   - [`POST /api/logout`](#post-apilogout)
   - [`PUT /api/users/{id}`](#put-apiusersid)
   - [`PUT /api/users/{id}`](#put-apiusersid)
   - [`DELETE /api/users/{id}`](#delete-apiusersid)
   - [`DELETE /api/users/{id}`](#delete-apiusersid)
+- [`/api/charts`](#apicharts)
+  - [`GET /api/charts`](#get-apicharts)
+  - [`GET /api/charts/{name}/{revision}`](#get-apichartsnamerevision)
+- [`/api/k8s`](#apik8s)
+  - [`GET /api/k8s/namespaces`](#get-apik8snamespaces)
+
 
 
 ### Overview
 ### Overview
 
 
@@ -392,3 +398,217 @@ User{
     }
     }
     ```
     ```
 
 
+### `/api/charts`
+
+#### `GET /api/charts`
+
+**Description:** Gets a list of charts for a current context and a kubeconfig retrieved from the user's ID. 
+
+**URL parameters:** N/A
+
+**Query parameters:** N/A
+
+**Request Body**:
+
+```js
+{
+  "user_id": Number,
+  "helm": {
+    // 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")
+  },
+  "filter": {
+    "namespace": String,
+    "limit": Number,
+    "skip": Number,
+    "byDate": Boolean,
+    "statusFilter": []String
+  }
+}
+```
+
+**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. 
+
+**URL parameters:** 
+
+- `name` The name of the release.
+- `revision` The number of the release (set to `0` for the latest deployed release).
+
+**Query parameters:** N/A
+
+**Request Body**:
+
+```js
+{
+  "user_id": Number,
+  "helm": {
+    // 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**: 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
+
+### `/api/k8s`
+
+#### `GET /api/k8s/namespaces`
+
+**Description:** 
+
+**URL parameters:** N/A
+
+**Query parameters:** N/A
+
+**Request Body**: 
+
+```js
+{
+  "user_id": Number,
+  "k8s": {
+    // The name of the context in the kubeconfig being used
+    "context": String,
+  }
+}
+```
+
+**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

+ 1 - 0
go.mod

@@ -40,6 +40,7 @@ require (
 	gopkg.in/go-playground/validator.v9 v9.31.0
 	gopkg.in/go-playground/validator.v9 v9.31.0
 	gopkg.in/yaml.v2 v2.3.0
 	gopkg.in/yaml.v2 v2.3.0
 	gorm.io/driver/postgres v1.0.2
 	gorm.io/driver/postgres v1.0.2
+	gorm.io/driver/sqlite v1.1.3
 	gorm.io/gorm v1.20.2
 	gorm.io/gorm v1.20.2
 	helm.sh/helm v2.16.12+incompatible
 	helm.sh/helm v2.16.12+incompatible
 	helm.sh/helm/v3 v3.3.4
 	helm.sh/helm/v3 v3.3.4

+ 4 - 104
go.sum

@@ -625,6 +625,8 @@ github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lL
 github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
 github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
+github.com/mattn/go-sqlite3 v1.14.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA=
+github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
@@ -703,9 +705,7 @@ github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIw
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
 github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
 github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
-github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
 github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
 github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
 github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
 github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
 github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
 github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
@@ -781,7 +781,6 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
 github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
 github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
-github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
 github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
@@ -815,12 +814,10 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3
 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -895,7 +892,6 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -1041,18 +1037,13 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw=
 golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8=
 golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8=
 golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d h1:L/IKR6COd7ubZrs2oTnTi73IhgqJ71c9s80WsQnh0Es=
-golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1153,7 +1144,6 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
 google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
 google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@@ -1228,7 +1218,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
 gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
 gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
 gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
-gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M=
 gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
 gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
 gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw=
 gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw=
 gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
 gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
@@ -1250,18 +1239,14 @@ gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/driver/postgres v1.0.1 h1:jRfDNUxpxNrea/97kbcscAQGmiks4UCKAYXsvh4rhOQ=
-gorm.io/driver/postgres v1.0.1/go.mod h1:pv4dVhHvEVrP7k/UYqdBIllbdbpB5VTz89X1O0uOrCA=
 gorm.io/driver/postgres v1.0.2 h1:mB5JjD4QglbCTdMT1aZDxQzHr87XDK1qh0MKIU3P96g=
 gorm.io/driver/postgres v1.0.2 h1:mB5JjD4QglbCTdMT1aZDxQzHr87XDK1qh0MKIU3P96g=
 gorm.io/driver/postgres v1.0.2/go.mod h1:FvRSYfBI9jEp6ZSjlpS9qNcSjxwYxFc03UOTrHdvvYA=
 gorm.io/driver/postgres v1.0.2/go.mod h1:FvRSYfBI9jEp6ZSjlpS9qNcSjxwYxFc03UOTrHdvvYA=
-gorm.io/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
-gorm.io/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
-gorm.io/gorm v1.20.1 h1:+hOwlHDqvqmBIMflemMVPLJH7tZYK4RxFDBHEfJTup0=
+gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=
+gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
 gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
 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 h1:bZzSEnq7NDGsrd+n3evOOedDrY5oLM5QPlCjZJUK2ro=
 gorm.io/gorm v1.20.2/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
 gorm.io/gorm v1.20.2/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
 gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
 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 h1:nQfifk10KcpAGD1RJaNZVW/fWiqluV0JMuuDwdba4rw=
 helm.sh/helm v2.16.12+incompatible/go.mod h1:0Xbc6ErzwWH9qC55X1+hE3ZwhM3atbhCm/NbFZw5i+4=
 helm.sh/helm v2.16.12+incompatible/go.mod h1:0Xbc6ErzwWH9qC55X1+hE3ZwhM3atbhCm/NbFZw5i+4=
 helm.sh/helm/v3 v3.3.4 h1:tbad6WQVMxEw1HlVBvI2rQqOblmI5lgXOrWAMwJ198M=
 helm.sh/helm/v3 v3.3.4 h1:tbad6WQVMxEw1HlVBvI2rQqOblmI5lgXOrWAMwJ198M=
@@ -1274,121 +1259,38 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-k8s.io/api v0.0.0-20200914174313-52bf62410745 h1:hEgy7kHg2J1GrSDvkf+IlbZWFBi536OTioyP1Cqujro=
-k8s.io/api v0.0.0-20200914174313-52bf62410745/go.mod h1:UT3vnXQcd48N6K0IuGGYk1ufh1lolzq+pC4aE2BPvWA=
 k8s.io/api v0.18.8 h1:aIKUzJPb96f3fKec2lxtY7acZC9gQNDLVhfSGpxBAC4=
 k8s.io/api v0.18.8 h1:aIKUzJPb96f3fKec2lxtY7acZC9gQNDLVhfSGpxBAC4=
 k8s.io/api v0.18.8/go.mod h1:d/CXqwWv+Z2XEG1LgceeDmHQwpUJhROPx16SlxJgERY=
 k8s.io/api v0.18.8/go.mod h1:d/CXqwWv+Z2XEG1LgceeDmHQwpUJhROPx16SlxJgERY=
-k8s.io/api v0.18.9-rc.0/go.mod h1:32vFAuLAX89RZVkp4GtXk81UEzc/lDGxAYghfu8qFEI=
-k8s.io/api v0.18.9/go.mod h1:9u/h6sUh6FxfErv7QqetX1EB3yBMIYOBXzdcf0Gf0rc=
-k8s.io/api v0.18.10-rc.0/go.mod h1:+/nAEryKg7rREk9NaRCGstia/6qH7sR6epIIIDu04O0=
-k8s.io/api v0.19.0-alpha.0/go.mod h1:gwFXFTdIdFlWazixRQwbq3p9mI191fabextJ/aOC1k8=
-k8s.io/api v0.19.0-alpha.1/go.mod h1:fNr5XDW1/+18jRxdpIGIdL8AeDhv/8y7jmMGTpIdAoA=
-k8s.io/api v0.19.0-alpha.2/go.mod h1:ujOZQ0qV79Ae02qAditrRXodlKq4GMe7khc3uS2hhEw=
-k8s.io/api v0.19.0-alpha.3/go.mod h1:W/NfI00K9E3/pXwbly0FrdxxtjIjUs/v4m3Li5pT3J8=
-k8s.io/api v0.19.0-beta.0/go.mod h1:6NPoy+1qu2rrljjwWauQMCxWpN1/VfhYnwCB6EB8Mn4=
-k8s.io/api v0.19.0-beta.1/go.mod h1:aZcY31HnWwA9OGlx1K9jTTG0gD7hXXLwzpIVn1Td1gw=
-k8s.io/api v0.19.0-beta.2/go.mod h1:LgaR0+wwwUQzSn968ds/S5cabkwptRrQB1VKZYB1zZg=
-k8s.io/api v0.19.0-rc.0/go.mod h1:WBGMHEmngOdQBAvJiYUgP5mGDdCWXM52yDm1gtos8C0=
-k8s.io/api v0.19.0-rc.1/go.mod h1:NdiA9gl+9BuvhHU0WfqE5A7SrA4iO5TrmRE0caDteuA=
-k8s.io/api v0.19.0-rc.2/go.mod h1:9nHeM2gbqeaL7yN6UFvOxKzLG5gZ4v+DJ6bpavDetZo=
-k8s.io/api v0.19.0-rc.3/go.mod h1:hfDN4tL/yqfs4aVrlvx2vNMhKACfoiTyGSvVnygqSd4=
-k8s.io/api v0.19.0-rc.4/go.mod h1:1xlMhKahfl3bVAn1T1PhMriUVYwRNJ7D8YMDnUz/yGw=
-k8s.io/api v0.19.0/go.mod h1:I1K45XlvTrDjmj5LoM5LuP/KYrhWbjUKT/SoPG0qTjw=
-k8s.io/api v0.19.1-rc.0/go.mod h1:T5DSfVHz1QXbjy7ezqThvoKt7j+goH0ltauR0oz4BGM=
-k8s.io/api v0.19.1/go.mod h1:+u/k4/K/7vp4vsfdT7dyl8Oxk1F26Md4g5F26Tu85PU=
-k8s.io/api v0.19.2-rc.0/go.mod h1:GOfZBLBzYQ228kgdElkfOSQ0eM8jeInXsVLF1PY0h70=
-k8s.io/api v0.19.2 h1:q+/krnHWKsL7OBZg/rxnycsl9569Pud76UJ77MvKXms=
-k8s.io/api v0.19.2/go.mod h1:IQpK0zFQ1xc5iNIQPqzgoOwuFugaYHK4iCknlAQP9nI=
 k8s.io/apiextensions-apiserver v0.18.8 h1:pkqYPKTHa0/3lYwH7201RpF9eFm0lmZDFBNzhN+k/sA=
 k8s.io/apiextensions-apiserver v0.18.8 h1:pkqYPKTHa0/3lYwH7201RpF9eFm0lmZDFBNzhN+k/sA=
 k8s.io/apiextensions-apiserver v0.18.8/go.mod h1:7f4ySEkkvifIr4+BRrRWriKKIJjPyg9mb/p63dJKnlM=
 k8s.io/apiextensions-apiserver v0.18.8/go.mod h1:7f4ySEkkvifIr4+BRrRWriKKIJjPyg9mb/p63dJKnlM=
-k8s.io/apimachinery v0.0.0-20200910171558-1173d23fd476/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA=
-k8s.io/apimachinery v0.0.0-20200916235632-714f1137f89b/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA=
 k8s.io/apimachinery v0.18.8 h1:jimPrycCqgx2QPearX3to1JePz7wSbVLq+7PdBTTwQ0=
 k8s.io/apimachinery v0.18.8 h1:jimPrycCqgx2QPearX3to1JePz7wSbVLq+7PdBTTwQ0=
 k8s.io/apimachinery v0.18.8/go.mod h1:6sQd+iHEqmOtALqOFjSWp2KZ9F0wlU/nWm0ZgsYWMig=
 k8s.io/apimachinery v0.18.8/go.mod h1:6sQd+iHEqmOtALqOFjSWp2KZ9F0wlU/nWm0ZgsYWMig=
-k8s.io/apimachinery v0.18.9-rc.0/go.mod h1:6sQd+iHEqmOtALqOFjSWp2KZ9F0wlU/nWm0ZgsYWMig=
-k8s.io/apimachinery v0.18.9/go.mod h1:PF5taHbXgTEJLU+xMypMmYTXTWPJ5LaW8bfsisxnEXk=
-k8s.io/apimachinery v0.18.10-rc.0/go.mod h1:PF5taHbXgTEJLU+xMypMmYTXTWPJ5LaW8bfsisxnEXk=
-k8s.io/apimachinery v0.19.0-alpha.0/go.mod h1:5X8oEhnd931nEg6/Nkumo00nT6ZsCLp2h7Xwd7Ym6P4=
-k8s.io/apimachinery v0.19.0-alpha.1/go.mod h1:MwmRUlFgPZZjQ9mmX205Ve0gth+HzXB7tiAFmJilVME=
-k8s.io/apimachinery v0.19.0-alpha.2/go.mod h1:imoz42hIYwpLTRWXU8pdJ9IE8DbxUsnU9lyVN8Y1SNo=
-k8s.io/apimachinery v0.19.0-alpha.3/go.mod h1:imoz42hIYwpLTRWXU8pdJ9IE8DbxUsnU9lyVN8Y1SNo=
-k8s.io/apimachinery v0.19.0-beta.0/go.mod h1:x4z2+k1N0YTBvV8PmaVs4/hSmKVVENZmTqI8gBygpLA=
-k8s.io/apimachinery v0.19.0-beta.1/go.mod h1:x4z2+k1N0YTBvV8PmaVs4/hSmKVVENZmTqI8gBygpLA=
-k8s.io/apimachinery v0.19.0-beta.2/go.mod h1:diAekxQB6O2LunkgrS6bHwK4dfE2K8KIxK3GeFjrgBU=
-k8s.io/apimachinery v0.19.0-rc.0/go.mod h1:EjWiYOPi+BZennZ5pGa3JLkQ+znhEOodGy/+umjiLDU=
-k8s.io/apimachinery v0.19.0-rc.1/go.mod h1:eHbWZVMaaewmYBAUuRYnAmTTMtDhvpPNZuh8/6Yl7v0=
-k8s.io/apimachinery v0.19.0-rc.2/go.mod h1:eHbWZVMaaewmYBAUuRYnAmTTMtDhvpPNZuh8/6Yl7v0=
-k8s.io/apimachinery v0.19.0-rc.3/go.mod h1:oE8UQU9DqIIc9PyIEYxTj/oJECzZLymCEU9dL0H4F+o=
-k8s.io/apimachinery v0.19.0-rc.4/go.mod h1:oE8UQU9DqIIc9PyIEYxTj/oJECzZLymCEU9dL0H4F+o=
-k8s.io/apimachinery v0.19.0/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA=
-k8s.io/apimachinery v0.19.1-rc.0/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA=
-k8s.io/apimachinery v0.19.1/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA=
-k8s.io/apimachinery v0.19.2-rc.0/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA=
-k8s.io/apimachinery v0.19.2 h1:5Gy9vQpAGTKHPVOh5c4plE274X8D/6cuEiTO2zve7tc=
-k8s.io/apimachinery v0.19.2/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA=
 k8s.io/apiserver v0.18.8/go.mod h1:12u5FuGql8Cc497ORNj79rhPdiXQC4bf53X/skR/1YM=
 k8s.io/apiserver v0.18.8/go.mod h1:12u5FuGql8Cc497ORNj79rhPdiXQC4bf53X/skR/1YM=
 k8s.io/cli-runtime v0.18.8 h1:ycmbN3hs7CfkJIYxJAOB10iW7BVPmXGXkfEyiV9NJ+k=
 k8s.io/cli-runtime v0.18.8 h1:ycmbN3hs7CfkJIYxJAOB10iW7BVPmXGXkfEyiV9NJ+k=
 k8s.io/cli-runtime v0.18.8/go.mod h1:7EzWiDbS9PFd0hamHHVoCY4GrokSTPSL32MA4rzIu0M=
 k8s.io/cli-runtime v0.18.8/go.mod h1:7EzWiDbS9PFd0hamHHVoCY4GrokSTPSL32MA4rzIu0M=
-k8s.io/cli-runtime v0.18.9-rc.0/go.mod h1:CAw3/xS/Kb7ERMha5eu2F/KGUDGecQT+8ghA74GHBms=
-k8s.io/cli-runtime v0.18.9/go.mod h1:Pw7UPmZd/wIlGd7DWGTUWA7qn92jCeybNeiS5WYJI6A=
-k8s.io/cli-runtime v0.18.10-rc.0/go.mod h1:+ilfZmUD5Be0SsqhUP1n/HfwBVFO5sJzOjxX2fYYDeA=
-k8s.io/cli-runtime v0.19.0-alpha.0/go.mod h1:UG9FPBN/kIdVGF8Iz6EsCqIHR3FJNOaKGbSRPgcNg8E=
-k8s.io/cli-runtime v0.19.0-alpha.1/go.mod h1:vTsSzQZWQfN4IXDBSWqIgWyICm7i6TAQG4Mlw3b4JcI=
-k8s.io/cli-runtime v0.19.0-alpha.2/go.mod h1:k2LnE5XW0j4nuDrfdkiXNOxLyNRAtN8Zd5udvGL/FyI=
-k8s.io/cli-runtime v0.19.0-alpha.3/go.mod h1:9Rd1gtyri8aAR3k13nZ3/iLv7fFTk2nvCEM/AIl2PAA=
-k8s.io/cli-runtime v0.19.0-beta.0/go.mod h1:CekH2Cm+SCrZXL28ylmfPcngWaTCqOovWylKSq6juyc=
-k8s.io/cli-runtime v0.19.0-beta.1/go.mod h1:WdBehFU2wQa+cT53trT/BZsRlAv/oQAwlNq43B53BoI=
-k8s.io/cli-runtime v0.19.0-beta.2/go.mod h1:xV5hDb8a5U9ep1Qu8YbSeqY63FL5m307+WP8mZ6fNR8=
-k8s.io/cli-runtime v0.19.0-rc.0/go.mod h1:91S0K5jlSkD5spOMdlcGuJp4nNO0Etdk/XvEcX90xws=
-k8s.io/cli-runtime v0.19.0-rc.1/go.mod h1:ygoCd1Hj7nwjxJkt2VJMGLiRNxC8BewCX41kwhkyGAs=
-k8s.io/cli-runtime v0.19.0-rc.2/go.mod h1:AeERvBzh6NuhN5I3D3gJhjzMBjmZKfuyV4cIB0SY5+w=
-k8s.io/cli-runtime v0.19.0-rc.3/go.mod h1:9Gx6aAV0meCZhUk20b+o8oSROGrIeuT1CofCKgnqXhA=
-k8s.io/cli-runtime v0.19.0-rc.4/go.mod h1:t3T2QwNf8DoGLBVbTO24lvpXShK65xbrTOnM4grRPyI=
-k8s.io/cli-runtime v0.19.0/go.mod h1:tun9l0eUklT8IHIM0jors17KmUjcrAxn0myoBYwuNuo=
-k8s.io/cli-runtime v0.19.1-rc.0/go.mod h1:ENz5EYxOnrf6IOu0usywgCha4Ltl7a4w5Sbz6sR51sM=
-k8s.io/cli-runtime v0.19.1/go.mod h1:X6g8e4NBiG8GMsKewXsRpo36MO6xrvXa+0wCg7zO4aU=
-k8s.io/cli-runtime v0.19.2-rc.0/go.mod h1:kR2WVmAw0WDUYxF9TVlkfmLEoq8k760JWeks3pWZ2kU=
-k8s.io/cli-runtime v0.19.2 h1:d4uOtKhy3ImdaKqZJ8yQgLrdtUwsJLfP4Dw7L/kVPOo=
-k8s.io/cli-runtime v0.19.2/go.mod h1:CMynmJM4Yf02TlkbhKxoSzi4Zf518PukJ5xep/NaNeY=
-k8s.io/client-go v0.0.0-20200917000235-cba7285b7f29 h1:2lPJxQtfK6wMW+gN6WrQFCZ5dYiFpgDphXneGsE3mZU=
-k8s.io/client-go v0.0.0-20200917000235-cba7285b7f29/go.mod h1:Plj2rfLmeMYfAMuMgA/1EGuUaDxt78tvB9yfdi6fg6A=
 k8s.io/client-go v0.18.8 h1:SdbLpIxk5j5YbFr1b7fq8S7mDgDjYmUxSbszyoesoDM=
 k8s.io/client-go v0.18.8 h1:SdbLpIxk5j5YbFr1b7fq8S7mDgDjYmUxSbszyoesoDM=
 k8s.io/client-go v0.18.8/go.mod h1:HqFqMllQ5NnQJNwjro9k5zMyfhZlOwpuTLVrxjkYSxU=
 k8s.io/client-go v0.18.8/go.mod h1:HqFqMllQ5NnQJNwjro9k5zMyfhZlOwpuTLVrxjkYSxU=
-k8s.io/client-go v0.19.2 h1:gMJuU3xJZs86L1oQ99R4EViAADUPMHHtS9jFshasHSc=
-k8s.io/client-go v0.19.2/go.mod h1:S5wPhCqyDNAlzM9CnEdgTGV4OqhsW3jGO1UM1epwfJA=
-k8s.io/client-go v1.5.1 h1:XaX/lo2/u3/pmFau8HN+sB5C/b4dc4Dmm2eXjBH4p1E=
-k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o=
-k8s.io/client-go v11.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
 k8s.io/code-generator v0.18.8/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c=
 k8s.io/code-generator v0.18.8/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c=
 k8s.io/component-base v0.18.8 h1:BW5CORobxb6q5mb+YvdwQlyXXS6NVH5fDXWbU7tf2L8=
 k8s.io/component-base v0.18.8 h1:BW5CORobxb6q5mb+YvdwQlyXXS6NVH5fDXWbU7tf2L8=
 k8s.io/component-base v0.18.8/go.mod h1:00frPRDas29rx58pPCxNkhUfPbwajlyyvu8ruNgSErU=
 k8s.io/component-base v0.18.8/go.mod h1:00frPRDas29rx58pPCxNkhUfPbwajlyyvu8ruNgSErU=
 k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 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-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
-k8s.io/helm v1.2.1 h1:Ny4wgW4p7X3tFXR34PziNkUxw2pV0G1DIFmI1QRDdo0=
-k8s.io/helm v2.16.12+incompatible h1:K2zhF8+B85Ya1n7n3eH34xwwp5qNUM42TBFENDZJT7w=
 k8s.io/helm v2.16.12+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI=
 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.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
 k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
 k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
 k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
 k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
 k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
-k8s.io/klog/v2 v2.1.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
 k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A=
 k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A=
 k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
 k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
-k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
-k8s.io/kube-openapi v0.0.0-20200403204345-e1beb1bd0f35/go.mod h1:NwPpO8COeh/j9Q9ModsqBxwHcWDo/PmrJOPyquZCC1A=
 k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6 h1:Oh3Mzx5pJ+yIumsAD0MOECPVeXsVot0UkiaCGVyfGQY=
 k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6 h1:Oh3Mzx5pJ+yIumsAD0MOECPVeXsVot0UkiaCGVyfGQY=
 k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
 k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
-k8s.io/kube-openapi v0.0.0-20200427153329-656914f816f9/go.mod h1:bfCVj+qXcEaE5SCvzBaqpOySr6tuCcpPKqF6HD8nyCw=
-k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6 h1:+WnxoVtG8TMiudHBSEtrVL1egv36TkkJm+bA8AxicmQ=
-k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o=
 k8s.io/kubectl v0.18.8 h1:qTkHCz21YmK0+S0oE6TtjtxmjeDP42gJcZJyRKsIenA=
 k8s.io/kubectl v0.18.8 h1:qTkHCz21YmK0+S0oE6TtjtxmjeDP42gJcZJyRKsIenA=
 k8s.io/kubectl v0.18.8/go.mod h1:PlEgIAjOMua4hDFTEkVf+W5M0asHUKfE4y7VDZkpLHM=
 k8s.io/kubectl v0.18.8/go.mod h1:PlEgIAjOMua4hDFTEkVf+W5M0asHUKfE4y7VDZkpLHM=
 k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
 k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
 k8s.io/metrics v0.18.8/go.mod h1:j7JzZdiyhLP2BsJm/Fzjs+j5Lb1Y7TySjhPWqBPwRXA=
 k8s.io/metrics v0.18.8/go.mod h1:j7JzZdiyhLP2BsJm/Fzjs+j5Lb1Y7TySjhPWqBPwRXA=
 k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
 k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
-k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 k8s.io/utils v0.0.0-20200912215256-4140de9c8800 h1:9ZNvfPvVIEsp/T1ez4GQuzCcCTEQWhovSofhqR73A6g=
 k8s.io/utils v0.0.0-20200912215256-4140de9c8800 h1:9ZNvfPvVIEsp/T1ez4GQuzCcCTEQWhovSofhqR73A6g=
 k8s.io/utils v0.0.0-20200912215256-4140de9c8800/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 k8s.io/utils v0.0.0-20200912215256-4140de9c8800/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
@@ -1398,10 +1300,8 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT
 sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
 sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
 sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
 sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
-sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200207200219-5e70324e7c1c/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E=
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E=
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
-sigs.k8s.io/structured-merge-diff/v4 v4.0.1 h1:YXTMot5Qz/X1iBRJhAt+vI+HVttY0WkSqqhKxQ0xVbA=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
 sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
 sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
 sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
 sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=

+ 5 - 0
internal/adapter/gorm.go

@@ -5,11 +5,16 @@ import (
 
 
 	"github.com/porter-dev/porter/internal/config"
 	"github.com/porter-dev/porter/internal/config"
 	"gorm.io/driver/postgres"
 	"gorm.io/driver/postgres"
+	"gorm.io/driver/sqlite"
 	"gorm.io/gorm"
 	"gorm.io/gorm"
 )
 )
 
 
 // New returns a new gorm database instance
 // New returns a new gorm database instance
 func New(conf *config.DBConf) (*gorm.DB, error) {
 func New(conf *config.DBConf) (*gorm.DB, error) {
+	if conf.SQLLite {
+		return gorm.Open(sqlite.Open("./internal/porter.db"), &gorm.Config{})
+	}
+
 	dsn := fmt.Sprintf(
 	dsn := fmt.Sprintf(
 		"user=%s password=%s port=%d host=%s sslmode=disable",
 		"user=%s password=%s port=%d host=%s sslmode=disable",
 		conf.Username,
 		conf.Username,

+ 9 - 0
internal/auth/sessionstore.go

@@ -127,6 +127,15 @@ func NewStore(repo *repository.Repository, conf config.ServerConf) (*PGStore, er
 	return dbStore, nil
 	return dbStore, nil
 }
 }
 
 
+// NewFilesystemStore takes session key pairs to create a session-store in the local fs without using a db.
+func NewFilesystemStore(conf config.ServerConf) *sessions.FilesystemStore {
+
+	// Defaults to os.TempDir() when first argument (path) isn't specified.
+	store := sessions.NewFilesystemStore("", conf.CookieSecret)
+
+	return store
+}
+
 // Get Fetches a session for a given name after it has been added to the
 // Get Fetches a session for a given name after it has been added to the
 // registry.
 // registry.
 func (store *PGStore) Get(r *http.Request, name string) (*sessions.Session, error) {
 func (store *PGStore) Get(r *http.Request, name string) (*sessions.Session, error) {

+ 6 - 4
internal/config/config.go

@@ -12,7 +12,7 @@ type Conf struct {
 	Debug  bool `env:"DEBUG,default=false"`
 	Debug  bool `env:"DEBUG,default=false"`
 	Server ServerConf
 	Server ServerConf
 	Db     DBConf
 	Db     DBConf
-	Helm   HelmGlobalConf
+	K8s    K8sConf
 }
 }
 
 
 // ServerConf is the server configuration
 // ServerConf is the server configuration
@@ -33,11 +33,13 @@ type DBConf struct {
 	Username string `env:"DB_USER,default=porter"`
 	Username string `env:"DB_USER,default=porter"`
 	Password string `env:"DB_PASS,default=porter"`
 	Password string `env:"DB_PASS,default=porter"`
 	DbName   string `env:"DB_NAME,default=porter"`
 	DbName   string `env:"DB_NAME,default=porter"`
+
+	SQLLite bool `env:"QUICK_START,default=false"`
 }
 }
 
 
-// HelmGlobalConf is the global configuration for the Helm agent
-type HelmGlobalConf struct {
-	IsTesting bool `env:"HELM_IS_TESTING,default=false"`
+// K8sConf is the global configuration for the k8s agents
+type K8sConf struct {
+	IsTesting bool `env:"K8S_IS_TESTING,default=false"`
 }
 }
 
 
 // FromEnv generates a configuration from environment variables
 // FromEnv generates a configuration from environment variables

+ 21 - 9
internal/forms/chart.go

@@ -5,23 +5,35 @@ import (
 	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/repository"
 )
 )
 
 
-// ListChartForm represents the accepted values for listing Helm charts
-type ListChartForm struct {
-	HelmOptions *helm.Form       `json:"helm" form:"required"`
-	ListFilter  *helm.ListFilter `json:"filter" form:"required"`
-	UserID      uint             `json:"user_id"`
+// ChartForm is the generic base type for CRUD operations on charts
+type ChartForm struct {
+	HelmOptions *helm.Form `json:"helm" form:"required"`
+	UserID      uint       `json:"user_id"`
 }
 }
 
 
 // PopulateHelmOptions uses the passed user ID to populate the HelmOptions object
 // PopulateHelmOptions uses the passed user ID to populate the HelmOptions object
-func (lcf *ListChartForm) PopulateHelmOptions(repo repository.UserRepository) error {
-	user, err := repo.ReadUser(lcf.UserID)
+func (cf *ChartForm) PopulateHelmOptions(repo repository.UserRepository) error {
+	user, err := repo.ReadUser(cf.UserID)
 
 
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	lcf.HelmOptions.AllowedContexts = user.ContextToSlice()
+	cf.HelmOptions.AllowedContexts = user.ContextToSlice()
+	cf.HelmOptions.KubeConfig = user.RawKubeConfig
 
 
-	lcf.HelmOptions.KubeConfig = user.RawKubeConfig
 	return nil
 	return nil
 }
 }
+
+// ListChartForm represents the accepted values for listing Helm charts
+type ListChartForm struct {
+	ChartForm
+	ListFilter *helm.ListFilter `json:"filter" form:"required"`
+}
+
+// GetChartForm represents the accepted values for getting a single Helm chart
+type GetChartForm struct {
+	ChartForm
+	Name     string `json:"name" form:"required"`
+	Revision int    `json:"release"`
+}

+ 26 - 0
internal/forms/k8s.go

@@ -0,0 +1,26 @@
+package forms
+
+import (
+	"github.com/porter-dev/porter/internal/kubernetes"
+	"github.com/porter-dev/porter/internal/repository"
+)
+
+// K8sForm is the generic base type for CRUD operations on k8s objects
+type K8sForm struct {
+	K8sOptions *kubernetes.OutOfClusterConfig `json:"k8s" form:"required"`
+	UserID     uint                           `json:"user_id"`
+}
+
+// PopulateK8sOptions uses the passed user ID to populate the HelmOptions object
+func (kf *K8sForm) PopulateK8sOptions(repo repository.UserRepository) error {
+	user, err := repo.ReadUser(kf.UserID)
+
+	if err != nil {
+		return err
+	}
+
+	kf.K8sOptions.AllowedContexts = user.ContextToSlice()
+	kf.K8sOptions.KubeConfig = user.RawKubeConfig
+
+	return nil
+}

+ 1 - 1
internal/forms/user.go

@@ -65,7 +65,7 @@ type UpdateUserForm struct {
 	WriteUserForm
 	WriteUserForm
 	ID              uint     `form:"required"`
 	ID              uint     `form:"required"`
 	RawKubeConfig   string   `json:"rawKubeConfig,omitempty"`
 	RawKubeConfig   string   `json:"rawKubeConfig,omitempty"`
-	AllowedContexts []string `json:"allowedContexts,omitempty"`
+	AllowedContexts []string `json:"allowedContexts"`
 }
 }
 
 
 // ToUser converts an UpdateUserForm to models.User by parsing the kubeconfig
 // ToUser converts an UpdateUserForm to models.User by parsing the kubeconfig

+ 0 - 50
internal/helm/action_config.go

@@ -1,50 +0,0 @@
-package helm
-
-import (
-	"github.com/porter-dev/porter/internal/logger"
-
-	"helm.sh/helm/v3/pkg/action"
-	"helm.sh/helm/v3/pkg/kube"
-	"k8s.io/cli-runtime/pkg/genericclioptions"
-	"k8s.io/client-go/kubernetes"
-	"k8s.io/client-go/rest"
-)
-
-// NewActionConfig creates an action.Configuration, which can then be used to create Helm 3 actions.
-// Among other things, the action.Configuration controls which namespace the command is run against.
-func NewActionConfig(
-	l *logger.Logger,
-	newStorageDriver NewStorageDriver,
-	config *rest.Config,
-	clientset *kubernetes.Clientset,
-	namespace string,
-) (*action.Configuration, error) {
-	actionConfig := &action.Configuration{}
-	store := newStorageDriver(l, namespace, clientset)
-	restClientGetter := NewConfigFlagsFromCluster(namespace, config)
-	actionConfig.RESTClientGetter = restClientGetter
-	actionConfig.KubeClient = kube.New(restClientGetter)
-	actionConfig.Releases = store
-	actionConfig.Log = l.Printf
-	return actionConfig, nil
-}
-
-// NewConfigFlagsFromCluster returns ConfigFlags with default values set from within cluster.
-func NewConfigFlagsFromCluster(namespace string, clusterConfig *rest.Config) genericclioptions.RESTClientGetter {
-	impersonateGroup := []string{}
-
-	// CertFile and KeyFile must be nil for the BearerToken to be used for authentication and authorization instead of the pod's service account.
-	return &genericclioptions.ConfigFlags{
-		Insecure:         &clusterConfig.TLSClientConfig.Insecure,
-		Timeout:          stringptr("0"),
-		Namespace:        stringptr(namespace),
-		APIServer:        stringptr(clusterConfig.Host),
-		CAFile:           stringptr(clusterConfig.CAFile),
-		BearerToken:      stringptr(clusterConfig.BearerToken),
-		ImpersonateGroup: &impersonateGroup,
-	}
-}
-
-func stringptr(val string) *string {
-	return &val
-}

+ 3 - 80
internal/helm/agent.go

@@ -1,17 +1,8 @@
 package helm
 package helm
 
 
 import (
 import (
-	"io/ioutil"
-
-	"github.com/porter-dev/porter/internal/config"
-	"github.com/porter-dev/porter/internal/kubernetes"
-	"github.com/porter-dev/porter/internal/logger"
 	"helm.sh/helm/v3/pkg/action"
 	"helm.sh/helm/v3/pkg/action"
 	"helm.sh/helm/v3/pkg/release"
 	"helm.sh/helm/v3/pkg/release"
-	"helm.sh/helm/v3/pkg/storage"
-
-	"helm.sh/helm/v3/pkg/chartutil"
-	kubefake "helm.sh/helm/v3/pkg/kube/fake"
 )
 )
 
 
 // Agent is a Helm agent for performing helm operations
 // Agent is a Helm agent for performing helm operations
@@ -19,77 +10,6 @@ type Agent struct {
 	ActionConfig *action.Configuration
 	ActionConfig *action.Configuration
 }
 }
 
 
-// Form represents the options for connecting to a cluster and
-// creating a Helm agent
-type Form struct {
-	KubeConfig      []byte
-	AllowedContexts []string
-	Context         string `json:"context" form:"required"`
-	Storage         string `json:"storage" form:"oneof=secret configmap memory"`
-	Namespace       string `json:"namespace"`
-}
-
-// ToAgent uses the Form to generate an agent. Setting testing=true will create
-// a test agent with in-memory storage
-func (h *Form) ToAgent(
-	l *logger.Logger,
-	helmConf *config.HelmGlobalConf,
-	storage *storage.Storage,
-) (*Agent, error) {
-	if helmConf.IsTesting {
-		testStorage := storage
-
-		if testStorage == nil {
-			testStorage = StorageMap["memory"](nil, h.Namespace, nil)
-		}
-
-		return &Agent{&action.Configuration{
-			Releases: testStorage,
-			KubeClient: &kubefake.FailingKubeClient{
-				PrintingKubeClient: kubefake.PrintingKubeClient{
-					Out: ioutil.Discard,
-				},
-			},
-			Capabilities: chartutil.DefaultCapabilities,
-			Log:          l.Printf,
-		}}, nil
-	}
-
-	// create a client config using the app's helm/kubernetes agents
-	conf, err := kubernetes.GetRestrictedClientConfigFromBytes(
-		h.KubeConfig,
-		h.Context,
-		h.AllowedContexts,
-	)
-
-	if err != nil {
-		return nil, err
-	}
-
-	restConf, err := conf.ClientConfig()
-
-	clientset, err := kubernetes.GetClientsetFromConfig(conf)
-
-	if err != nil {
-		return nil, err
-	}
-
-	// create a new agent
-	actionConfig, err := NewActionConfig(
-		l,
-		StorageMap[h.Storage],
-		restConf,
-		clientset,
-		h.Namespace,
-	)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return &Agent{actionConfig}, nil
-}
-
 // ListReleases lists releases based on a ListFilter
 // ListReleases lists releases based on a ListFilter
 func (a *Agent) ListReleases(
 func (a *Agent) ListReleases(
 	namespace string,
 	namespace string,
@@ -105,9 +25,12 @@ func (a *Agent) ListReleases(
 // GetRelease returns the info of a release.
 // GetRelease returns the info of a release.
 func (a *Agent) GetRelease(
 func (a *Agent) GetRelease(
 	name string,
 	name string,
+	version int,
 ) (*release.Release, error) {
 ) (*release.Release, error) {
 	// Namespace is already known by the RESTClientGetter.
 	// Namespace is already known by the RESTClientGetter.
 	cmd := action.NewGet(a.ActionConfig)
 	cmd := action.NewGet(a.ActionConfig)
 
 
+	cmd.Version = version
+
 	return cmd.Run(name)
 	return cmd.Run(name)
 }
 }

+ 46 - 7
internal/helm/agent_test.go

@@ -5,7 +5,6 @@ import (
 
 
 	"helm.sh/helm/v3/pkg/storage/driver"
 	"helm.sh/helm/v3/pkg/storage/driver"
 
 
-	"github.com/porter-dev/porter/internal/config"
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/logger"
 	"github.com/porter-dev/porter/internal/logger"
 
 
@@ -17,15 +16,12 @@ func newAgentFixture(t *testing.T, namespace string) *helm.Agent {
 	t.Helper()
 	t.Helper()
 
 
 	l := logger.NewConsole(true)
 	l := logger.NewConsole(true)
-	opts := &helm.Form{
+
+	form := &helm.Form{
 		Namespace: namespace,
 		Namespace: namespace,
 	}
 	}
 
 
-	agent, _ := opts.ToAgent(l, &config.HelmGlobalConf{
-		IsTesting: true,
-	}, nil)
-
-	return agent
+	return helm.GetAgentTesting(form, nil, l)
 }
 }
 
 
 type releaseStub struct {
 type releaseStub struct {
@@ -173,3 +169,46 @@ func TestListReleases(t *testing.T) {
 		compareReleaseToStubs(t, releases, tc.expRes)
 		compareReleaseToStubs(t, releases, tc.expRes)
 	}
 	}
 }
 }
+
+type getReleaseTest struct {
+	name       string
+	namespace  string
+	releases   []releaseStub
+	getName    string
+	getVersion int
+	expRes     releaseStub
+}
+
+var getReleaseTests = []getReleaseTest{
+	getReleaseTest{
+		name:      "simple get with revision 0 (latest)",
+		namespace: "default",
+		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},
+		},
+		getName:    "airwatch",
+		getVersion: 0,
+		expRes:     releaseStub{"airwatch", "default", 1, "1.0.0", release.StatusDeployed},
+	},
+}
+
+func TestGetReleases(t *testing.T) {
+	for _, tc := range getReleaseTests {
+		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)
+
+		rel, err := agent.GetRelease(tc.getName, tc.getVersion)
+
+		if err != nil {
+			t.Errorf("%v", err)
+		}
+
+		compareReleaseToStubs(t, []*release.Release{rel}, []releaseStub{tc.expRes})
+	}
+}

+ 101 - 0
internal/helm/config.go

@@ -0,0 +1,101 @@
+package helm
+
+import (
+	"errors"
+	"io/ioutil"
+
+	"github.com/porter-dev/porter/internal/kubernetes"
+	"github.com/porter-dev/porter/internal/logger"
+	"helm.sh/helm/v3/pkg/action"
+	"helm.sh/helm/v3/pkg/chartutil"
+	"helm.sh/helm/v3/pkg/kube"
+	kubefake "helm.sh/helm/v3/pkg/kube/fake"
+	"helm.sh/helm/v3/pkg/storage"
+	k8s "k8s.io/client-go/kubernetes"
+)
+
+// Form represents the options for connecting to a cluster and
+// creating a Helm agent
+type Form struct {
+	KubeConfig      []byte
+	AllowedContexts []string
+	Context         string `json:"context" form:"required"`
+	Storage         string `json:"storage" form:"oneof=secret configmap memory"`
+	Namespace       string `json:"namespace"`
+}
+
+// GetAgentOutOfClusterConfig creates a new Agent from outside the cluster using
+// the underlying kubernetes.GetAgentOutOfClusterConfig method
+func GetAgentOutOfClusterConfig(form *Form, l *logger.Logger) (*Agent, error) {
+	// create a kubernetes agent
+	conf := &kubernetes.OutOfClusterConfig{
+		KubeConfig:      form.KubeConfig,
+		AllowedContexts: form.AllowedContexts,
+		Context:         form.Context,
+	}
+
+	k8sAgent, err := kubernetes.GetAgentOutOfClusterConfig(conf)
+
+	if err != nil {
+		return nil, err
+	}
+
+	clientset, ok := k8sAgent.Clientset.(*k8s.Clientset)
+
+	if !ok {
+		return nil, errors.New("Agent Clientset was not of type *(k8s.io/client-go/kubernetes).Clientset")
+	}
+
+	// use k8s agent to create Helm agent
+	return &Agent{&action.Configuration{
+		RESTClientGetter: k8sAgent.RESTClientGetter,
+		KubeClient:       kube.New(k8sAgent.RESTClientGetter),
+		Releases:         StorageMap[form.Storage](l, form.Namespace, clientset),
+		Log:              l.Printf,
+	}}, nil
+}
+
+// GetAgentInClusterConfig creates a new Agent from inside the cluster using
+// the underlying kubernetes.GetAgentInClusterConfig method
+func GetAgentInClusterConfig(form *Form, l *logger.Logger) (*Agent, error) {
+	// create a kubernetes agent
+	k8sAgent, err := kubernetes.GetAgentInClusterConfig()
+
+	if err != nil {
+		return nil, err
+	}
+
+	clientset, ok := k8sAgent.Clientset.(*k8s.Clientset)
+
+	if !ok {
+		return nil, errors.New("Agent Clientset was not of type *(k8s.io/client-go/kubernetes).Clientset")
+	}
+
+	// use k8s agent to create Helm agent
+	return &Agent{&action.Configuration{
+		RESTClientGetter: k8sAgent.RESTClientGetter,
+		KubeClient:       kube.New(k8sAgent.RESTClientGetter),
+		Releases:         StorageMap[form.Storage](l, form.Namespace, clientset),
+		Log:              l.Printf,
+	}}, nil
+}
+
+// GetAgentTesting creates a new Agent using an optional existing storage class
+func GetAgentTesting(form *Form, storage *storage.Storage, l *logger.Logger) *Agent {
+	testStorage := storage
+
+	if testStorage == nil {
+		testStorage = StorageMap["memory"](nil, form.Namespace, nil)
+	}
+
+	return &Agent{&action.Configuration{
+		Releases: testStorage,
+		KubeClient: &kubefake.FailingKubeClient{
+			PrintingKubeClient: kubefake.PrintingKubeClient{
+				Out: ioutil.Discard,
+			},
+		},
+		Capabilities: chartutil.DefaultCapabilities,
+		Log:          l.Printf,
+	}}
+}

+ 4 - 2
internal/helm/driver.go → internal/helm/storage.go

@@ -8,8 +8,10 @@ package helm
 // - memory
 // - memory
 // - postgres
 // - postgres
 //
 //
-// This file implements first-class support for each driver type, and integrates with the
-// logger.
+// This file implements first-class support for the first three driver types,
+// and integrates with the logger.
+//
+// TODO -- include support for SQL storage...
 
 
 import (
 import (
 	"github.com/porter-dev/porter/internal/logger"
 	"github.com/porter-dev/porter/internal/logger"

+ 12 - 6
internal/kubernetes/agent.go

@@ -4,16 +4,22 @@ import (
 	"context"
 	"context"
 
 
 	v1 "k8s.io/api/core/v1"
 	v1 "k8s.io/api/core/v1"
-	v1Machinery "k8s.io/apimachinery/pkg/apis/meta/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/kubernetes"
 )
 )
 
 
+// Agent is a Kubernetes agent for performing operations that interact with the
+// api server
+type Agent struct {
+	RESTClientGetter genericclioptions.RESTClientGetter
+	Clientset        kubernetes.Interface
+}
+
 // ListNamespaces simply lists namespaces
 // ListNamespaces simply lists namespaces
-func ListNamespaces(clientset *kubernetes.Clientset) *v1.NamespaceList {
-	namespaces, _ := clientset.CoreV1().Namespaces().List(
+func (a *Agent) ListNamespaces() (*v1.NamespaceList, error) {
+	return a.Clientset.CoreV1().Namespaces().List(
 		context.TODO(),
 		context.TODO(),
-		v1Machinery.ListOptions{},
+		metav1.ListOptions{},
 	)
 	)
-
-	return namespaces
 }
 }

+ 65 - 0
internal/kubernetes/agent_test.go

@@ -0,0 +1,65 @@
+package kubernetes_test
+
+import (
+	"testing"
+
+	"github.com/porter-dev/porter/internal/kubernetes"
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/discovery"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/clientcmd"
+
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+type fakeRESTClientGetter struct{}
+
+func (f *fakeRESTClientGetter) ToRESTConfig() (*rest.Config, error) {
+	return nil, nil
+}
+
+func (f *fakeRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
+	return nil
+}
+
+func (f *fakeRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
+	return nil, nil
+}
+
+func (f *fakeRESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
+	return nil, nil
+}
+
+func newAgentFixture(t *testing.T, objects ...runtime.Object) *kubernetes.Agent {
+	t.Helper()
+
+	return kubernetes.GetAgentTesting(objects...)
+}
+
+func TestOutOfClusterConfig(t *testing.T) {
+	k8sAgent := newAgentFixture(t, &v1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "test-namespace-0",
+		},
+	}, &v1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "test-namespace-1",
+		},
+	})
+
+	namespaces, err := k8sAgent.ListNamespaces()
+
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	names := []string{"test-namespace-0", "test-namespace-1"}
+
+	for i, ns := range namespaces.Items {
+		if names[i] != ns.Name {
+			t.Errorf("Namespace names do not match: expected %s, got %s\n", names[i], ns.Name)
+		}
+	}
+}

+ 0 - 18
internal/kubernetes/client.go

@@ -1,18 +0,0 @@
-package kubernetes
-
-import (
-	"k8s.io/client-go/kubernetes"
-	"k8s.io/client-go/tools/clientcmd"
-)
-
-// GetClientsetFromConfig is a simple wrapper that returns a *kubernetes.Clientset based on
-// a clientcmd.ClientConfig
-func GetClientsetFromConfig(conf clientcmd.ClientConfig) (*kubernetes.Clientset, error) {
-	clientConf, err := conf.ClientConfig()
-
-	if err != nil {
-		return nil, err
-	}
-
-	return kubernetes.NewForConfig(clientConf)
-}

+ 170 - 0
internal/kubernetes/config.go

@@ -0,0 +1,170 @@
+package kubernetes
+
+import (
+	"path/filepath"
+	"regexp"
+	"strings"
+	"time"
+
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/cli-runtime/pkg/genericclioptions"
+	"k8s.io/client-go/discovery"
+	diskcached "k8s.io/client-go/discovery/cached/disk"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/kubernetes/fake"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/restmapper"
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/util/homedir"
+)
+
+// GetAgentOutOfClusterConfig creates a new Agent using the OutOfClusterConfig
+func GetAgentOutOfClusterConfig(conf *OutOfClusterConfig) (*Agent, error) {
+	restConf, err := conf.ToRESTConfig()
+
+	if err != nil {
+		return nil, err
+	}
+
+	clientset, err := kubernetes.NewForConfig(restConf)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return &Agent{conf, clientset}, nil
+}
+
+// GetAgentInClusterConfig uses the service account that kubernetes
+// gives to pods to connect
+func GetAgentInClusterConfig() (*Agent, error) {
+	conf, err := rest.InClusterConfig()
+
+	if err != nil {
+		return nil, err
+	}
+
+	restClientGetter := newRESTClientGetterFromInClusterConfig(conf)
+	clientset, err := kubernetes.NewForConfig(conf)
+
+	return &Agent{restClientGetter, clientset}, nil
+}
+
+// GetAgentTesting creates a new Agent using an optional existing storage class
+func GetAgentTesting(objects ...runtime.Object) *Agent {
+	return &Agent{&fakeRESTClientGetter{}, fake.NewSimpleClientset(objects...)}
+}
+
+// OutOfClusterConfig is the set of parameters required for an out-of-cluster connection.
+// This implements RESTClientGetter
+type OutOfClusterConfig struct {
+	KubeConfig      []byte
+	AllowedContexts []string
+	Context         string `json:"context" form:"required"`
+}
+
+// ToRESTConfig creates a kubernetes REST client factory -- it simply calls ClientConfig on
+// the result of ToRawKubeConfigLoader
+func (conf *OutOfClusterConfig) ToRESTConfig() (*rest.Config, error) {
+	restConf, err := conf.ToRawKubeConfigLoader().ClientConfig()
+
+	if err != nil {
+		return nil, err
+	}
+
+	rest.SetKubernetesDefaults(restConf)
+	return restConf, nil
+}
+
+// ToRawKubeConfigLoader creates a clientcmd.ClientConfig from the raw kubeconfig found in
+// the OutOfClusterConfig. It does not implement loading rules or overrides.
+func (conf *OutOfClusterConfig) ToRawKubeConfigLoader() clientcmd.ClientConfig {
+	cmdConf, _ := GetRestrictedClientConfigFromBytes(
+		conf.KubeConfig,
+		conf.Context,
+		conf.AllowedContexts,
+	)
+
+	return cmdConf
+}
+
+// ToDiscoveryClient returns a CachedDiscoveryInterface using a computed RESTConfig
+// It's required to implement the interface genericclioptions.RESTClientGetter
+func (conf *OutOfClusterConfig) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
+	// From: k8s.io/cli-runtime/pkg/genericclioptions/config_flags.go > func (*configFlags) ToDiscoveryClient()
+	restConf, err := conf.ToRESTConfig()
+
+	if err != nil {
+		return nil, err
+	}
+
+	restConf.Burst = 100
+	defaultHTTPCacheDir := filepath.Join(homedir.HomeDir(), ".kube", "http-cache")
+
+	// takes the parentDir and the host and comes up with a "usually non-colliding" name for the discoveryCacheDir
+	parentDir := filepath.Join(homedir.HomeDir(), ".kube", "cache", "discovery")
+	// strip the optional scheme from host if its there:
+	schemelessHost := strings.Replace(strings.Replace(restConf.Host, "https://", "", 1), "http://", "", 1)
+	// now do a simple collapse of non-AZ09 characters.  Collisions are possible but unlikely.  Even if we do collide the problem is short lived
+	safeHost := regexp.MustCompile(`[^(\w/\.)]`).ReplaceAllString(schemelessHost, "_")
+	discoveryCacheDir := filepath.Join(parentDir, safeHost)
+
+	return diskcached.NewCachedDiscoveryClientForConfig(restConf, discoveryCacheDir, defaultHTTPCacheDir, time.Duration(10*time.Minute))
+}
+
+// ToRESTMapper returns a mapper
+func (conf *OutOfClusterConfig) ToRESTMapper() (meta.RESTMapper, error) {
+	// From: k8s.io/cli-runtime/pkg/genericclioptions/config_flags.go > func (*configFlags) ToRESTMapper()
+	discoveryClient, err := conf.ToDiscoveryClient()
+	if err != nil {
+		return nil, err
+	}
+
+	mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
+	expander := restmapper.NewShortcutExpander(mapper, discoveryClient)
+	return expander, nil
+}
+
+// newRESTClientGetterFromInClusterConfig returns a RESTClientGetter using
+// default values set from the *rest.Config
+func newRESTClientGetterFromInClusterConfig(conf *rest.Config) genericclioptions.RESTClientGetter {
+	cfs := genericclioptions.NewConfigFlags(false)
+
+	cfs.ClusterName = &conf.ServerName
+	cfs.Insecure = &conf.Insecure
+	cfs.APIServer = &conf.Host
+	cfs.CAFile = &conf.CAFile
+	cfs.KeyFile = &conf.KeyFile
+	cfs.CertFile = &conf.CertFile
+	cfs.BearerToken = &conf.BearerToken
+	cfs.Timeout = stringptr(conf.Timeout.String())
+	cfs.Impersonate = &conf.Impersonate.UserName
+	cfs.ImpersonateGroup = &conf.Impersonate.Groups
+	cfs.Username = &conf.Username
+	cfs.Password = &conf.Password
+
+	return cfs
+}
+
+func stringptr(val string) *string {
+	return &val
+}
+
+type fakeRESTClientGetter struct{}
+
+func (f *fakeRESTClientGetter) ToRESTConfig() (*rest.Config, error) {
+	return nil, nil
+}
+
+func (f *fakeRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
+	return nil
+}
+
+func (f *fakeRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
+	return nil, nil
+}
+
+func (f *fakeRESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
+	return nil, nil
+}

+ 31 - 11
server/api/api.go

@@ -4,26 +4,33 @@ import (
 	"github.com/go-playground/locales/en"
 	"github.com/go-playground/locales/en"
 	ut "github.com/go-playground/universal-translator"
 	ut "github.com/go-playground/universal-translator"
 	"github.com/go-playground/validator/v10"
 	"github.com/go-playground/validator/v10"
-	sessionstore "github.com/porter-dev/porter/internal/auth"
-	"github.com/porter-dev/porter/internal/config"
+
+	"github.com/gorilla/sessions"
+	"github.com/porter-dev/porter/internal/helm"
+	"github.com/porter-dev/porter/internal/kubernetes"
 	lr "github.com/porter-dev/porter/internal/logger"
 	lr "github.com/porter-dev/porter/internal/logger"
 	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/repository"
 	"helm.sh/helm/v3/pkg/storage"
 	"helm.sh/helm/v3/pkg/storage"
 )
 )
 
 
+// TestAgents are the k8s agents used for testing
+type TestAgents struct {
+	HelmAgent             *helm.Agent
+	HelmTestStorageDriver *storage.Storage
+	K8sAgent              *kubernetes.Agent
+}
+
 // App represents an API instance with handler methods attached, a DB connection
 // App represents an API instance with handler methods attached, a DB connection
 // and a logger instance
 // and a logger instance
 type App struct {
 type App struct {
 	logger     *lr.Logger
 	logger     *lr.Logger
 	repo       *repository.Repository
 	repo       *repository.Repository
 	validator  *validator.Validate
 	validator  *validator.Validate
-	store      *sessionstore.PGStore
+	store      sessions.Store
 	translator *ut.Translator
 	translator *ut.Translator
-	helmConf   *config.HelmGlobalConf
-	// HelmTestStorageDriver is used by testing libraries to query the in-memory
-	// Helm storage driver
-	HelmTestStorageDriver *storage.Storage
-	cookieName            string
+	cookieName string
+	testing    bool
+	TestAgents *TestAgents
 }
 }
 
 
 // New returns a new App instance
 // New returns a new App instance
@@ -31,9 +38,9 @@ func New(
 	logger *lr.Logger,
 	logger *lr.Logger,
 	repo *repository.Repository,
 	repo *repository.Repository,
 	validator *validator.Validate,
 	validator *validator.Validate,
-	store *sessionstore.PGStore,
-	helmConf *config.HelmGlobalConf,
+	store sessions.Store,
 	cookieName string,
 	cookieName string,
+	testing bool,
 ) *App {
 ) *App {
 	// for now, will just support the english translator from the
 	// for now, will just support the english translator from the
 	// validator/translations package
 	// validator/translations package
@@ -41,14 +48,27 @@ func New(
 	uni := ut.New(en, en)
 	uni := ut.New(en, en)
 	trans, _ := uni.GetTranslator("en")
 	trans, _ := uni.GetTranslator("en")
 
 
+	var testAgents *TestAgents = nil
+
+	if testing {
+		memStorage := helm.StorageMap["memory"](nil, "", nil)
+
+		testAgents = &TestAgents{
+			HelmAgent:             helm.GetAgentTesting(&helm.Form{}, nil, logger),
+			HelmTestStorageDriver: memStorage,
+			K8sAgent:              kubernetes.GetAgentTesting(),
+		}
+	}
+
 	return &App{
 	return &App{
 		logger:     logger,
 		logger:     logger,
 		repo:       repo,
 		repo:       repo,
 		validator:  validator,
 		validator:  validator,
 		store:      store,
 		store:      store,
 		translator: &trans,
 		translator: &trans,
-		helmConf:   helmConf,
 		cookieName: cookieName,
 		cookieName: cookieName,
+		testing:    testing,
+		TestAgents: testAgents,
 	}
 	}
 }
 }
 
 

+ 64 - 1
server/api/chart_handler.go

@@ -3,8 +3,11 @@ package api
 import (
 import (
 	"encoding/json"
 	"encoding/json"
 	"net/http"
 	"net/http"
+	"strconv"
 
 
+	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/forms"
+	"github.com/porter-dev/porter/internal/helm"
 )
 )
 
 
 // Enumeration of chart API error codes, represented as int64
 // Enumeration of chart API error codes, represented as int64
@@ -33,7 +36,14 @@ func (app *App) HandleListCharts(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 
 
 	// create a new agent
 	// create a new agent
-	agent, err := form.HelmOptions.ToAgent(app.logger, app.helmConf, app.HelmTestStorageDriver)
+	var agent *helm.Agent
+	var err error
+
+	if app.testing {
+		agent = app.TestAgents.HelmAgent
+	} else {
+		agent, err = helm.GetAgentOutOfClusterConfig(form.HelmOptions, app.logger)
+	}
 
 
 	releases, err := agent.ListReleases(form.HelmOptions.Namespace, form.ListFilter)
 	releases, err := agent.ListReleases(form.HelmOptions.Namespace, form.ListFilter)
 
 
@@ -47,3 +57,56 @@ func (app *App) HandleListCharts(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 }
 }
+
+// HandleGetChart retrieves a single chart based on a name and revision
+func (app *App) HandleGetChart(w http.ResponseWriter, r *http.Request) {
+	name := chi.URLParam(r, "name")
+	revision, err := strconv.ParseUint(chi.URLParam(r, "revision"), 0, 64)
+
+	// decode from JSON to form value
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrChartDecode, w)
+		return
+	}
+
+	// get the filter options
+	form := &forms.GetChartForm{
+		Name:     name,
+		Revision: int(revision),
+	}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrChartDecode, w)
+		return
+	}
+
+	form.PopulateHelmOptions(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.HelmOptions, app.logger)
+	}
+
+	release, err := agent.GetRelease(form.Name, form.Revision)
+
+	if err != nil {
+		app.handleErrorFormValidation(err, ErrChartValidateFields, w)
+		return
+	}
+
+	if err := json.NewEncoder(w).Encode(release); err != nil {
+		app.handleErrorFormDecoding(err, ErrChartDecode, w)
+		return
+	}
+}

+ 39 - 41
server/api/chart_handler_test.go

@@ -7,13 +7,10 @@ import (
 	"strings"
 	"strings"
 	"testing"
 	"testing"
 
 
-	"github.com/porter-dev/porter/internal/config"
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/helm"
-	"github.com/porter-dev/porter/internal/logger"
 
 
 	"helm.sh/helm/v3/pkg/chart"
 	"helm.sh/helm/v3/pkg/chart"
 	"helm.sh/helm/v3/pkg/release"
 	"helm.sh/helm/v3/pkg/release"
-	"helm.sh/helm/v3/pkg/storage"
 	"helm.sh/helm/v3/pkg/storage/driver"
 	"helm.sh/helm/v3/pkg/storage/driver"
 )
 )
 
 
@@ -25,28 +22,6 @@ type releaseStub struct {
 	status       release.Status
 	status       release.Status
 }
 }
 
 
-// type ListFilter struct {
-// 	Namespace    string   `json:"namespace"`
-// 	Limit        int      `json:"limit"`
-// 	Skip         int      `json:"skip"`
-// 	ByDate       bool     `json:"byDate"`
-// 	StatusFilter []string `json:"statusFilter"`
-// }
-
-// type Form struct {
-// 	KubeConfig      []byte   `form:"required"`
-// 	AllowedContexts []string `form:"required"`
-// 	Context         string   `json:"context" form:"required"`
-// 	Storage         string   `json:"storage" form:"oneof=secret configmap memory"`
-// 	Namespace       string   `json:"namespace"`
-// }
-
-// type ListChartForm struct {
-// 	HelmOptions *helm.Form       `json:"helm" form:"required"`
-// 	ListFilter  *helm.ListFilter `json:"filter" form:"required"`
-// 	UserID      uint             `json:"user_id"`
-// }
-
 // ------------------------- TEST TYPES AND MAIN LOOP ------------------------- //
 // ------------------------- TEST TYPES AND MAIN LOOP ------------------------- //
 
 
 type chartTest struct {
 type chartTest struct {
@@ -64,8 +39,7 @@ type chartTest struct {
 func testChartRequests(t *testing.T, tests []*chartTest, canQuery bool) {
 func testChartRequests(t *testing.T, tests []*chartTest, canQuery bool) {
 	for _, c := range tests {
 	for _, c := range tests {
 		// create a new tester
 		// create a new tester
-		storage := helm.StorageMap["memory"](nil, "", nil)
-		tester := newTester(canQuery, storage)
+		tester := newTester(canQuery)
 
 
 		// if there's an initializer, call it
 		// if there's an initializer, call it
 		for _, init := range c.initializers {
 		for _, init := range c.initializers {
@@ -142,12 +116,41 @@ func TestHandleListCharts(t *testing.T) {
 	testChartRequests(t, listChartsTests, true)
 	testChartRequests(t, listChartsTests, true)
 }
 }
 
 
+var getChartTests = []*chartTest{
+	&chartTest{
+		initializers: []func(tester *tester){
+			initDefaultCharts,
+		},
+		msg:      "Get charts",
+		method:   "GET",
+		endpoint: "/api/charts/airwatch/0",
+		body: `{
+			"user_id": 1,
+			"helm": {
+				"namespace": "",
+				"context": "context-test",
+				"storage": "memory"
+			}
+		}`,
+		expStatus: http.StatusOK,
+		expBody:   releaseStubToChartJSON(sampleReleaseStubs[0]),
+		useCookie: true,
+		validators: []func(c *chartTest, tester *tester, t *testing.T){
+			chartReleaseBodyValidator,
+		},
+	},
+}
+
+func TestHandleGetChart(t *testing.T) {
+	testChartRequests(t, getChartTests, true)
+}
+
 // ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
 // ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
 
 
 func initDefaultCharts(tester *tester) {
 func initDefaultCharts(tester *tester) {
 	initUserDefault(tester)
 	initUserDefault(tester)
 
 
-	agent := newAgentFixture("default", tester.app.HelmTestStorageDriver)
+	agent := tester.app.TestAgents.HelmAgent
 
 
 	makeReleases(agent, sampleReleaseStubs)
 	makeReleases(agent, sampleReleaseStubs)
 
 
@@ -156,19 +159,6 @@ func initDefaultCharts(tester *tester) {
 	agent.ActionConfig.Releases.Driver.(*driver.Memory).SetNamespace("")
 	agent.ActionConfig.Releases.Driver.(*driver.Memory).SetNamespace("")
 }
 }
 
 
-func newAgentFixture(namespace string, storage *storage.Storage) *helm.Agent {
-	l := logger.NewConsole(true)
-	opts := &helm.Form{
-		Namespace: namespace,
-	}
-
-	agent, _ := opts.ToAgent(l, &config.HelmGlobalConf{
-		IsTesting: true,
-	}, storage)
-
-	return agent
-}
-
 var sampleReleaseStubs = []releaseStub{
 var sampleReleaseStubs = []releaseStub{
 	releaseStub{"airwatch", "default", 1, "1.0.0", release.StatusDeployed},
 	releaseStub{"airwatch", "default", 1, "1.0.0", release.StatusDeployed},
 	releaseStub{"not-in-default-namespace", "other", 1, "1.0.1", release.StatusDeployed},
 	releaseStub{"not-in-default-namespace", "other", 1, "1.0.1", release.StatusDeployed},
@@ -189,6 +179,14 @@ func releaseStubsToChartJSON(rels []releaseStub) string {
 	return string(str)
 	return string(str)
 }
 }
 
 
+func releaseStubToChartJSON(r releaseStub) string {
+	rel := releaseStubToRelease(r)
+
+	str, _ := json.Marshal(rel)
+
+	return string(str)
+}
+
 func releaseStubToRelease(r releaseStub) *release.Release {
 func releaseStubToRelease(r releaseStub) *release.Release {
 	return &release.Release{
 	return &release.Release{
 		Name:      r.name,
 		Name:      r.name,

+ 3 - 5
server/api/helpers_test.go

@@ -13,7 +13,6 @@ import (
 	"github.com/porter-dev/porter/internal/repository/test"
 	"github.com/porter-dev/porter/internal/repository/test"
 	"github.com/porter-dev/porter/server/api"
 	"github.com/porter-dev/porter/server/api"
 	"github.com/porter-dev/porter/server/router"
 	"github.com/porter-dev/porter/server/router"
-	"helm.sh/helm/v3/pkg/storage"
 
 
 	sessionstore "github.com/porter-dev/porter/internal/auth"
 	sessionstore "github.com/porter-dev/porter/internal/auth"
 	vr "github.com/porter-dev/porter/internal/validator"
 	vr "github.com/porter-dev/porter/internal/validator"
@@ -55,7 +54,7 @@ func (t *tester) createUserSession(email string, pw string) {
 	t.reset()
 	t.reset()
 }
 }
 
 
-func newTester(canQuery bool, storage *storage.Storage) *tester {
+func newTester(canQuery bool) *tester {
 	appConf := config.Conf{
 	appConf := config.Conf{
 		Debug: true,
 		Debug: true,
 		Server: config.ServerConf{
 		Server: config.ServerConf{
@@ -69,7 +68,7 @@ func newTester(canQuery bool, storage *storage.Storage) *tester {
 		// unimportant here
 		// unimportant here
 		Db: config.DBConf{},
 		Db: config.DBConf{},
 		// set the helm config to testing
 		// set the helm config to testing
-		Helm: config.HelmGlobalConf{
+		K8s: config.K8sConf{
 			IsTesting: true,
 			IsTesting: true,
 		},
 		},
 	}
 	}
@@ -80,8 +79,7 @@ func newTester(canQuery bool, storage *storage.Storage) *tester {
 	repo := test.NewRepository(canQuery)
 	repo := test.NewRepository(canQuery)
 
 
 	store, _ := sessionstore.NewStore(repo, appConf.Server)
 	store, _ := sessionstore.NewStore(repo, appConf.Server)
-	app := api.New(logger, repo, validator, store, &appConf.Helm, appConf.Server.CookieName)
-	app.HelmTestStorageDriver = storage
+	app := api.New(logger, repo, validator, store, appConf.Server.CookieName, true)
 	r := router.New(app, store, appConf.Server.CookieName)
 	r := router.New(app, store, appConf.Server.CookieName)
 
 
 	return &tester{
 	return &tester{

+ 57 - 0
server/api/k8s_handler.go

@@ -0,0 +1,57 @@
+package api
+
+import (
+	"encoding/json"
+	"net/http"
+
+	"github.com/porter-dev/porter/internal/kubernetes"
+
+	"github.com/porter-dev/porter/internal/forms"
+)
+
+// Enumeration of k8s API error codes, represented as int64
+const (
+	ErrK8sDecode ErrorCode = iota + 600
+	ErrK8sValidate
+)
+
+// HandleListNamespaces retrieves a list of namespaces
+func (app *App) HandleListNamespaces(w http.ResponseWriter, r *http.Request) {
+	form := &forms.K8sForm{}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrK8sDecode, w)
+		return
+	}
+
+	form.PopulateK8sOptions(app.repo.User)
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrK8sValidate, w)
+		return
+	}
+
+	// create a new agent
+	var agent *kubernetes.Agent
+	var err error
+
+	if app.testing {
+		agent = app.TestAgents.K8sAgent
+	} else {
+		agent, err = kubernetes.GetAgentOutOfClusterConfig(form.K8sOptions)
+	}
+
+	namespaces, err := agent.ListNamespaces()
+
+	if err != nil {
+		app.handleErrorFormValidation(err, ErrK8sValidate, w)
+		return
+	}
+
+	if err := json.NewEncoder(w).Encode(namespaces); err != nil {
+		app.handleErrorFormDecoding(err, ErrK8sDecode, w)
+		return
+	}
+}

+ 143 - 0
server/api/k8s_handler_test.go

@@ -0,0 +1,143 @@
+package api_test
+
+import (
+	"encoding/json"
+	"net/http"
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/porter-dev/porter/internal/kubernetes"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+// ------------------------- TEST TYPES AND MAIN LOOP ------------------------- //
+
+type k8sTest struct {
+	initializers []func(tester *tester)
+	msg          string
+	method       string
+	endpoint     string
+	body         string
+	expStatus    int
+	expBody      string
+	useCookie    bool
+	validators   []func(c *k8sTest, tester *tester, t *testing.T)
+}
+
+func testK8sRequests(t *testing.T, tests []*k8sTest, canQuery bool) {
+	for _, c := range tests {
+		// create a new tester
+		tester := newTester(canQuery)
+
+		// if there's an initializer, call it
+		for _, init := range c.initializers {
+			init(tester)
+		}
+
+		req, err := http.NewRequest(
+			c.method,
+			c.endpoint,
+			strings.NewReader(c.body),
+		)
+
+		tester.req = req
+
+		if c.useCookie {
+			req.AddCookie(tester.cookie)
+		}
+
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		tester.execute()
+		rr := tester.rr
+
+		// first, check that the status matches
+		if status := rr.Code; status != c.expStatus {
+			t.Errorf("%s, handler returned wrong status code: got %v want %v",
+				c.msg, status, c.expStatus)
+		}
+
+		// if there's a validator, call it
+		for _, validate := range c.validators {
+			validate(c, tester, t)
+		}
+	}
+}
+
+// ------------------------- TEST FIXTURES AND FUNCTIONS  ------------------------- //
+
+var listNamespacesTests = []*k8sTest{
+	&k8sTest{
+		initializers: []func(tester *tester){
+			initDefaultK8s,
+		},
+		msg:      "List namespaces",
+		method:   "GET",
+		endpoint: "/api/k8s/namespaces",
+		body: `{
+			"user_id": 1,
+			"k8s": {
+				"namespace": "",
+				"context": "context-test"
+			}
+		}`,
+		expStatus: http.StatusOK,
+		expBody:   objectsToJSON(defaultObjects),
+		useCookie: true,
+		validators: []func(c *k8sTest, tester *tester, t *testing.T){
+			k8sNamespaceListValidator,
+		},
+	},
+}
+
+func TestHandleListNamespaces(t *testing.T) {
+	testK8sRequests(t, listNamespacesTests, true)
+}
+
+// ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
+
+var defaultObjects = []runtime.Object{
+	&v1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "test-namespace-0",
+		},
+	},
+	&v1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "test-namespace-1",
+		},
+	},
+}
+
+func initDefaultK8s(tester *tester) {
+	initUserDefault(tester)
+
+	agent := kubernetes.GetAgentTesting(defaultObjects...)
+
+	// overwrite the test agent with new resources
+	tester.app.TestAgents.K8sAgent = agent
+}
+
+func objectsToJSON(objs []runtime.Object) string {
+	str, _ := json.Marshal(objs)
+
+	return string(str)
+}
+
+func k8sNamespaceListValidator(c *k8sTest, tester *tester, t *testing.T) {
+	gotBody := &v1.NamespaceList{}
+	expBody := &[]v1.Namespace{}
+
+	json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
+	json.Unmarshal([]byte(c.expBody), expBody)
+
+	if !reflect.DeepEqual(gotBody.Items, *expBody) {
+		t.Errorf("%s, handler returned wrong body: got %v want %v",
+			c.msg, gotBody.Items, expBody)
+	}
+}

+ 1 - 1
server/api/user_handler_test.go

@@ -29,7 +29,7 @@ type userTest struct {
 func testUserRequests(t *testing.T, tests []*userTest, canQuery bool) {
 func testUserRequests(t *testing.T, tests []*userTest, canQuery bool) {
 	for _, c := range tests {
 	for _, c := range tests {
 		// create a new tester
 		// create a new tester
-		tester := newTester(canQuery, nil)
+		tester := newTester(canQuery)
 
 
 		// if there's an initializer, call it
 		// if there's an initializer, call it
 		for _, init := range c.initializers {
 		for _, init := range c.initializers {

+ 4 - 3
server/router/middleware/auth.go

@@ -8,18 +8,18 @@ import (
 	"strconv"
 	"strconv"
 
 
 	"github.com/go-chi/chi"
 	"github.com/go-chi/chi"
-	sessionstore "github.com/porter-dev/porter/internal/auth"
+	"github.com/gorilla/sessions"
 )
 )
 
 
 // Auth implements the authorization functions
 // Auth implements the authorization functions
 type Auth struct {
 type Auth struct {
-	store      *sessionstore.PGStore
+	store      sessions.Store
 	cookieName string
 	cookieName string
 }
 }
 
 
 // NewAuth returns a new Auth instance
 // NewAuth returns a new Auth instance
 func NewAuth(
 func NewAuth(
-	store *sessionstore.PGStore,
+	store sessions.Store,
 	cookieName string,
 	cookieName string,
 ) *Auth {
 ) *Auth {
 	return &Auth{store, cookieName}
 	return &Auth{store, cookieName}
@@ -66,6 +66,7 @@ func (auth *Auth) DoesUserIDMatch(next http.Handler, loc IDLocation) http.Handle
 			form := &bodyID{}
 			form := &bodyID{}
 			body, _ := ioutil.ReadAll(r.Body)
 			body, _ := ioutil.ReadAll(r.Body)
 			err = json.Unmarshal(body, form)
 			err = json.Unmarshal(body, form)
+
 			id = form.UserID
 			id = form.UserID
 
 
 			// need to create a new stream for the body
 			// need to create a new stream for the body

+ 6 - 3
server/router/router.go

@@ -2,15 +2,14 @@ package router
 
 
 import (
 import (
 	"github.com/go-chi/chi"
 	"github.com/go-chi/chi"
+	"github.com/gorilla/sessions"
 	"github.com/porter-dev/porter/server/api"
 	"github.com/porter-dev/porter/server/api"
 	"github.com/porter-dev/porter/server/requestlog"
 	"github.com/porter-dev/porter/server/requestlog"
 	mw "github.com/porter-dev/porter/server/router/middleware"
 	mw "github.com/porter-dev/porter/server/router/middleware"
-
-	sessionstore "github.com/porter-dev/porter/internal/auth"
 )
 )
 
 
 // New creates a new Chi router instance
 // New creates a new Chi router instance
-func New(a *api.App, store *sessionstore.PGStore, cookieName string) *chi.Mux {
+func New(a *api.App, store sessions.Store, cookieName string) *chi.Mux {
 	l := a.Logger()
 	l := a.Logger()
 	r := chi.NewRouter()
 	r := chi.NewRouter()
 	auth := mw.NewAuth(store, cookieName)
 	auth := mw.NewAuth(store, cookieName)
@@ -30,6 +29,10 @@ func New(a *api.App, store *sessionstore.PGStore, cookieName string) *chi.Mux {
 
 
 		// /api/charts routes
 		// /api/charts routes
 		r.Method("GET", "/charts", auth.DoesUserIDMatch(requestlog.NewHandler(a.HandleListCharts, l), mw.BodyParam))
 		r.Method("GET", "/charts", auth.DoesUserIDMatch(requestlog.NewHandler(a.HandleListCharts, l), mw.BodyParam))
+		r.Method("GET", "/charts/{name}/{revision}", auth.DoesUserIDMatch(requestlog.NewHandler(a.HandleGetChart, l), mw.BodyParam))
+
+		// /api/k8s routes
+		r.Method("GET", "/k8s/namespaces", auth.DoesUserIDMatch(requestlog.NewHandler(a.HandleListNamespaces, l), mw.BodyParam))
 	})
 	})
 
 
 	return r
 	return r