Repository: kubernetes/dashboard
Pull request #4017 (2019-07-04 10:34:27)

Plugin controller to manage plugin source from dashboard backend
Ref: #3996
- [x] Add CRD definition for custom plugins
- [x] Add go types for `Plugin` kind
- [x] Read plugin source from ConfigMapRef
- [x] Serve plugin sources via backend whose CRD exists

k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/dashboard/pull/4017#" title="Author self-approved">ajatprabha</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **rf232**
You can assign the PR to them by writing `/assign @rf232` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fdashboard).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes/dashboard/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["rf232"]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/kubernetes/dashboard/pull/4017?src=pr&el=h1) Report
> Merging [#4017](https://codecov.io/gh/kubernetes/dashboard/pull/4017?src=pr&el=desc) into [master](https://codecov.io/gh/kubernetes/dashboard/commit/3b106bb63ba64e6407238acdbbc7519ef7452d33?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/kubernetes/dashboard/pull/4017/graphs/tree.svg?width=650&token=Wj7jysJ9A5&height=150&src=pr)](https://codecov.io/gh/kubernetes/dashboard/pull/4017?src=pr&el=tree)

```diff
@@          Coverage Diff           @@
##           master   #4017   +/-   ##
======================================
  Coverage    46.4%   46.4%           
======================================
  Files         188     188           
  Lines        8708    8708           
  Branches       71      71           
======================================
  Hits         4041    4041           
  Misses       4417    4417           
  Partials      250     250
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/kubernetes/dashboard/pull/4017?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/kubernetes/dashboard/pull/4017?src=pr&el=footer). Last update [3b106bb...a0aae53](https://codecov.io/gh/kubernetes/dashboard/pull/4017?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

k8s-ci-robot: @ajatprabha: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
maciaszczykm: Looks good, added some comments.
k8s-ci-robot: @ajatprabha: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/dashboard/pull/4017#" title="Author self-approved">ajatprabha</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **bryk**
You can assign the PR to them by writing `/assign @bryk` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fdashboard).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes/dashboard/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["bryk"]} -->
floreks: Awesome!

/lgtm
maciaszczykm: /approve

@floreks will you LGTM?
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/dashboard/pull/4017#" title="Author self-approved">ajatprabha</a>*, *<a href="https://github.com/kubernetes/dashboard/pull/4017#issuecomment-511699403" title="LGTM">floreks</a>*, *<a href="https://github.com/kubernetes/dashboard/pull/4017#issuecomment-511699483" title="Approved">maciaszczykm</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fdashboard).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes/dashboard/blob/master/OWNERS)~~ [floreks,maciaszczykm]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/aio/gulp/check.js b/aio/gulp/check.js
index ceb503638..be019fb41 100644
--- a/aio/gulp/check.js
+++ b/aio/gulp/check.js
@@ -59,7 +59,10 @@ gulp.task('check-license-headers', () => {
 
   return gulp
       .src(
-          [path.join(conf.paths.src, getLicenseFileFilter('ts', 'go', 'scss', 'html'))],
+          [
+            path.join(conf.paths.src, getLicenseFileFilter('!deepcopy.go', 'ts', 'go', 'scss', 'html')),
+            path.join("!", conf.paths.src, getLicenseFileFilter('deepcopy.go'))
+          ],
           {base: conf.paths.base})
       .pipe(commonFilter)
       .pipe(licenseCheck(licenseConfig('aio/templates/header.txt')).on('log', handleLogEvent))
@@ -93,7 +96,10 @@ gulp.task('update-license-headers', (doneFn) => {
   const matchRate = 0.9;
 
   gulp.src(
-          [path.join(conf.paths.src, getLicenseFileFilter('ts', 'go', 'scss', 'html'))],
+          [
+            path.join(conf.paths.src, getLicenseFileFilter('ts', 'go', 'scss', 'html')),
+            path.join("!", conf.paths.src, getLicenseFileFilter('deepcopy.go'))
+          ],
           {base: conf.paths.base})
       .pipe(commonFilter)
       .pipe(license(fs.readFileSync('aio/templates/header.txt', 'utf8'), {}, matchRate))
diff --git a/aio/scripts/license-header.go.txt b/aio/scripts/license-header.go.txt
new file mode 100644
index 000000000..d216c039a
--- /dev/null
+++ b/aio/scripts/license-header.go.txt
@@ -0,0 +1,13 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
diff --git a/aio/scripts/update-codegen.sh b/aio/scripts/update-codegen.sh
new file mode 100755
index 000000000..9d52198a2
--- /dev/null
+++ b/aio/scripts/update-codegen.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+# Copyright 2017 The Kubernetes Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/../..
+CODEGEN_PKG=${GOPATH}/src/k8s.io/code-generator
+
+# generate the code with:
+# --output-base    because this script should also be able to run inside the vendor dir of
+#                  kl8s.io/kubernetes. The output-base is needed for the generators to output into the vendor dir
+#                  instead of the $GOPATH directly. For normal projects this can be dropped.
+"${CODEGEN_PKG}"/generate-groups.sh "deepcopy,client,informer,lister" \
+  github.com/kubernetes/dashboard/src/app/backend/plugin/client github.com/kubernetes/dashboard/src/app/backend/plugin \
+  apis:v1alpha1 \
+  --go-header-file "${SCRIPT_ROOT}"/aio/scripts/license-header.go.txt \
+  --output-base "$(dirname "${BASH_SOURCE[0]}")/../../../../.."
diff --git a/go.sum b/go.sum
index 67fdfc27a..cffc9ffd1 100644
--- a/go.sum
+++ b/go.sum
@@ -28,6 +28,7 @@ github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI=
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8=
 github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -45,6 +46,7 @@ github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:
 github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
diff --git a/src/app/backend/client/api/types.go b/src/app/backend/client/api/types.go
index f16bf48e4..d7b6f5072 100644
--- a/src/app/backend/client/api/types.go
+++ b/src/app/backend/client/api/types.go
@@ -17,6 +17,7 @@ package api
 import (
 	"github.com/emicklei/go-restful"
 	authApi "github.com/kubernetes/dashboard/src/app/backend/auth/api"
+	pluginclientset "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned"
 	v1 "k8s.io/api/authorization/v1"
 	apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -40,6 +41,7 @@ type ClientManager interface {
 	Client(req *restful.Request) (kubernetes.Interface, error)
 	InsecureClient() kubernetes.Interface
 	APIExtensionsClient(req *restful.Request) (apiextensionsclientset.Interface, error)
+	PluginClient(req *restful.Request) (pluginclientset.Interface, error)
 	InsecureAPIExtensionsClient() apiextensionsclientset.Interface
 	CanI(req *restful.Request, ssar *v1.SelfSubjectAccessReview) bool
 	Config(req *restful.Request) (*rest.Config, error)
diff --git a/src/app/backend/client/manager.go b/src/app/backend/client/manager.go
index 0fcfc0758..4bc588959 100644
--- a/src/app/backend/client/manager.go
+++ b/src/app/backend/client/manager.go
@@ -19,6 +19,7 @@ import (
 	"strings"
 
 	"github.com/emicklei/go-restful"
+	pluginclientset "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned"
 	v1 "k8s.io/api/authorization/v1"
 	apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
 	"k8s.io/client-go/kubernetes"
@@ -71,6 +72,9 @@ type clientManager struct {
 	// API Extensions client created without providing auth info. It uses permissions granted to
 	// service account used by dashboard or kubeconfig file if it was passed during dashboard init.
 	insecureAPIExtensionsClient apiextensionsclientset.Interface
+	// Plugin client created without providing auth info. It uses permissions granted to
+	// service account used by dashboard or kubeconfig file if it was passed during dashboard init.
+	insecurePluginClient pluginclientset.Interface
 	// Kubernetes client created without providing auth info. It uses permissions granted to
 	// service account used by dashboard or kubeconfig file if it was passed during dashboard init.
 	insecureClient kubernetes.Interface
@@ -110,6 +114,21 @@ func (self *clientManager) APIExtensionsClient(req *restful.Request) (apiextensi
 	return self.InsecureAPIExtensionsClient(), nil
 }
 
+// PluginClient returns a plugin client. In case dashboard login is enabled and
+// option to skip login page is disabled only secure client will be returned, otherwise insecure
+// client will be used.
+func (self *clientManager) PluginClient(req *restful.Request) (pluginclientset.Interface, error) {
+	if req == nil {
+		return nil, errors.NewBadRequest("request can not be nil!")
+	}
+
+	if self.isSecureModeEnabled(req) {
+		return self.securePluginClient(req)
+	}
+
+	return self.InsecurePluginClient(), nil
+}
+
 // Config returns a rest config. In case dashboard login is enabled and option to skip
 // login page is disabled only secure config will be returned, otherwise insecure config will be
 // used.
@@ -139,6 +158,13 @@ func (self *clientManager) InsecureAPIExtensionsClient() apiextensionsclientset.
 	return self.insecureAPIExtensionsClient
 }
 
+// InsecurePluginClient returns plugin client that was created without providing
+// auth info. It uses permissions granted to service account used by dashboard or kubeconfig file
+// if it was passed during dashboard init.
+func (self *clientManager) InsecurePluginClient() pluginclientset.Interface {
+	return self.insecurePluginClient
+}
+
 // InsecureConfig returns kubernetes client config that used privileges of dashboard service account
 // or kubeconfig file if it was passed during dashboard init.
 func (self *clientManager) InsecureConfig() *rest.Config {
@@ -358,6 +384,20 @@ func (self *clientManager) secureAPIExtensionsClient(req *restful.Request) (apie
 	return client, nil
 }
 
+func (self *clientManager) securePluginClient(req *restful.Request) (pluginclientset.Interface, error) {
+	cfg, err := self.secureConfig(req)
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := pluginclientset.NewForConfig(cfg)
+	if err != nil {
+		return nil, err
+	}
+
+	return client, nil
+}
+
 func (self *clientManager) secureConfig(req *restful.Request) (*rest.Config, error) {
 	cmdConfig, err := self.ClientCmdConfig(req)
 	if err != nil {
@@ -425,8 +465,14 @@ func (self *clientManager) initInsecureClients() {
 		panic(err)
 	}
 
+	pluginclient, err := pluginclientset.NewForConfig(self.insecureConfig)
+	if err != nil {
+		panic(err)
+	}
+
 	self.insecureClient = k8sClient
 	self.insecureAPIExtensionsClient = apiextensionsclient
+	self.insecurePluginClient = pluginclient
 }
 
 func (self *clientManager) initInsecureConfig() {
diff --git a/src/app/backend/handler/apihandler.go b/src/app/backend/handler/apihandler.go
index 8d61fbc62..6191b0ac8 100644
--- a/src/app/backend/handler/apihandler.go
+++ b/src/app/backend/handler/apihandler.go
@@ -20,7 +20,9 @@ import (
 	"strconv"
 	"strings"
 
-	restful "github.com/emicklei/go-restful"
+	"github.com/kubernetes/dashboard/src/app/backend/plugin"
+
+	"github.com/emicklei/go-restful"
 	"github.com/kubernetes/dashboard/src/app/backend/api"
 	"github.com/kubernetes/dashboard/src/app/backend/auth"
 	authApi "github.com/kubernetes/dashboard/src/app/backend/auth/api"
@@ -106,6 +108,9 @@ func CreateHTTPAPIHandler(iManager integration.IntegrationManager, cManager clie
 	integrationHandler := integration.NewIntegrationHandler(iManager)
 	integrationHandler.Install(apiV1Ws)
 
+	pluginHandler := plugin.NewPluginHandler(cManager)
+	pluginHandler.Install(apiV1Ws)
+
 	authHandler := auth.NewAuthHandler(authManager)
 	authHandler.Install(apiV1Ws)
 
diff --git a/src/app/backend/plugin/apis/register.go b/src/app/backend/plugin/apis/register.go
new file mode 100644
index 000000000..178ceed9b
--- /dev/null
+++ b/src/app/backend/plugin/apis/register.go
@@ -0,0 +1,20 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package apis
+
+// GroupName is the group name used in this package
+const (
+	GroupName = "dashboard.k8s.io"
+)
diff --git a/src/app/backend/plugin/apis/v1alpha1/doc.go b/src/app/backend/plugin/apis/v1alpha1/doc.go
new file mode 100644
index 000000000..9ee15851f
--- /dev/null
+++ b/src/app/backend/plugin/apis/v1alpha1/doc.go
@@ -0,0 +1,19 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// +k8s:deepcopy-gen=package
+// +groupName=dashboard.k8s.io
+
+// Package v1alpha1 is the v1alpha1 version of the API.
+package v1alpha1
diff --git a/src/app/backend/plugin/apis/v1alpha1/register.go b/src/app/backend/plugin/apis/v1alpha1/register.go
new file mode 100644
index 000000000..f14fa4405
--- /dev/null
+++ b/src/app/backend/plugin/apis/v1alpha1/register.go
@@ -0,0 +1,53 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	"github.com/kubernetes/dashboard/src/app/backend/plugin/apis"
+)
+
+// SchemeGroupVersion is group version used to register these objects
+var SchemeGroupVersion = schema.GroupVersion{Group: apis.GroupName, Version: "v1alpha1"}
+
+// Kind takes an unqualified kind and returns back a Group qualified GroupKind
+func Kind(kind string) schema.GroupKind {
+	return SchemeGroupVersion.WithKind(kind).GroupKind()
+}
+
+// Resource takes an unqualified resource and returns a Group qualified GroupResource
+func Resource(resource string) schema.GroupResource {
+	return SchemeGroupVersion.WithResource(resource).GroupResource()
+}
+
+var (
+	// SchemeBuilder initializes a scheme builder
+	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
+	// AddToScheme is a global function that registers this API group & version to a scheme
+	AddToScheme = SchemeBuilder.AddToScheme
+)
+
+// Adds the list of known types to Scheme.
+func addKnownTypes(scheme *runtime.Scheme) error {
+	scheme.AddKnownTypes(SchemeGroupVersion,
+		&Plugin{},
+		&PluginList{},
+	)
+	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
+	return nil
+}
diff --git a/src/app/backend/plugin/apis/v1alpha1/types.go b/src/app/backend/plugin/apis/v1alpha1/types.go
new file mode 100644
index 000000000..5840f0dab
--- /dev/null
+++ b/src/app/backend/plugin/apis/v1alpha1/types.go
@@ -0,0 +1,52 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1alpha1
+
+import (
+	coreV1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// +genclient
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// Plugin holds the object information for Plugin kind, it also implements runtime.Object
+type Plugin struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec PluginSpec `json:"spec"`
+}
+
+// PluginSpec holds the specs for the Plugin kind
+type PluginSpec struct {
+	Source Source `json:"source"`
+}
+
+// Source holds the information about the plugin's source code origin
+type Source struct {
+	Filename     string                     `json:"filename,omitempty"`
+	ConfigMapRef *coreV1.ConfigMapEnvSource `json:"configMapRef,omitempty" protobuf:"bytes,1,opt,name=configMapRef"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// PluginList holds the list information for Plugin kind, it also implements runtime.Object
+type PluginList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata"`
+
+	Items []Plugin `json:"items"`
+}
diff --git a/src/app/backend/plugin/apis/v1alpha1/zz_generated.deepcopy.go b/src/app/backend/plugin/apis/v1alpha1/zz_generated.deepcopy.go
new file mode 100644
index 000000000..dcd50829f
--- /dev/null
+++ b/src/app/backend/plugin/apis/v1alpha1/zz_generated.deepcopy.go
@@ -0,0 +1,122 @@
+// +build !ignore_autogenerated
+
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by deepcopy-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	v1 "k8s.io/api/core/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Plugin) DeepCopyInto(out *Plugin) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Plugin.
+func (in *Plugin) DeepCopy() *Plugin {
+	if in == nil {
+		return nil
+	}
+	out := new(Plugin)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Plugin) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PluginList) DeepCopyInto(out *PluginList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]Plugin, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginList.
+func (in *PluginList) DeepCopy() *PluginList {
+	if in == nil {
+		return nil
+	}
+	out := new(PluginList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *PluginList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PluginSpec) DeepCopyInto(out *PluginSpec) {
+	*out = *in
+	in.Source.DeepCopyInto(&out.Source)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginSpec.
+func (in *PluginSpec) DeepCopy() *PluginSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(PluginSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Source) DeepCopyInto(out *Source) {
+	*out = *in
+	if in.ConfigMapRef != nil {
+		in, out := &in.ConfigMapRef, &out.ConfigMapRef
+		*out = new(v1.ConfigMapEnvSource)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Source.
+func (in *Source) DeepCopy() *Source {
+	if in == nil {
+		return nil
+	}
+	out := new(Source)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/src/app/backend/plugin/client/clientset/versioned/clientset.go b/src/app/backend/plugin/client/clientset/versioned/clientset.go
new file mode 100644
index 000000000..66d79fe29
--- /dev/null
+++ b/src/app/backend/plugin/client/clientset/versioned/clientset.go
@@ -0,0 +1,88 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package versioned
+
+import (
+	dashboardv1alpha1 "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1"
+	discovery "k8s.io/client-go/discovery"
+	rest "k8s.io/client-go/rest"
+	flowcontrol "k8s.io/client-go/util/flowcontrol"
+)
+
+type Interface interface {
+	Discovery() discovery.DiscoveryInterface
+	DashboardV1alpha1() dashboardv1alpha1.DashboardV1alpha1Interface
+}
+
+// Clientset contains the clients for groups. Each group has exactly one
+// version included in a Clientset.
+type Clientset struct {
+	*discovery.DiscoveryClient
+	dashboardV1alpha1 *dashboardv1alpha1.DashboardV1alpha1Client
+}
+
+// DashboardV1alpha1 retrieves the DashboardV1alpha1Client
+func (c *Clientset) DashboardV1alpha1() dashboardv1alpha1.DashboardV1alpha1Interface {
+	return c.dashboardV1alpha1
+}
+
+// Discovery retrieves the DiscoveryClient
+func (c *Clientset) Discovery() discovery.DiscoveryInterface {
+	if c == nil {
+		return nil
+	}
+	return c.DiscoveryClient
+}
+
+// NewForConfig creates a new Clientset for the given config.
+func NewForConfig(c *rest.Config) (*Clientset, error) {
+	configShallowCopy := *c
+	if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 {
+		configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst)
+	}
+	var cs Clientset
+	var err error
+	cs.dashboardV1alpha1, err = dashboardv1alpha1.NewForConfig(&configShallowCopy)
+	if err != nil {
+		return nil, err
+	}
+
+	cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy)
+	if err != nil {
+		return nil, err
+	}
+	return &cs, nil
+}
+
+// NewForConfigOrDie creates a new Clientset for the given config and
+// panics if there is an error in the config.
+func NewForConfigOrDie(c *rest.Config) *Clientset {
+	var cs Clientset
+	cs.dashboardV1alpha1 = dashboardv1alpha1.NewForConfigOrDie(c)
+
+	cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c)
+	return &cs
+}
+
+// New creates a new Clientset for the given RESTClient.
+func New(c rest.Interface) *Clientset {
+	var cs Clientset
+	cs.dashboardV1alpha1 = dashboardv1alpha1.New(c)
+
+	cs.DiscoveryClient = discovery.NewDiscoveryClient(c)
+	return &cs
+}
diff --git a/src/app/backend/plugin/client/clientset/versioned/doc.go b/src/app/backend/plugin/client/clientset/versioned/doc.go
new file mode 100644
index 000000000..9e5158cd5
--- /dev/null
+++ b/src/app/backend/plugin/client/clientset/versioned/doc.go
@@ -0,0 +1,18 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package has the automatically generated clientset.
+package versioned
diff --git a/src/app/backend/plugin/client/clientset/versioned/fake/clientset_generated.go b/src/app/backend/plugin/client/clientset/versioned/fake/clientset_generated.go
new file mode 100644
index 000000000..2d111646f
--- /dev/null
+++ b/src/app/backend/plugin/client/clientset/versioned/fake/clientset_generated.go
@@ -0,0 +1,80 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	clientset "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned"
+	dashboardv1alpha1 "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1"
+	fakedashboardv1alpha1 "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/fake"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/watch"
+	"k8s.io/client-go/discovery"
+	fakediscovery "k8s.io/client-go/discovery/fake"
+	"k8s.io/client-go/testing"
+)
+
+// NewSimpleClientset returns a clientset that will respond with the provided objects.
+// It's backed by a very simple object tracker that processes creates, updates and deletions as-is,
+// without applying any validations and/or defaults. It shouldn't be considered a replacement
+// for a real clientset and is mostly useful in simple unit tests.
+func NewSimpleClientset(objects ...runtime.Object) *Clientset {
+	o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder())
+	for _, obj := range objects {
+		if err := o.Add(obj); err != nil {
+			panic(err)
+		}
+	}
+
+	cs := &Clientset{tracker: o}
+	cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake}
+	cs.AddReactor("*", "*", testing.ObjectReaction(o))
+	cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) {
+		gvr := action.GetResource()
+		ns := action.GetNamespace()
+		watch, err := o.Watch(gvr, ns)
+		if err != nil {
+			return false, nil, err
+		}
+		return true, watch, nil
+	})
+
+	return cs
+}
+
+// Clientset implements clientset.Interface. Meant to be embedded into a
+// struct to get a default implementation. This makes faking out just the method
+// you want to test easier.
+type Clientset struct {
+	testing.Fake
+	discovery *fakediscovery.FakeDiscovery
+	tracker   testing.ObjectTracker
+}
+
+func (c *Clientset) Discovery() discovery.DiscoveryInterface {
+	return c.discovery
+}
+
+func (c *Clientset) Tracker() testing.ObjectTracker {
+	return c.tracker
+}
+
+var _ clientset.Interface = &Clientset{}
+
+// DashboardV1alpha1 retrieves the DashboardV1alpha1Client
+func (c *Clientset) DashboardV1alpha1() dashboardv1alpha1.DashboardV1alpha1Interface {
+	return &fakedashboardv1alpha1.FakeDashboardV1alpha1{Fake: &c.Fake}
+}
diff --git a/src/app/backend/plugin/client/clientset/versioned/fake/doc.go b/src/app/backend/plugin/client/clientset/versioned/fake/doc.go
new file mode 100644
index 000000000..f746cb888
--- /dev/null
+++ b/src/app/backend/plugin/client/clientset/versioned/fake/doc.go
@@ -0,0 +1,18 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package has the automatically generated fake clientset.
+package fake
diff --git a/src/app/backend/plugin/client/clientset/versioned/fake/register.go b/src/app/backend/plugin/client/clientset/versioned/fake/register.go
new file mode 100644
index 000000000..7b331eb2a
--- /dev/null
+++ b/src/app/backend/plugin/client/clientset/versioned/fake/register.go
@@ -0,0 +1,54 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	dashboardv1alpha1 "github.com/kubernetes/dashboard/src/app/backend/plugin/apis/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	serializer "k8s.io/apimachinery/pkg/runtime/serializer"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+)
+
+var scheme = runtime.NewScheme()
+var codecs = serializer.NewCodecFactory(scheme)
+var parameterCodec = runtime.NewParameterCodec(scheme)
+var localSchemeBuilder = runtime.SchemeBuilder{
+	dashboardv1alpha1.AddToScheme,
+}
+
+// AddToScheme adds all types of this clientset into the given scheme. This allows composition
+// of clientsets, like in:
+//
+//   import (
+//     "k8s.io/client-go/kubernetes"
+//     clientsetscheme "k8s.io/client-go/kubernetes/scheme"
+//     aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
+//   )
+//
+//   kclientset, _ := kubernetes.NewForConfig(c)
+//   _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
+//
+// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
+// correctly.
+var AddToScheme = localSchemeBuilder.AddToScheme
+
+func init() {
+	v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
+	utilruntime.Must(AddToScheme(scheme))
+}
diff --git a/src/app/backend/plugin/client/clientset/versioned/scheme/doc.go b/src/app/backend/plugin/client/clientset/versioned/scheme/doc.go
new file mode 100644
index 000000000..7c13e40fd
--- /dev/null
+++ b/src/app/backend/plugin/client/clientset/versioned/scheme/doc.go
@@ -0,0 +1,18 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package contains the scheme of the automatically generated clientset.
+package scheme
diff --git a/src/app/backend/plugin/client/clientset/versioned/scheme/register.go b/src/app/backend/plugin/client/clientset/versioned/scheme/register.go
new file mode 100644
index 000000000..e3445e8c0
--- /dev/null
+++ b/src/app/backend/plugin/client/clientset/versioned/scheme/register.go
@@ -0,0 +1,54 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package scheme
+
+import (
+	dashboardv1alpha1 "github.com/kubernetes/dashboard/src/app/backend/plugin/apis/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	serializer "k8s.io/apimachinery/pkg/runtime/serializer"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+)
+
+var Scheme = runtime.NewScheme()
+var Codecs = serializer.NewCodecFactory(Scheme)
+var ParameterCodec = runtime.NewParameterCodec(Scheme)
+var localSchemeBuilder = runtime.SchemeBuilder{
+	dashboardv1alpha1.AddToScheme,
+}
+
+// AddToScheme adds all types of this clientset into the given scheme. This allows composition
+// of clientsets, like in:
+//
+//   import (
+//     "k8s.io/client-go/kubernetes"
+//     clientsetscheme "k8s.io/client-go/kubernetes/scheme"
+//     aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
+//   )
+//
+//   kclientset, _ := kubernetes.NewForConfig(c)
+//   _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
+//
+// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
+// correctly.
+var AddToScheme = localSchemeBuilder.AddToScheme
+
+func init() {
+	v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
+	utilruntime.Must(AddToScheme(Scheme))
+}
diff --git a/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/apis_client.go b/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/apis_client.go
new file mode 100644
index 000000000..6dd5f677e
--- /dev/null
+++ b/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/apis_client.go
@@ -0,0 +1,87 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	v1alpha1 "github.com/kubernetes/dashboard/src/app/backend/plugin/apis/v1alpha1"
+	"github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned/scheme"
+	rest "k8s.io/client-go/rest"
+)
+
+type DashboardV1alpha1Interface interface {
+	RESTClient() rest.Interface
+	PluginsGetter
+}
+
+// DashboardV1alpha1Client is used to interact with features provided by the dashboard.k8s.io group.
+type DashboardV1alpha1Client struct {
+	restClient rest.Interface
+}
+
+func (c *DashboardV1alpha1Client) Plugins(namespace string) PluginInterface {
+	return newPlugins(c, namespace)
+}
+
+// NewForConfig creates a new DashboardV1alpha1Client for the given config.
+func NewForConfig(c *rest.Config) (*DashboardV1alpha1Client, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	client, err := rest.RESTClientFor(&config)
+	if err != nil {
+		return nil, err
+	}
+	return &DashboardV1alpha1Client{client}, nil
+}
+
+// NewForConfigOrDie creates a new DashboardV1alpha1Client for the given config and
+// panics if there is an error in the config.
+func NewForConfigOrDie(c *rest.Config) *DashboardV1alpha1Client {
+	client, err := NewForConfig(c)
+	if err != nil {
+		panic(err)
+	}
+	return client
+}
+
+// New creates a new DashboardV1alpha1Client for the given RESTClient.
+func New(c rest.Interface) *DashboardV1alpha1Client {
+	return &DashboardV1alpha1Client{c}
+}
+
+func setConfigDefaults(config *rest.Config) error {
+	gv := v1alpha1.SchemeGroupVersion
+	config.GroupVersion = &gv
+	config.APIPath = "/apis"
+	config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
+
+	if config.UserAgent == "" {
+		config.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+
+	return nil
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *DashboardV1alpha1Client) RESTClient() rest.Interface {
+	if c == nil {
+		return nil
+	}
+	return c.restClient
+}
diff --git a/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/doc.go b/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/doc.go
new file mode 100644
index 000000000..42ba49ef2
--- /dev/null
+++ b/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/doc.go
@@ -0,0 +1,18 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package has the automatically generated typed clients.
+package v1alpha1
diff --git a/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/fake/doc.go b/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/fake/doc.go
new file mode 100644
index 000000000..9d02b28e2
--- /dev/null
+++ b/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/fake/doc.go
@@ -0,0 +1,18 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// Package fake has the automatically generated clients.
+package fake
diff --git a/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apis_client.go b/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apis_client.go
new file mode 100644
index 000000000..de1467073
--- /dev/null
+++ b/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apis_client.go
@@ -0,0 +1,38 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	v1alpha1 "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1"
+	rest "k8s.io/client-go/rest"
+	testing "k8s.io/client-go/testing"
+)
+
+type FakeDashboardV1alpha1 struct {
+	*testing.Fake
+}
+
+func (c *FakeDashboardV1alpha1) Plugins(namespace string) v1alpha1.PluginInterface {
+	return &FakePlugins{c, namespace}
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *FakeDashboardV1alpha1) RESTClient() rest.Interface {
+	var ret *rest.RESTClient
+	return ret
+}
diff --git a/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_plugin.go b/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_plugin.go
new file mode 100644
index 000000000..d08b3e7b8
--- /dev/null
+++ b/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_plugin.go
@@ -0,0 +1,126 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	v1alpha1 "github.com/kubernetes/dashboard/src/app/backend/plugin/apis/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	testing "k8s.io/client-go/testing"
+)
+
+// FakePlugins implements PluginInterface
+type FakePlugins struct {
+	Fake *FakeDashboardV1alpha1
+	ns   string
+}
+
+var pluginsResource = schema.GroupVersionResource{Group: "dashboard.k8s.io", Version: "v1alpha1", Resource: "plugins"}
+
+var pluginsKind = schema.GroupVersionKind{Group: "dashboard.k8s.io", Version: "v1alpha1", Kind: "Plugin"}
+
+// Get takes name of the plugin, and returns the corresponding plugin object, and an error if there is any.
+func (c *FakePlugins) Get(name string, options v1.GetOptions) (result *v1alpha1.Plugin, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(pluginsResource, c.ns, name), &v1alpha1.Plugin{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.Plugin), err
+}
+
+// List takes label and field selectors, and returns the list of Plugins that match those selectors.
+func (c *FakePlugins) List(opts v1.ListOptions) (result *v1alpha1.PluginList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(pluginsResource, pluginsKind, c.ns, opts), &v1alpha1.PluginList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1alpha1.PluginList{ListMeta: obj.(*v1alpha1.PluginList).ListMeta}
+	for _, item := range obj.(*v1alpha1.PluginList).Items {
+		if label.Matches(labels.Set(item.Labels)) {
+			list.Items = append(list.Items, item)
+		}
+	}
+	return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested plugins.
+func (c *FakePlugins) Watch(opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(pluginsResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a plugin and creates it.  Returns the server's representation of the plugin, and an error, if there is any.
+func (c *FakePlugins) Create(plugin *v1alpha1.Plugin) (result *v1alpha1.Plugin, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(pluginsResource, c.ns, plugin), &v1alpha1.Plugin{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.Plugin), err
+}
+
+// Update takes the representation of a plugin and updates it. Returns the server's representation of the plugin, and an error, if there is any.
+func (c *FakePlugins) Update(plugin *v1alpha1.Plugin) (result *v1alpha1.Plugin, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(pluginsResource, c.ns, plugin), &v1alpha1.Plugin{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.Plugin), err
+}
+
+// Delete takes name of the plugin and deletes it. Returns an error if one occurs.
+func (c *FakePlugins) Delete(name string, options *v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteAction(pluginsResource, c.ns, name), &v1alpha1.Plugin{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakePlugins) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(pluginsResource, c.ns, listOptions)
+
+	_, err := c.Fake.Invokes(action, &v1alpha1.PluginList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched plugin.
+func (c *FakePlugins) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Plugin, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(pluginsResource, c.ns, name, pt, data, subresources...), &v1alpha1.Plugin{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.Plugin), err
+}
diff --git a/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/generated_expansion.go b/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/generated_expansion.go
new file mode 100644
index 000000000..062e8e957
--- /dev/null
+++ b/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/generated_expansion.go
@@ -0,0 +1,19 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+type PluginExpansion interface{}
diff --git a/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/plugin.go b/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/plugin.go
new file mode 100644
index 000000000..0c5bcc882
--- /dev/null
+++ b/src/app/backend/plugin/client/clientset/versioned/typed/apis/v1alpha1/plugin.go
@@ -0,0 +1,172 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"time"
+
+	v1alpha1 "github.com/kubernetes/dashboard/src/app/backend/plugin/apis/v1alpha1"
+	scheme "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned/scheme"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// PluginsGetter has a method to return a PluginInterface.
+// A group's client should implement this interface.
+type PluginsGetter interface {
+	Plugins(namespace string) PluginInterface
+}
+
+// PluginInterface has methods to work with Plugin resources.
+type PluginInterface interface {
+	Create(*v1alpha1.Plugin) (*v1alpha1.Plugin, error)
+	Update(*v1alpha1.Plugin) (*v1alpha1.Plugin, error)
+	Delete(name string, options *v1.DeleteOptions) error
+	DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error
+	Get(name string, options v1.GetOptions) (*v1alpha1.Plugin, error)
+	List(opts v1.ListOptions) (*v1alpha1.PluginList, error)
+	Watch(opts v1.ListOptions) (watch.Interface, error)
+	Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Plugin, err error)
+	PluginExpansion
+}
+
+// plugins implements PluginInterface
+type plugins struct {
+	client rest.Interface
+	ns     string
+}
+
+// newPlugins returns a Plugins
+func newPlugins(c *DashboardV1alpha1Client, namespace string) *plugins {
+	return &plugins{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the plugin, and returns the corresponding plugin object, and an error if there is any.
+func (c *plugins) Get(name string, options v1.GetOptions) (result *v1alpha1.Plugin, err error) {
+	result = &v1alpha1.Plugin{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("plugins").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do().
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of Plugins that match those selectors.
+func (c *plugins) List(opts v1.ListOptions) (result *v1alpha1.PluginList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1alpha1.PluginList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("plugins").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do().
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested plugins.
+func (c *plugins) Watch(opts v1.ListOptions) (watch.Interface, error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	opts.Watch = true
+	return c.client.Get().
+		Namespace(c.ns).
+		Resource("plugins").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch()
+}
+
+// Create takes the representation of a plugin and creates it.  Returns the server's representation of the plugin, and an error, if there is any.
+func (c *plugins) Create(plugin *v1alpha1.Plugin) (result *v1alpha1.Plugin, err error) {
+	result = &v1alpha1.Plugin{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("plugins").
+		Body(plugin).
+		Do().
+		Into(result)
+	return
+}
+
+// Update takes the representation of a plugin and updates it. Returns the server's representation of the plugin, and an error, if there is any.
+func (c *plugins) Update(plugin *v1alpha1.Plugin) (result *v1alpha1.Plugin, err error) {
+	result = &v1alpha1.Plugin{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("plugins").
+		Name(plugin.Name).
+		Body(plugin).
+		Do().
+		Into(result)
+	return
+}
+
+// Delete takes name of the plugin and deletes it. Returns an error if one occurs.
+func (c *plugins) Delete(name string, options *v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("plugins").
+		Name(name).
+		Body(options).
+		Do().
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *plugins) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
+	var timeout time.Duration
+	if listOptions.TimeoutSeconds != nil {
+		timeout = time.Duration(*listOptions.TimeoutSeconds) * time.Second
+	}
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("plugins").
+		VersionedParams(&listOptions, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(options).
+		Do().
+		Error()
+}
+
+// Patch applies the patch and returns the patched plugin.
+func (c *plugins) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Plugin, err error) {
+	result = &v1alpha1.Plugin{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("plugins").
+		SubResource(subresources...).
+		Name(name).
+		Body(data).
+		Do().
+		Into(result)
+	return
+}
diff --git a/src/app/backend/plugin/client/informers/externalversions/apis/interface.go b/src/app/backend/plugin/client/informers/externalversions/apis/interface.go
new file mode 100644
index 000000000..0b3fba862
--- /dev/null
+++ b/src/app/backend/plugin/client/informers/externalversions/apis/interface.go
@@ -0,0 +1,44 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package dashboard
+
+import (
+	v1alpha1 "github.com/kubernetes/dashboard/src/app/backend/plugin/client/informers/externalversions/apis/v1alpha1"
+	internalinterfaces "github.com/kubernetes/dashboard/src/app/backend/plugin/client/informers/externalversions/internalinterfaces"
+)
+
+// Interface provides access to each of this group's versions.
+type Interface interface {
+	// V1alpha1 provides access to shared informers for resources in V1alpha1.
+	V1alpha1() v1alpha1.Interface
+}
+
+type group struct {
+	factory          internalinterfaces.SharedInformerFactory
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// New returns a new Interface.
+func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
+	return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
+}
+
+// V1alpha1 returns a new v1alpha1.Interface.
+func (g *group) V1alpha1() v1alpha1.Interface {
+	return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions)
+}
diff --git a/src/app/backend/plugin/client/informers/externalversions/apis/v1alpha1/interface.go b/src/app/backend/plugin/client/informers/externalversions/apis/v1alpha1/interface.go
new file mode 100644
index 000000000..f5d63c067
--- /dev/null
+++ b/src/app/backend/plugin/client/informers/externalversions/apis/v1alpha1/interface.go
@@ -0,0 +1,43 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	internalinterfaces "github.com/kubernetes/dashboard/src/app/backend/plugin/client/informers/externalversions/internalinterfaces"
+)
+
+// Interface provides access to all the informers in this group version.
+type Interface interface {
+	// Plugins returns a PluginInformer.
+	Plugins() PluginInformer
+}
+
+type version struct {
+	factory          internalinterfaces.SharedInformerFactory
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// New returns a new Interface.
+func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
+	return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
+}
+
+// Plugins returns a PluginInformer.
+func (v *version) Plugins() PluginInformer {
+	return &pluginInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
diff --git a/src/app/backend/plugin/client/informers/externalversions/apis/v1alpha1/plugin.go b/src/app/backend/plugin/client/informers/externalversions/apis/v1alpha1/plugin.go
new file mode 100644
index 000000000..9fb2fbad8
--- /dev/null
+++ b/src/app/backend/plugin/client/informers/externalversions/apis/v1alpha1/plugin.go
@@ -0,0 +1,87 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	time "time"
+
+	apisv1alpha1 "github.com/kubernetes/dashboard/src/app/backend/plugin/apis/v1alpha1"
+	versioned "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned"
+	internalinterfaces "github.com/kubernetes/dashboard/src/app/backend/plugin/client/informers/externalversions/internalinterfaces"
+	v1alpha1 "github.com/kubernetes/dashboard/src/app/backend/plugin/client/listers/apis/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// PluginInformer provides access to a shared informer and lister for
+// Plugins.
+type PluginInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1alpha1.PluginLister
+}
+
+type pluginInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewPluginInformer constructs a new informer for Plugin type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewPluginInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredPluginInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredPluginInformer constructs a new informer for Plugin type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredPluginInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.DashboardV1alpha1().Plugins(namespace).List(options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.DashboardV1alpha1().Plugins(namespace).Watch(options)
+			},
+		},
+		&apisv1alpha1.Plugin{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *pluginInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredPluginInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *pluginInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&apisv1alpha1.Plugin{}, f.defaultInformer)
+}
+
+func (f *pluginInformer) Lister() v1alpha1.PluginLister {
+	return v1alpha1.NewPluginLister(f.Informer().GetIndexer())
+}
diff --git a/src/app/backend/plugin/client/informers/externalversions/factory.go b/src/app/backend/plugin/client/informers/externalversions/factory.go
new file mode 100644
index 000000000..ac57e25ef
--- /dev/null
+++ b/src/app/backend/plugin/client/informers/externalversions/factory.go
@@ -0,0 +1,178 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package externalversions
+
+import (
+	reflect "reflect"
+	sync "sync"
+	time "time"
+
+	versioned "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned"
+	apis "github.com/kubernetes/dashboard/src/app/backend/plugin/client/informers/externalversions/apis"
+	internalinterfaces "github.com/kubernetes/dashboard/src/app/backend/plugin/client/informers/externalversions/internalinterfaces"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// SharedInformerOption defines the functional option type for SharedInformerFactory.
+type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory
+
+type sharedInformerFactory struct {
+	client           versioned.Interface
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	lock             sync.Mutex
+	defaultResync    time.Duration
+	customResync     map[reflect.Type]time.Duration
+
+	informers map[reflect.Type]cache.SharedIndexInformer
+	// startedInformers is used for tracking which informers have been started.
+	// This allows Start() to be called multiple times safely.
+	startedInformers map[reflect.Type]bool
+}
+
+// WithCustomResyncConfig sets a custom resync period for the specified informer types.
+func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption {
+	return func(factory *sharedInformerFactory) *sharedInformerFactory {
+		for k, v := range resyncConfig {
+			factory.customResync[reflect.TypeOf(k)] = v
+		}
+		return factory
+	}
+}
+
+// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory.
+func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption {
+	return func(factory *sharedInformerFactory) *sharedInformerFactory {
+		factory.tweakListOptions = tweakListOptions
+		return factory
+	}
+}
+
+// WithNamespace limits the SharedInformerFactory to the specified namespace.
+func WithNamespace(namespace string) SharedInformerOption {
+	return func(factory *sharedInformerFactory) *sharedInformerFactory {
+		factory.namespace = namespace
+		return factory
+	}
+}
+
+// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces.
+func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory {
+	return NewSharedInformerFactoryWithOptions(client, defaultResync)
+}
+
+// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory.
+// Listers obtained via this SharedInformerFactory will be subject to the same filters
+// as specified here.
+// Deprecated: Please use NewSharedInformerFactoryWithOptions instead
+func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory {
+	return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions))
+}
+
+// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options.
+func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory {
+	factory := &sharedInformerFactory{
+		client:           client,
+		namespace:        v1.NamespaceAll,
+		defaultResync:    defaultResync,
+		informers:        make(map[reflect.Type]cache.SharedIndexInformer),
+		startedInformers: make(map[reflect.Type]bool),
+		customResync:     make(map[reflect.Type]time.Duration),
+	}
+
+	// Apply all options
+	for _, opt := range options {
+		factory = opt(factory)
+	}
+
+	return factory
+}
+
+// Start initializes all requested informers.
+func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) {
+	f.lock.Lock()
+	defer f.lock.Unlock()
+
+	for informerType, informer := range f.informers {
+		if !f.startedInformers[informerType] {
+			go informer.Run(stopCh)
+			f.startedInformers[informerType] = true
+		}
+	}
+}
+
+// WaitForCacheSync waits for all started informers' cache were synced.
+func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool {
+	informers := func() map[reflect.Type]cache.SharedIndexInformer {
+		f.lock.Lock()
+		defer f.lock.Unlock()
+
+		informers := map[reflect.Type]cache.SharedIndexInformer{}
+		for informerType, informer := range f.informers {
+			if f.startedInformers[informerType] {
+				informers[informerType] = informer
+			}
+		}
+		return informers
+	}()
+
+	res := map[reflect.Type]bool{}
+	for informType, informer := range informers {
+		res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced)
+	}
+	return res
+}
+
+// InternalInformerFor returns the SharedIndexInformer for obj using an internal
+// client.
+func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer {
+	f.lock.Lock()
+	defer f.lock.Unlock()
+
+	informerType := reflect.TypeOf(obj)
+	informer, exists := f.informers[informerType]
+	if exists {
+		return informer
+	}
+
+	resyncPeriod, exists := f.customResync[informerType]
+	if !exists {
+		resyncPeriod = f.defaultResync
+	}
+
+	informer = newFunc(f.client, resyncPeriod)
+	f.informers[informerType] = informer
+
+	return informer
+}
+
+// SharedInformerFactory provides shared informers for resources in all known
+// API group versions.
+type SharedInformerFactory interface {
+	internalinterfaces.SharedInformerFactory
+	ForResource(resource schema.GroupVersionResource) (GenericInformer, error)
+	WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool
+
+	Dashboard() apis.Interface
+}
+
+func (f *sharedInformerFactory) Dashboard() apis.Interface {
+	return apis.New(f, f.namespace, f.tweakListOptions)
+}
diff --git a/src/app/backend/plugin/client/informers/externalversions/generic.go b/src/app/backend/plugin/client/informers/externalversions/generic.go
new file mode 100644
index 000000000..7c9620df3
--- /dev/null
+++ b/src/app/backend/plugin/client/informers/externalversions/generic.go
@@ -0,0 +1,60 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package externalversions
+
+import (
+	"fmt"
+
+	v1alpha1 "github.com/kubernetes/dashboard/src/app/backend/plugin/apis/v1alpha1"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// GenericInformer is type of SharedIndexInformer which will locate and delegate to other
+// sharedInformers based on type
+type GenericInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() cache.GenericLister
+}
+
+type genericInformer struct {
+	informer cache.SharedIndexInformer
+	resource schema.GroupResource
+}
+
+// Informer returns the SharedIndexInformer.
+func (f *genericInformer) Informer() cache.SharedIndexInformer {
+	return f.informer
+}
+
+// Lister returns the GenericLister.
+func (f *genericInformer) Lister() cache.GenericLister {
+	return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource)
+}
+
+// ForResource gives generic access to a shared informer of the matching type
+// TODO extend this to unknown resources with a client pool
+func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) {
+	switch resource {
+	// Group=dashboard.k8s.io, Version=v1alpha1
+	case v1alpha1.SchemeGroupVersion.WithResource("plugins"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Dashboard().V1alpha1().Plugins().Informer()}, nil
+
+	}
+
+	return nil, fmt.Errorf("no informer found for %v", resource)
+}
diff --git a/src/app/backend/plugin/client/informers/externalversions/internalinterfaces/factory_interfaces.go b/src/app/backend/plugin/client/informers/externalversions/internalinterfaces/factory_interfaces.go
new file mode 100644
index 000000000..ed335a0ce
--- /dev/null
+++ b/src/app/backend/plugin/client/informers/externalversions/internalinterfaces/factory_interfaces.go
@@ -0,0 +1,38 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package internalinterfaces
+
+import (
+	time "time"
+
+	versioned "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer.
+type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer
+
+// SharedInformerFactory a small interface to allow for adding an informer without an import cycle
+type SharedInformerFactory interface {
+	Start(stopCh <-chan struct{})
+	InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer
+}
+
+// TweakListOptionsFunc is a function that transforms a v1.ListOptions.
+type TweakListOptionsFunc func(*v1.ListOptions)
diff --git a/src/app/backend/plugin/client/listers/apis/v1alpha1/expansion_generated.go b/src/app/backend/plugin/client/listers/apis/v1alpha1/expansion_generated.go
new file mode 100644
index 000000000..dd8a8221e
--- /dev/null
+++ b/src/app/backend/plugin/client/listers/apis/v1alpha1/expansion_generated.go
@@ -0,0 +1,25 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// PluginListerExpansion allows custom methods to be added to
+// PluginLister.
+type PluginListerExpansion interface{}
+
+// PluginNamespaceListerExpansion allows custom methods to be added to
+// PluginNamespaceLister.
+type PluginNamespaceListerExpansion interface{}
diff --git a/src/app/backend/plugin/client/listers/apis/v1alpha1/plugin.go b/src/app/backend/plugin/client/listers/apis/v1alpha1/plugin.go
new file mode 100644
index 000000000..3e9578013
--- /dev/null
+++ b/src/app/backend/plugin/client/listers/apis/v1alpha1/plugin.go
@@ -0,0 +1,92 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	v1alpha1 "github.com/kubernetes/dashboard/src/app/backend/plugin/apis/v1alpha1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+)
+
+// PluginLister helps list Plugins.
+type PluginLister interface {
+	// List lists all Plugins in the indexer.
+	List(selector labels.Selector) (ret []*v1alpha1.Plugin, err error)
+	// Plugins returns an object that can list and get Plugins.
+	Plugins(namespace string) PluginNamespaceLister
+	PluginListerExpansion
+}
+
+// pluginLister implements the PluginLister interface.
+type pluginLister struct {
+	indexer cache.Indexer
+}
+
+// NewPluginLister returns a new PluginLister.
+func NewPluginLister(indexer cache.Indexer) PluginLister {
+	return &pluginLister{indexer: indexer}
+}
+
+// List lists all Plugins in the indexer.
+func (s *pluginLister) List(selector labels.Selector) (ret []*v1alpha1.Plugin, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1alpha1.Plugin))
+	})
+	return ret, err
+}
+
+// Plugins returns an object that can list and get Plugins.
+func (s *pluginLister) Plugins(namespace string) PluginNamespaceLister {
+	return pluginNamespaceLister{indexer: s.indexer, namespace: namespace}
+}
+
+// PluginNamespaceLister helps list and get Plugins.
+type PluginNamespaceLister interface {
+	// List lists all Plugins in the indexer for a given namespace.
+	List(selector labels.Selector) (ret []*v1alpha1.Plugin, err error)
+	// Get retrieves the Plugin from the indexer for a given namespace and name.
+	Get(name string) (*v1alpha1.Plugin, error)
+	PluginNamespaceListerExpansion
+}
+
+// pluginNamespaceLister implements the PluginNamespaceLister
+// interface.
+type pluginNamespaceLister struct {
+	indexer   cache.Indexer
+	namespace string
+}
+
+// List lists all Plugins in the indexer for a given namespace.
+func (s pluginNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.Plugin, err error) {
+	err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1alpha1.Plugin))
+	})
+	return ret, err
+}
+
+// Get retrieves the Plugin from the indexer for a given namespace and name.
+func (s pluginNamespaceLister) Get(name string) (*v1alpha1.Plugin, error) {
+	obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1alpha1.Resource("plugin"), name)
+	}
+	return obj.(*v1alpha1.Plugin), nil
+}
diff --git a/src/app/backend/plugin/detail.go b/src/app/backend/plugin/detail.go
new file mode 100644
index 000000000..8be7c65a6
--- /dev/null
+++ b/src/app/backend/plugin/detail.go
@@ -0,0 +1,34 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package plugin
+
+import (
+	pluginclientset "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+// GetPluginSource has the logic to get the actual plugin source code from information in Plugin.Spec
+func GetPluginSource(client pluginclientset.Interface, k8sClient kubernetes.Interface, ns string, name string) ([]byte, error) {
+	plugin, err := client.DashboardV1alpha1().Plugins(ns).Get(name, v1.GetOptions{})
+	if err != nil {
+		return nil, err
+	}
+	cfgMap, err := k8sClient.CoreV1().ConfigMaps(ns).Get(plugin.Spec.Source.ConfigMapRef.Name, v1.GetOptions{})
+	if err != nil {
+		return nil, err
+	}
+	return []byte(cfgMap.Data[plugin.Spec.Source.Filename]), nil
+}
diff --git a/src/app/backend/plugin/handler.go b/src/app/backend/plugin/handler.go
new file mode 100644
index 000000000..2b497795a
--- /dev/null
+++ b/src/app/backend/plugin/handler.go
@@ -0,0 +1,98 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package plugin
+
+import (
+	"net/http"
+	"path/filepath"
+	"strings"
+
+	"github.com/emicklei/go-restful"
+	clientapi "github.com/kubernetes/dashboard/src/app/backend/client/api"
+	"github.com/kubernetes/dashboard/src/app/backend/errors"
+)
+
+const (
+	contentTypeHeader = "Content-Type"
+	jsContentType     = "text/javascript; charset=utf-8"
+)
+
+// Handler manages all endpoints related to plugin use cases, such as list and get.
+type Handler struct {
+	cManager clientapi.ClientManager
+}
+
+// Install creates new endpoints for plugins. All information that any plugin would want
+// to expose by creating new endpoints should be kept here, i.e. plugin service might want to
+// create endpoint to list available proxy paths to another backend.
+//
+// By default, endpoint for getting and listing plugins is installed. It allows user
+// to list the installed plugins and get the source code for a plugin.
+func (h *Handler) Install(ws *restful.WebService) {
+	ws.Route(
+		ws.GET("/plugins/{namespace}").
+			To(h.handlePluginList).
+			Writes(PluginList{}))
+
+	ws.Route(
+		ws.GET("/plugin/{namespace}/{pluginName}").
+			To(h.servePluginSource))
+}
+
+// NewPluginHandler creates plugin.Handler.
+func NewPluginHandler(cManager clientapi.ClientManager) *Handler {
+	return &Handler{cManager: cManager}
+}
+
+func (h *Handler) handlePluginList(request *restful.Request, response *restful.Response) {
+	pluginClient, err := h.cManager.PluginClient(request)
+	if err != nil {
+		errors.HandleInternalError(response, err)
+		return
+	}
+	namespace := request.PathParameter("namespace")
+
+	result, err := GetPluginList(pluginClient, namespace)
+	if err != nil {
+		errors.HandleInternalError(response, err)
+		return
+	}
+	response.WriteHeaderAndEntity(http.StatusOK, result)
+}
+
+func (h *Handler) servePluginSource(request *restful.Request, response *restful.Response) {
+	pluginClient, err := h.cManager.PluginClient(request)
+	if err != nil {
+		errors.HandleInternalError(response, err)
+		return
+	}
+	k8sClient, err := h.cManager.Client(request)
+	if err != nil {
+		errors.HandleInternalError(response, err)
+		return
+	}
+	namespace := request.PathParameter("namespace")
+	// Removes .js extension if it's present
+	pluginName := request.PathParameter("pluginName")
+	name := strings.TrimSuffix(pluginName, filepath.Ext(pluginName))
+
+	result, err := GetPluginSource(pluginClient, k8sClient, namespace, name)
+	if err != nil {
+		errors.HandleInternalError(response, err)
+		return
+	}
+	response.AddHeader(contentTypeHeader, jsContentType)
+	response.Write(result)
+}
diff --git a/src/app/backend/plugin/list.go b/src/app/backend/plugin/list.go
new file mode 100644
index 000000000..0e7da21ec
--- /dev/null
+++ b/src/app/backend/plugin/list.go
@@ -0,0 +1,58 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package plugin
+
+import (
+	"fmt"
+
+	"github.com/kubernetes/dashboard/src/app/backend/api"
+	pluginclientset "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// PluginList holds only necessary information and is used to
+// map v1alpha1.PluginList to plugin.PluginList
+type PluginList struct {
+	ListMeta api.ListMeta `json:"listMeta"`
+	Plugins  []Plugin     `json:"plugins"`
+	Errors   []error      `json:"errors"`
+}
+
+// PluginList holds only necessary information and is used to
+// map v1alpha1.Plugin to plugin.Plugin
+type Plugin struct {
+	Name         string   `json:"name"`
+	Path         string   `json:"path"`
+	Dependencies []string `json:"dependencies"`
+}
+
+// GetPluginList returns all the registered plugins
+func GetPluginList(client pluginclientset.Interface, ns string) (*PluginList, error) {
+	pluginList, err := client.DashboardV1alpha1().Plugins(ns).List(v1.ListOptions{})
+	if err != nil {
+		return nil, err
+	}
+
+	result := &PluginList{Errors: []error{}, Plugins: []Plugin{}}
+	for _, plugin := range pluginList.Items {
+		result.Plugins = append(result.Plugins,
+			Plugin{
+				Name:         plugin.Name,
+				Path:         fmt.Sprintf("/plugin/%s/%s.js", plugin.Namespace, plugin.Name),
+				Dependencies: []string{}})
+		result.ListMeta.TotalItems++
+	}
+	return result, nil
+}


Test patch:
diff --git a/aio/test-resources/plugin-crd.yml b/aio/test-resources/plugin-crd.yml
new file mode 100644
index 000000000..86204c7c3
--- /dev/null
+++ b/aio/test-resources/plugin-crd.yml
@@ -0,0 +1,41 @@
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  name: plugins.dashboard.k8s.io
+spec:
+  group: dashboard.k8s.io
+  scope: Namespaced
+  versions:
+    - name: v1alpha1
+      served: true
+      storage: true
+  names:
+    kind: Plugin
+    plural: plugins
+    singular: plugin
+  validation:
+    openAPIV3Schema:
+      type: object
+      properties:
+        spec:
+          type: object
+          properties:
+            source:
+              type: object
+              properties:
+                configMapRef:
+                  type: object
+                  properties:
+                    name:
+                      type: string
+                  required:
+                    - name
+                filename:
+                  type: string
+              required:
+                - configMapRef
+                - filename
+          required:
+            - source
+      required:
+        - spec
diff --git a/aio/test-resources/plugin-test.yml b/aio/test-resources/plugin-test.yml
new file mode 100644
index 000000000..b7f6975b9
--- /dev/null
+++ b/aio/test-resources/plugin-test.yml
@@ -0,0 +1,25 @@
+# Copyright 2017 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# A manifest that creates a plugin resource.
+
+apiVersion: dashboard.k8s.io/v1alpha1
+kind: Plugin
+metadata:
+  name: plugin-test
+spec:
+  source:
+    configMapRef:
+      name: plugin-test-src
+    filename: plugin1.js
diff --git a/src/app/backend/auth/manager_test.go b/src/app/backend/auth/manager_test.go
index ba4095ace..484c656e3 100644
--- a/src/app/backend/auth/manager_test.go
+++ b/src/app/backend/auth/manager_test.go
@@ -26,6 +26,7 @@ import (
 	clientapi "github.com/kubernetes/dashboard/src/app/backend/client/api"
 	"github.com/kubernetes/dashboard/src/app/backend/errors"
 
+	pluginclientset "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned"
 	v1 "k8s.io/api/authorization/v1"
 	apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
 	"k8s.io/client-go/kubernetes"
@@ -51,6 +52,10 @@ func (self *fakeClientManager) APIExtensionsClient(req *restful.Request) (apiext
 	return nil, nil
 }
 
+func (self *fakeClientManager) PluginClient(req *restful.Request) (pluginclientset.Interface, error) {
+	return nil, nil
+}
+
 func (self *fakeClientManager) InsecureClient() kubernetes.Interface {
 	return nil
 }
diff --git a/src/app/backend/plugin/detail_test.go b/src/app/backend/plugin/detail_test.go
new file mode 100644
index 000000000..eec149a27
--- /dev/null
+++ b/src/app/backend/plugin/detail_test.go
@@ -0,0 +1,73 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package plugin
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/kubernetes/dashboard/src/app/backend/plugin/apis/v1alpha1"
+	fakePluginClientset "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned/fake"
+	coreV1 "k8s.io/api/core/v1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	fakeK8sClient "k8s.io/client-go/kubernetes/fake"
+)
+
+var srcData = "randomPluginSourceCode"
+
+func TestGetPluginSource(t *testing.T) {
+	ns := "default"
+	pluginName := "test-plugin"
+	filename := "plugin-test.js"
+	cfgMapName := "plugin-test-cfgMap"
+
+	pcs := fakePluginClientset.NewSimpleClientset()
+	cs := fakeK8sClient.NewSimpleClientset()
+
+	_, err := GetPluginSource(pcs, cs, ns, pluginName)
+	if err == nil {
+		t.Errorf("error 'plugins.dashboard.k8s.io \"%s\" not found' did not occur", pluginName)
+	}
+
+	_, _ = pcs.DashboardV1alpha1().Plugins(ns).Create(&v1alpha1.Plugin{
+		ObjectMeta: v1.ObjectMeta{Name: pluginName, Namespace: ns},
+		Spec: v1alpha1.PluginSpec{
+			Source: v1alpha1.Source{
+				ConfigMapRef: &coreV1.ConfigMapEnvSource{
+					LocalObjectReference: coreV1.LocalObjectReference{Name: cfgMapName},
+				},
+				Filename: filename}},
+	})
+
+	_, err = GetPluginSource(pcs, cs, ns, pluginName)
+	if err == nil {
+		t.Errorf("error 'configmaps \"%s\" not found' did not occur", cfgMapName)
+	}
+
+	_, _ = cs.CoreV1().ConfigMaps(ns).Create(&coreV1.ConfigMap{
+		ObjectMeta: v1.ObjectMeta{
+			Name: cfgMapName, Namespace: ns},
+		Data: map[string]string{filename: srcData},
+	})
+
+	data, err := GetPluginSource(pcs, cs, ns, pluginName)
+	if err != nil {
+		t.Errorf("error while fetching plugin source: %s", err)
+	}
+
+	if !bytes.Equal(data, []byte(srcData)) {
+		t.Error("bytes in configMap and bytes from GetPluginSource are different")
+	}
+}
diff --git a/src/app/backend/plugin/handler_test.go b/src/app/backend/plugin/handler_test.go
new file mode 100644
index 000000000..294262feb
--- /dev/null
+++ b/src/app/backend/plugin/handler_test.go
@@ -0,0 +1,31 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package plugin
+
+import (
+	"testing"
+
+	"github.com/emicklei/go-restful"
+)
+
+func TestIntegrationHandler_Install(t *testing.T) {
+	pHandler := NewPluginHandler(nil)
+	ws := new(restful.WebService)
+	pHandler.Install(ws)
+
+	if len(ws.Routes()) == 0 {
+		t.Error("Failed to install routes.")
+	}
+}
diff --git a/src/app/backend/plugin/list_test.go b/src/app/backend/plugin/list_test.go
new file mode 100644
index 000000000..5b858e3e7
--- /dev/null
+++ b/src/app/backend/plugin/list_test.go
@@ -0,0 +1,52 @@
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package plugin
+
+import (
+	"testing"
+
+	"github.com/kubernetes/dashboard/src/app/backend/plugin/apis/v1alpha1"
+	fakePluginClientset "github.com/kubernetes/dashboard/src/app/backend/plugin/client/clientset/versioned/fake"
+	coreV1 "k8s.io/api/core/v1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestGetPluginList(t *testing.T) {
+	ns := "default"
+	pluginName := "test-plugin"
+	filename := "plugin-test.js"
+	cfgMapName := "plugin-test-cfgMap"
+
+	pcs := fakePluginClientset.NewSimpleClientset()
+
+	_, _ = pcs.DashboardV1alpha1().Plugins(ns).Create(&v1alpha1.Plugin{
+		ObjectMeta: v1.ObjectMeta{Name: pluginName, Namespace: ns},
+		Spec: v1alpha1.PluginSpec{
+			Source: v1alpha1.Source{
+				ConfigMapRef: &coreV1.ConfigMapEnvSource{
+					LocalObjectReference: coreV1.LocalObjectReference{Name: cfgMapName},
+				},
+				Filename: filename}},
+	})
+
+	data, err := GetPluginList(pcs, ns)
+	if err != nil {
+		t.Errorf("error while fetching plugins: %s", err)
+	}
+
+	if data.ListMeta.TotalItems != 1 {
+		t.Errorf("there should be one plugin registered, got %d", data.ListMeta.TotalItems)
+	}
+}