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
pull/1359/head
Adam Reese 8 years ago
parent 9c3f883ec3
commit 0f5990f4cd

@ -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"}),
},
}
}

@ -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

@ -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

@ -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

@ -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
}
t := &Tunnel{
Local: local,
Remote: remote,
stopChan: make(chan struct{}, 1),
readyChan: make(chan struct{}, 1),
return fmt.Errorf("could not find an available port: %s", err)
}
t.Local = local
ports := []string{fmt.Sprintf("%d:%d", local, remote)}
ports := []string{fmt.Sprintf("%d:%d", t.Local, t.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…
Cancel
Save