mirror of
https://github.com/helm/helm.git
synced 2026-05-28 04:35:48 -04:00
feat(helm): add kubeconfig context switching to init command
- decouple tunnel from kube client - add context switching for init cmd - add unit tests for installer and init command - refactor installer and remove unused code
This commit is contained in:
parent
9c3f883ec3
commit
0f5990f4cd
10 changed files with 238 additions and 102 deletions
|
|
@ -25,6 +25,10 @@ import (
|
|||
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"k8s.io/kubernetes/pkg/client/restclient"
|
||||
"k8s.io/kubernetes/pkg/client/unversioned"
|
||||
|
||||
"k8s.io/helm/pkg/kube"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -145,8 +149,8 @@ func setupConnection(c *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
func teardown() {
|
||||
if tunnel != nil {
|
||||
tunnel.Close()
|
||||
if tillerTunnel != nil {
|
||||
tillerTunnel.Close()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -176,3 +180,17 @@ func prettyError(err error) error {
|
|||
func homePath() string {
|
||||
return os.ExpandEnv(helmHome)
|
||||
}
|
||||
|
||||
// getKubeClient is a convenience method for creating kubernetes config and client
|
||||
// for a given kubeconfig context
|
||||
func getKubeClient(context string) (*restclient.Config, *unversioned.Client, error) {
|
||||
config, err := kube.GetConfig(context).ClientConfig()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not get kubernetes config for context '%s': %s", context, err)
|
||||
}
|
||||
client, err := unversioned.New(config)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not get kubernetes client: %s", err)
|
||||
}
|
||||
return config, client, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,10 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
kerrors "k8s.io/kubernetes/pkg/api/errors"
|
||||
"k8s.io/kubernetes/pkg/client/unversioned"
|
||||
|
||||
"k8s.io/helm/cmd/helm/helmpath"
|
||||
"k8s.io/helm/cmd/helm/installer"
|
||||
|
|
@ -47,6 +48,7 @@ type initCmd struct {
|
|||
clientOnly bool
|
||||
out io.Writer
|
||||
home helmpath.Home
|
||||
kubeClient unversioned.DeploymentsNamespacer
|
||||
}
|
||||
|
||||
func newInitCmd(out io.Writer) *cobra.Command {
|
||||
|
|
@ -77,8 +79,15 @@ func (i *initCmd) run() error {
|
|||
}
|
||||
|
||||
if !i.clientOnly {
|
||||
if err := installer.Install(tillerNamespace, i.image, flagDebug); err != nil {
|
||||
if !strings.Contains(err.Error(), `"tiller-deploy" already exists`) {
|
||||
if i.kubeClient == nil {
|
||||
_, c, err := getKubeClient(kubeContext)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get kubernetes client: %s", err)
|
||||
}
|
||||
i.kubeClient = c
|
||||
}
|
||||
if err := installer.Install(i.kubeClient, tillerNamespace, i.image, flagDebug); err != nil {
|
||||
if !kerrors.IsAlreadyExists(err) {
|
||||
return fmt.Errorf("error installing: %s", err)
|
||||
}
|
||||
fmt.Fprintln(i.out, "Warning: Tiller is already installed in the cluster. (Use --client-only to suppress this message.)")
|
||||
|
|
|
|||
|
|
@ -20,11 +20,83 @@ import (
|
|||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/errors"
|
||||
"k8s.io/kubernetes/pkg/client/unversioned/testclient"
|
||||
"k8s.io/kubernetes/pkg/runtime"
|
||||
|
||||
"k8s.io/helm/cmd/helm/helmpath"
|
||||
)
|
||||
|
||||
func TestInitCmd(t *testing.T) {
|
||||
home, err := ioutil.TempDir("", "helm_home")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(home)
|
||||
|
||||
var buf bytes.Buffer
|
||||
fake := testclient.Fake{}
|
||||
cmd := &initCmd{out: &buf, home: helmpath.Home(home), kubeClient: fake.Extensions()}
|
||||
if err := cmd.run(); err != nil {
|
||||
t.Errorf("expected error: %v", err)
|
||||
}
|
||||
actions := fake.Actions()
|
||||
if action, ok := actions[0].(testclient.CreateAction); !ok || action.GetResource() != "deployments" {
|
||||
t.Errorf("unexpected action: %v, expected create deployment", actions[0])
|
||||
}
|
||||
expected := "Tiller (the helm server side component) has been installed into your Kubernetes Cluster."
|
||||
if !strings.Contains(buf.String(), expected) {
|
||||
t.Errorf("expected %q, got %q", expected, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitCmd_exsits(t *testing.T) {
|
||||
home, err := ioutil.TempDir("", "helm_home")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(home)
|
||||
|
||||
var buf bytes.Buffer
|
||||
fake := testclient.Fake{}
|
||||
fake.AddReactor("*", "*", func(action testclient.Action) (bool, runtime.Object, error) {
|
||||
return true, nil, errors.NewAlreadyExists(api.Resource("deployments"), "1")
|
||||
})
|
||||
cmd := &initCmd{out: &buf, home: helmpath.Home(home), kubeClient: fake.Extensions()}
|
||||
if err := cmd.run(); err != nil {
|
||||
t.Errorf("expected error: %v", err)
|
||||
}
|
||||
expected := "Warning: Tiller is already installed in the cluster. (Use --client-only to suppress this message.)"
|
||||
if !strings.Contains(buf.String(), expected) {
|
||||
t.Errorf("expected %q, got %q", expected, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitCmd_clientOnly(t *testing.T) {
|
||||
home, err := ioutil.TempDir("", "helm_home")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(home)
|
||||
|
||||
var buf bytes.Buffer
|
||||
fake := testclient.Fake{}
|
||||
cmd := &initCmd{out: &buf, home: helmpath.Home(home), kubeClient: fake.Extensions(), clientOnly: true}
|
||||
if err := cmd.run(); err != nil {
|
||||
t.Errorf("expected error: %v", err)
|
||||
}
|
||||
if len(fake.Actions()) != 0 {
|
||||
t.Error("expected client call")
|
||||
}
|
||||
expected := "Not installing tiller due to 'client-only' flag having been set"
|
||||
if !strings.Contains(buf.String(), expected) {
|
||||
t.Errorf("expected %q, got %q", expected, buf.String())
|
||||
}
|
||||
}
|
||||
func TestEnsureHome(t *testing.T) {
|
||||
home, err := ioutil.TempDir("", "helm_home")
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -18,14 +18,12 @@ package installer // import "k8s.io/helm/cmd/helm/installer"
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/errors"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
"k8s.io/kubernetes/pkg/client/unversioned"
|
||||
"k8s.io/kubernetes/pkg/util/intstr"
|
||||
|
||||
"k8s.io/helm/pkg/kube"
|
||||
"k8s.io/helm/pkg/version"
|
||||
)
|
||||
|
||||
|
|
@ -37,38 +35,12 @@ const defaultImage = "gcr.io/kubernetes-helm/tiller"
|
|||
// command failed.
|
||||
//
|
||||
// If verbose is true, this will print the manifest to stdout.
|
||||
func Install(namespace, image string, verbose bool) error {
|
||||
kc := kube.New(nil)
|
||||
|
||||
if namespace == "" {
|
||||
ns, _, err := kc.DefaultNamespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
namespace = ns
|
||||
}
|
||||
|
||||
c, err := kc.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ns := generateNamespace(namespace)
|
||||
if _, err := c.Namespaces().Create(ns); err != nil {
|
||||
if !errors.IsAlreadyExists(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func Install(client unversioned.DeploymentsNamespacer, namespace, image string, verbose bool) error {
|
||||
if image == "" {
|
||||
// strip git sha off version
|
||||
tag := strings.Split(version.Version, "+")[0]
|
||||
image = fmt.Sprintf("%s:%s", defaultImage, tag)
|
||||
image = fmt.Sprintf("%s:%s", defaultImage, version.Version)
|
||||
}
|
||||
|
||||
rc := generateDeployment(image)
|
||||
|
||||
_, err = c.Deployments(namespace).Create(rc)
|
||||
obj := generateDeployment(image)
|
||||
_, err := client.Deployments(namespace).Create(obj)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -125,12 +97,3 @@ func generateDeployment(image string) *extensions.Deployment {
|
|||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func generateNamespace(namespace string) *api.Namespace {
|
||||
return &api.Namespace{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: namespace,
|
||||
Labels: generateLabels(map[string]string{"name": "helm-namespace"}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
49
cmd/helm/installer/install_test.go
Normal file
49
cmd/helm/installer/install_test.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors 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.
|
||||
*/
|
||||
|
||||
package installer // import "k8s.io/helm/cmd/helm/installer"
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
"k8s.io/kubernetes/pkg/client/unversioned/testclient"
|
||||
"k8s.io/kubernetes/pkg/runtime"
|
||||
)
|
||||
|
||||
func TestInstall(t *testing.T) {
|
||||
image := "gcr.io/kubernetes-helm/tiller:v2.0.0"
|
||||
|
||||
fake := testclient.Fake{}
|
||||
fake.AddReactor("create", "deployments", func(action testclient.Action) (bool, runtime.Object, error) {
|
||||
obj := action.(testclient.CreateAction).GetObject().(*extensions.Deployment)
|
||||
l := obj.GetLabels()
|
||||
if reflect.DeepEqual(l, map[string]string{"app": "helm"}) {
|
||||
t.Errorf("expected labels = '', got '%s'", l)
|
||||
}
|
||||
i := obj.Spec.Template.Spec.Containers[0].Image
|
||||
if i != image {
|
||||
t.Errorf("expected image = '%s', got '%s'", image, i)
|
||||
}
|
||||
return true, obj, nil
|
||||
})
|
||||
|
||||
err := Install(fake.Extensions(), "default", image, false)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %#+v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -21,27 +21,16 @@ import (
|
|||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/client/unversioned"
|
||||
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
|
||||
"k8s.io/kubernetes/pkg/labels"
|
||||
|
||||
"k8s.io/helm/pkg/kube"
|
||||
)
|
||||
|
||||
// TODO refactor out this global var
|
||||
var tunnel *kube.Tunnel
|
||||
|
||||
func getKubeConfig(context string) clientcmd.ClientConfig {
|
||||
rules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
overrides := &clientcmd.ConfigOverrides{}
|
||||
if context != "" {
|
||||
overrides.CurrentContext = context
|
||||
}
|
||||
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides)
|
||||
}
|
||||
var tillerTunnel *kube.Tunnel
|
||||
|
||||
func newTillerPortForwarder(namespace, context string) (*kube.Tunnel, error) {
|
||||
kc := kube.New(getKubeConfig(context))
|
||||
client, err := kc.Client()
|
||||
config, client, err := getKubeClient(context)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -51,7 +40,8 @@ func newTillerPortForwarder(namespace, context string) (*kube.Tunnel, error) {
|
|||
return nil, err
|
||||
}
|
||||
const tillerPort = 44134
|
||||
return kc.ForwardPort(namespace, podName, tillerPort)
|
||||
t := kube.NewTunnel(client.RESTClient, config, namespace, podName, tillerPort)
|
||||
return t, t.ForwardPort()
|
||||
}
|
||||
|
||||
func getTillerPodName(client unversioned.PodsNamespacer, namespace string) (string, error) {
|
||||
|
|
|
|||
7
glide.lock
generated
7
glide.lock
generated
|
|
@ -1,5 +1,5 @@
|
|||
hash: 0b56505a7d2b0bde1a8aba9c4ac52ef18ea1eae6d46157db598e5a1051b64cf5
|
||||
updated: 2016-10-11T12:54:05.869559929-06:00
|
||||
hash: e04a956fc7be01bd1a2450cdc0000ed25394da383f0c7a7e057a9377bd832c1e
|
||||
updated: 2016-10-13T12:28:13.380298639-07:00
|
||||
imports:
|
||||
- name: bitbucket.org/ww/goautoneg
|
||||
version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675
|
||||
|
|
@ -434,7 +434,7 @@ imports:
|
|||
- 1.4/tools/metrics
|
||||
- 1.4/transport
|
||||
- name: k8s.io/kubernetes
|
||||
version: ef16c3f8079df0654c8336741134ba142846ec13
|
||||
version: fc3dab7de68c15de3421896dd051c2f127fb64ab
|
||||
subpackages:
|
||||
- federation/apis/federation
|
||||
- federation/apis/federation/install
|
||||
|
|
@ -446,7 +446,6 @@ imports:
|
|||
- pkg/api
|
||||
- pkg/api/annotations
|
||||
- pkg/api/endpoints
|
||||
- pkg/api/error
|
||||
- pkg/api/errors
|
||||
- pkg/api/install
|
||||
- pkg/api/meta
|
||||
|
|
|
|||
11
glide.yaml
11
glide.yaml
|
|
@ -25,25 +25,26 @@ import:
|
|||
version: ~1.4.1
|
||||
subpackages:
|
||||
- pkg/api
|
||||
- pkg/api/meta
|
||||
- pkg/api/error
|
||||
- pkg/api/errors
|
||||
- pkg/api/unversioned
|
||||
- pkg/apimachinery/registered
|
||||
- pkg/apis/batch
|
||||
- pkg/apis/extensions
|
||||
- pkg/client/restclient
|
||||
- pkg/client/unversioned
|
||||
- pkg/apis/batch
|
||||
- pkg/client/unversioned/clientcmd
|
||||
- pkg/client/unversioned/fake
|
||||
- pkg/client/unversioned/portforward
|
||||
- pkg/client/unversioned/remotecommand
|
||||
- pkg/client/unversioned/testclient
|
||||
- pkg/kubectl
|
||||
- pkg/kubectl/cmd/util
|
||||
- pkg/kubectl/resource
|
||||
- pkg/labels
|
||||
- pkg/runtime
|
||||
- pkg/watch
|
||||
- pkg/util/intstr
|
||||
- pkg/util/strategicpatch
|
||||
- pkg/util/yaml
|
||||
- pkg/watch
|
||||
- package: github.com/gosuri/uitable
|
||||
- package: github.com/asaskevich/govalidator
|
||||
version: ^4.0.0
|
||||
|
|
|
|||
31
pkg/kube/config.go
Normal file
31
pkg/kube/config.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors 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.
|
||||
*/
|
||||
|
||||
package kube // import "k8s.io/helm/pkg/kube"
|
||||
|
||||
import (
|
||||
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
|
||||
)
|
||||
|
||||
// GetConfig returns a kubernetes client config for a given context.
|
||||
func GetConfig(context string) clientcmd.ClientConfig {
|
||||
rules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
overrides := &clientcmd.ConfigOverrides{}
|
||||
if context != "" {
|
||||
overrides.CurrentContext = context
|
||||
}
|
||||
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides)
|
||||
}
|
||||
|
|
@ -17,11 +17,13 @@ limitations under the License.
|
|||
package kube
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"k8s.io/kubernetes/pkg/client/restclient"
|
||||
"k8s.io/kubernetes/pkg/client/unversioned/portforward"
|
||||
"k8s.io/kubernetes/pkg/client/unversioned/remotecommand"
|
||||
)
|
||||
|
|
@ -30,8 +32,27 @@ import (
|
|||
type Tunnel struct {
|
||||
Local int
|
||||
Remote int
|
||||
Namespace string
|
||||
PodName string
|
||||
Out io.Writer
|
||||
stopChan chan struct{}
|
||||
readyChan chan struct{}
|
||||
config *restclient.Config
|
||||
client *restclient.RESTClient
|
||||
}
|
||||
|
||||
// NewTunnel creates a new tunnel
|
||||
func NewTunnel(client *restclient.RESTClient, config *restclient.Config, namespace, podName string, remote int) *Tunnel {
|
||||
return &Tunnel{
|
||||
config: config,
|
||||
client: client,
|
||||
Namespace: namespace,
|
||||
PodName: podName,
|
||||
Remote: remote,
|
||||
stopChan: make(chan struct{}, 1),
|
||||
readyChan: make(chan struct{}, 1),
|
||||
Out: ioutil.Discard,
|
||||
}
|
||||
}
|
||||
|
||||
// Close disconnects a tunnel connection
|
||||
|
|
@ -41,48 +62,31 @@ func (t *Tunnel) Close() {
|
|||
}
|
||||
|
||||
// ForwardPort opens a tunnel to a kubernetes pod
|
||||
func (c *Client) ForwardPort(namespace, podName string, remote int) (*Tunnel, error) {
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := c.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build a url to the portforward endpoing
|
||||
func (t *Tunnel) ForwardPort() error {
|
||||
// Build a url to the portforward endpoint
|
||||
// example: http://localhost:8080/api/v1/namespaces/helm/pods/tiller-deploy-9itlq/portforward
|
||||
u := client.RESTClient.Post().
|
||||
u := t.client.Post().
|
||||
Resource("pods").
|
||||
Namespace(namespace).
|
||||
Name(podName).
|
||||
Namespace(t.Namespace).
|
||||
Name(t.PodName).
|
||||
SubResource("portforward").URL()
|
||||
|
||||
dialer, err := remotecommand.NewExecutor(config, "POST", u)
|
||||
dialer, err := remotecommand.NewExecutor(t.config, "POST", u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
local, err := getAvailablePort()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return fmt.Errorf("could not find an available port: %s", err)
|
||||
}
|
||||
t.Local = local
|
||||
|
||||
t := &Tunnel{
|
||||
Local: local,
|
||||
Remote: remote,
|
||||
stopChan: make(chan struct{}, 1),
|
||||
readyChan: make(chan struct{}, 1),
|
||||
}
|
||||
ports := []string{fmt.Sprintf("%d:%d", t.Local, t.Remote)}
|
||||
|
||||
ports := []string{fmt.Sprintf("%d:%d", local, remote)}
|
||||
|
||||
var b bytes.Buffer
|
||||
pf, err := portforward.New(dialer, ports, t.stopChan, t.readyChan, &b, &b)
|
||||
pf, err := portforward.New(dialer, ports, t.stopChan, t.readyChan, t.Out, t.Out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
errChan := make(chan error)
|
||||
|
|
@ -92,9 +96,9 @@ func (c *Client) ForwardPort(namespace, podName string, remote int) (*Tunnel, er
|
|||
|
||||
select {
|
||||
case err = <-errChan:
|
||||
return t, fmt.Errorf("Error forwarding ports: %v\n", err)
|
||||
return fmt.Errorf("Error forwarding ports: %v\n", err)
|
||||
case <-pf.Ready:
|
||||
return t, nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue