Commit 0f5990f4 authored by Adam Reese's avatar Adam Reese

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
parent 9c3f883e
...@@ -25,6 +25,10 @@ import ( ...@@ -25,6 +25,10 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/grpc" "google.golang.org/grpc"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/helm/pkg/kube"
) )
const ( const (
...@@ -145,8 +149,8 @@ func setupConnection(c *cobra.Command, args []string) error { ...@@ -145,8 +149,8 @@ func setupConnection(c *cobra.Command, args []string) error {
} }
func teardown() { func teardown() {
if tunnel != nil { if tillerTunnel != nil {
tunnel.Close() tillerTunnel.Close()
} }
} }
...@@ -176,3 +180,17 @@ func prettyError(err error) error { ...@@ -176,3 +180,17 @@ func prettyError(err error) error {
func homePath() string { func homePath() string {
return os.ExpandEnv(helmHome) 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 ( ...@@ -21,9 +21,10 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"strings"
"github.com/spf13/cobra" "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/helmpath"
"k8s.io/helm/cmd/helm/installer" "k8s.io/helm/cmd/helm/installer"
...@@ -47,6 +48,7 @@ type initCmd struct { ...@@ -47,6 +48,7 @@ type initCmd struct {
clientOnly bool clientOnly bool
out io.Writer out io.Writer
home helmpath.Home home helmpath.Home
kubeClient unversioned.DeploymentsNamespacer
} }
func newInitCmd(out io.Writer) *cobra.Command { func newInitCmd(out io.Writer) *cobra.Command {
...@@ -77,8 +79,15 @@ func (i *initCmd) run() error { ...@@ -77,8 +79,15 @@ func (i *initCmd) run() error {
} }
if !i.clientOnly { if !i.clientOnly {
if err := installer.Install(tillerNamespace, i.image, flagDebug); err != nil { if i.kubeClient == nil {
if !strings.Contains(err.Error(), `"tiller-deploy" already exists`) { _, 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) 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.)") fmt.Fprintln(i.out, "Warning: Tiller is already installed in the cluster. (Use --client-only to suppress this message.)")
......
...@@ -20,11 +20,83 @@ import ( ...@@ -20,11 +20,83 @@ import (
"bytes" "bytes"
"io/ioutil" "io/ioutil"
"os" "os"
"strings"
"testing" "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" "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) { func TestEnsureHome(t *testing.T) {
home, err := ioutil.TempDir("", "helm_home") home, err := ioutil.TempDir("", "helm_home")
if err != nil { if err != nil {
......
...@@ -18,14 +18,12 @@ package installer // import "k8s.io/helm/cmd/helm/installer" ...@@ -18,14 +18,12 @@ package installer // import "k8s.io/helm/cmd/helm/installer"
import ( import (
"fmt" "fmt"
"strings"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/util/intstr" "k8s.io/kubernetes/pkg/util/intstr"
"k8s.io/helm/pkg/kube"
"k8s.io/helm/pkg/version" "k8s.io/helm/pkg/version"
) )
...@@ -37,38 +35,12 @@ const defaultImage = "gcr.io/kubernetes-helm/tiller" ...@@ -37,38 +35,12 @@ const defaultImage = "gcr.io/kubernetes-helm/tiller"
// command failed. // command failed.
// //
// If verbose is true, this will print the manifest to stdout. // If verbose is true, this will print the manifest to stdout.
func Install(namespace, image string, verbose bool) error { func Install(client unversioned.DeploymentsNamespacer, 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
}
}
if image == "" { if image == "" {
// strip git sha off version image = fmt.Sprintf("%s:%s", defaultImage, version.Version)
tag := strings.Split(version.Version, "+")[0]
image = fmt.Sprintf("%s:%s", defaultImage, tag)
} }
obj := generateDeployment(image)
rc := generateDeployment(image) _, err := client.Deployments(namespace).Create(obj)
_, err = c.Deployments(namespace).Create(rc)
return err return err
} }
...@@ -125,12 +97,3 @@ func generateDeployment(image string) *extensions.Deployment { ...@@ -125,12 +97,3 @@ func generateDeployment(image string) *extensions.Deployment {
} }
return d return d
} }
func generateNamespace(namespace string) *api.Namespace {
return &api.Namespace{
ObjectMeta: api.ObjectMeta{
Name: namespace,
Labels: generateLabels(map[string]string{"name": "helm-namespace"}),
},
}
}
/*
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 ( ...@@ -21,27 +21,16 @@ import (
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
"k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/labels"
"k8s.io/helm/pkg/kube" "k8s.io/helm/pkg/kube"
) )
// TODO refactor out this global var // TODO refactor out this global var
var tunnel *kube.Tunnel var tillerTunnel *kube.Tunnel
func getKubeConfig(context string) clientcmd.ClientConfig {
rules := clientcmd.NewDefaultClientConfigLoadingRules()
overrides := &clientcmd.ConfigOverrides{}
if context != "" {
overrides.CurrentContext = context
}
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides)
}
func newTillerPortForwarder(namespace, context string) (*kube.Tunnel, error) { func newTillerPortForwarder(namespace, context string) (*kube.Tunnel, error) {
kc := kube.New(getKubeConfig(context)) config, client, err := getKubeClient(context)
client, err := kc.Client()
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -51,7 +40,8 @@ func newTillerPortForwarder(namespace, context string) (*kube.Tunnel, error) { ...@@ -51,7 +40,8 @@ func newTillerPortForwarder(namespace, context string) (*kube.Tunnel, error) {
return nil, err return nil, err
} }
const tillerPort = 44134 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) { func getTillerPodName(client unversioned.PodsNamespacer, namespace string) (string, error) {
......
hash: 0b56505a7d2b0bde1a8aba9c4ac52ef18ea1eae6d46157db598e5a1051b64cf5 hash: e04a956fc7be01bd1a2450cdc0000ed25394da383f0c7a7e057a9377bd832c1e
updated: 2016-10-11T12:54:05.869559929-06:00 updated: 2016-10-13T12:28:13.380298639-07:00
imports: imports:
- name: bitbucket.org/ww/goautoneg - name: bitbucket.org/ww/goautoneg
version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675
...@@ -434,7 +434,7 @@ imports: ...@@ -434,7 +434,7 @@ imports:
- 1.4/tools/metrics - 1.4/tools/metrics
- 1.4/transport - 1.4/transport
- name: k8s.io/kubernetes - name: k8s.io/kubernetes
version: ef16c3f8079df0654c8336741134ba142846ec13 version: fc3dab7de68c15de3421896dd051c2f127fb64ab
subpackages: subpackages:
- federation/apis/federation - federation/apis/federation
- federation/apis/federation/install - federation/apis/federation/install
...@@ -446,7 +446,6 @@ imports: ...@@ -446,7 +446,6 @@ imports:
- pkg/api - pkg/api
- pkg/api/annotations - pkg/api/annotations
- pkg/api/endpoints - pkg/api/endpoints
- pkg/api/error
- pkg/api/errors - pkg/api/errors
- pkg/api/install - pkg/api/install
- pkg/api/meta - pkg/api/meta
......
...@@ -25,25 +25,26 @@ import: ...@@ -25,25 +25,26 @@ import:
version: ~1.4.1 version: ~1.4.1
subpackages: subpackages:
- pkg/api - pkg/api
- pkg/api/meta - pkg/api/errors
- pkg/api/error
- pkg/api/unversioned - pkg/api/unversioned
- pkg/apimachinery/registered - pkg/apimachinery/registered
- pkg/apis/batch
- pkg/apis/extensions
- pkg/client/restclient - pkg/client/restclient
- pkg/client/unversioned - pkg/client/unversioned
- pkg/apis/batch
- pkg/client/unversioned/clientcmd - pkg/client/unversioned/clientcmd
- pkg/client/unversioned/fake
- pkg/client/unversioned/portforward - pkg/client/unversioned/portforward
- pkg/client/unversioned/remotecommand - pkg/client/unversioned/remotecommand
- pkg/client/unversioned/testclient
- pkg/kubectl - pkg/kubectl
- pkg/kubectl/cmd/util - pkg/kubectl/cmd/util
- pkg/kubectl/resource - pkg/kubectl/resource
- pkg/labels - pkg/labels
- pkg/runtime - pkg/runtime
- pkg/watch - pkg/util/intstr
- pkg/util/strategicpatch - pkg/util/strategicpatch
- pkg/util/yaml - pkg/util/yaml
- pkg/watch
- package: github.com/gosuri/uitable - package: github.com/gosuri/uitable
- package: github.com/asaskevich/govalidator - package: github.com/asaskevich/govalidator
version: ^4.0.0 version: ^4.0.0
......
/*
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. ...@@ -17,11 +17,13 @@ limitations under the License.
package kube package kube
import ( import (
"bytes"
"fmt" "fmt"
"io"
"io/ioutil"
"net" "net"
"strconv" "strconv"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/client/unversioned/portforward" "k8s.io/kubernetes/pkg/client/unversioned/portforward"
"k8s.io/kubernetes/pkg/client/unversioned/remotecommand" "k8s.io/kubernetes/pkg/client/unversioned/remotecommand"
) )
...@@ -30,8 +32,27 @@ import ( ...@@ -30,8 +32,27 @@ import (
type Tunnel struct { type Tunnel struct {
Local int Local int
Remote int Remote int
Namespace string
PodName string
Out io.Writer
stopChan chan struct{} stopChan chan struct{}
readyChan 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 // Close disconnects a tunnel connection
...@@ -41,48 +62,31 @@ func (t *Tunnel) Close() { ...@@ -41,48 +62,31 @@ func (t *Tunnel) Close() {
} }
// ForwardPort opens a tunnel to a kubernetes pod // ForwardPort opens a tunnel to a kubernetes pod
func (c *Client) ForwardPort(namespace, podName string, remote int) (*Tunnel, error) { func (t *Tunnel) ForwardPort() error {
client, err := c.Client() // Build a url to the portforward endpoint
if err != nil {
return nil, err
}
config, err := c.ClientConfig()
if err != nil {
return nil, err
}
// Build a url to the portforward endpoing
// example: http://localhost:8080/api/v1/namespaces/helm/pods/tiller-deploy-9itlq/portforward // example: http://localhost:8080/api/v1/namespaces/helm/pods/tiller-deploy-9itlq/portforward
u := client.RESTClient.Post(). u := t.client.Post().
Resource("pods"). Resource("pods").
Namespace(namespace). Namespace(t.Namespace).
Name(podName). Name(t.PodName).
SubResource("portforward").URL() SubResource("portforward").URL()
dialer, err := remotecommand.NewExecutor(config, "POST", u) dialer, err := remotecommand.NewExecutor(t.config, "POST", u)
if err != nil { if err != nil {
return nil, err return err
} }
local, err := getAvailablePort() local, err := getAvailablePort()
if err != nil { if err != nil {
return nil, err return fmt.Errorf("could not find an available port: %s", err)
}
t := &Tunnel{
Local: local,
Remote: remote,
stopChan: make(chan struct{}, 1),
readyChan: make(chan struct{}, 1),
} }
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, t.Out, t.Out)
pf, err := portforward.New(dialer, ports, t.stopChan, t.readyChan, &b, &b)
if err != nil { if err != nil {
return nil, err return err
} }
errChan := make(chan error) errChan := make(chan error)
...@@ -92,9 +96,9 @@ func (c *Client) ForwardPort(namespace, podName string, remote int) (*Tunnel, er ...@@ -92,9 +96,9 @@ func (c *Client) ForwardPort(namespace, podName string, remote int) (*Tunnel, er
select { select {
case err = <-errChan: 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: case <-pf.Ready:
return t, nil return nil
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment