Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(kubernetes): let kubectl handle namespaces #208

Merged
merged 5 commits into from
Feb 12, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions pkg/kubernetes/client/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package client

import (
"os"
"os/exec"
"strings"

"github.com/grafana/tanka/pkg/kubernetes/manifest"
Expand All @@ -23,10 +22,7 @@ func (k Kubectl) Apply(data manifest.List, opts ApplyOpts) error {
}

func (k Kubectl) apply(data manifest.List, opts ApplyOpts) error {
argv := []string{"apply",
"--context", k.context.Get("name").MustStr(),
"-f", "-",
}
argv := []string{"-f", "-"}
if opts.Force {
argv = append(argv, "--force")
}
Expand All @@ -35,7 +31,7 @@ func (k Kubectl) apply(data manifest.List, opts ApplyOpts) error {
argv = append(argv, "--validate=false")
}

cmd := exec.Command("kubectl", argv...)
cmd := k.ctl("apply", argv...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

Expand Down
36 changes: 34 additions & 2 deletions pkg/kubernetes/client/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@ import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/pkg/errors"
"github.com/stretchr/objx"
funk "github.com/thoas/go-funk"
)

// setupContext uses `kubectl config view` to obtain the KUBECONFIG and extracts the correct context from it
func (k *Kubectl) setupContext() error {
// setupContext makes sure the kubectl client is set up to use the correct
// context for the cluster IP:
// - find a context that matches the IP
// - create a patch for it to set the default namespace
func (k *Kubectl) setupContext(namespace string) error {
if k.context != nil {
return nil
}
Expand All @@ -23,9 +29,35 @@ func (k *Kubectl) setupContext() error {
if err != nil {
return err
}

nsPatch, err := writeNamespacePatch(k.context, namespace)
if err != nil {
return errors.Wrap(err, "creating $KUBECONFIG patch for default namespace")
}
k.nsPatch = nsPatch

return nil
}

func writeNamespacePatch(context objx.Map, namespace string) (string, error) {
context.Set("context.namespace", namespace)

kubectx := map[string]interface{}{
"contexts": []interface{}{context},
}
out, err := json.Marshal(kubectx)
if err != nil {
return "", err
}

f := filepath.Join(os.TempDir(), "tk-kubectx-namespace.yaml")
if err := ioutil.WriteFile(f, []byte(out), 0644); err != nil {
return "", err
}

return f, nil
}

// Kubeconfig returns the merged $KUBECONFIG of the host
func Kubeconfig() (map[string]interface{}, error) {
cmd := exec.Command("kubectl", "config", "view", "-o", "json")
Expand Down
6 changes: 2 additions & 4 deletions pkg/kubernetes/client/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ func (k Kubectl) DeleteByLabels(namespace string, labels map[string]interface{},
}

func (k Kubectl) delete(namespace string, sel []string, opts DeleteOpts) error {
argv := append([]string{"delete",
"-n", namespace,
"--context", k.context.Get("name").MustStr(),
}, sel...)
argv := append([]string{"-n", namespace}, sel...)
k.ctl("delete", argv...)

if opts.Force {
argv = append(argv, "--force")
Expand Down
9 changes: 3 additions & 6 deletions pkg/kubernetes/client/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,7 @@ func (k Kubectl) DiffServerSide(data manifest.List) (*string, error) {
}

ready, missing := separateMissingNamespace(data, ns)
argv := []string{"diff",
"--context", k.context.Get("name").MustStr(),
"-f", "-",
}
cmd := exec.Command("kubectl", argv...)
cmd := k.ctl("diff", "-f", "-")

raw := bytes.Buffer{}
cmd.Stdout = &raw
Expand Down Expand Up @@ -58,7 +54,8 @@ func (k Kubectl) DiffServerSide(data manifest.List) (*string, error) {

func separateMissingNamespace(in manifest.List, exists map[string]bool) (ready, missingNamespace manifest.List) {
for _, r := range in {
if !exists[r.Metadata().Namespace()] {
// namespace does not exist, also ignore implicit default ("")
if ns := r.Metadata().Namespace(); ns != "" && !exists[ns] {
missingNamespace = append(missingNamespace, r)
continue
}
Expand Down
91 changes: 91 additions & 0 deletions pkg/kubernetes/client/diff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package client

import (
"testing"

"github.com/grafana/tanka/pkg/kubernetes/manifest"
"github.com/stretchr/testify/assert"
)

func TestSeparateMissingNamespace(t *testing.T) {
cases := []struct {
name string
td nsTd

missing bool
}{
// default should always exist
{
name: "default",
td: newNsTd(func(m manifest.Metadata) {
m["namespace"] = "default"
}, []string{}),
missing: false,
},
// implcit default (not specfiying an ns at all) also
{
name: "implicit-default",
td: newNsTd(func(m manifest.Metadata) {
delete(m, "namespace")
}, []string{}),
missing: false,
},
// custom ns that exists
{
name: "custom-ns",
td: newNsTd(func(m manifest.Metadata) {
m["namespace"] = "custom"
}, []string{"custom"}),
missing: false,
},
// custom ns that does not exist
{
name: "missing-ns",
td: newNsTd(func(m manifest.Metadata) {
m["namespace"] = "missing"
}, []string{}),
missing: true,
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
ready, missing := separateMissingNamespace(manifest.List{c.td.m}, c.td.ns)
if c.missing {
assert.Lenf(t, ready, 0, "expected manifest to be missing (ready = 0)")
assert.Lenf(t, missing, 1, "expected manifest to be missing (missing = 1)")
} else {
assert.Lenf(t, ready, 1, "expected manifest to be ready (ready = 1)")
assert.Lenf(t, missing, 0, "expected manifest to be ready (missing = 0)")
}
})
}
}

type nsTd struct {
m manifest.Manifest
ns map[string]bool
}

func newNsTd(f func(m manifest.Metadata), ns []string) nsTd {
m := manifest.Manifest{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{},
}
if f != nil {
f(m.Metadata())
}

nsMap := map[string]bool{
"default": true, // you can't get rid of this one ever
}
for _, n := range ns {
nsMap[n] = true
}

return nsTd{
m: m,
ns: nsMap,
}
}
56 changes: 56 additions & 0 deletions pkg/kubernetes/client/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package client

import (
"fmt"
"os"
"os/exec"
"sort"
"strings"
)

// ctl returns an `exec.Cmd` for `kubectl`. It also forces the correct context
// and injects our patched $KUBECONFIG for the default namespace.
func (k Kubectl) ctl(action string, args ...string) *exec.Cmd {
// prepare the arguments
argv := []string{action,
"--context", k.context.Get("name").MustStr(),
}
argv = append(argv, args...)

// prepare the cmd
cmd := exec.Command("kubectl", argv...)
cmd.Env = patchKubeconfig(k.nsPatch, os.Environ())

return cmd
}

func patchKubeconfig(file string, e []string) []string {
// prepend namespace patch to $KUBECONFIG
env := newEnv(e)
if _, ok := env["KUBECONFIG"]; !ok {
env["KUBECONFIG"] = "~/.kube/config" // kubectl default
sh0rez marked this conversation as resolved.
Show resolved Hide resolved
}
env["KUBECONFIG"] = fmt.Sprintf("%s:%s", file, env["KUBECONFIG"])
return env.render()
}

// environment is a helper type for manipulating os.Environ() more easily
type environment map[string]string

func newEnv(e []string) environment {
env := make(environment)
for _, s := range e {
kv := strings.SplitN(s, "=", 2)
env[kv[0]] = kv[1]
}
return env
}

func (e environment) render() []string {
s := make([]string, 0, len(e))
for k, v := range e {
s = append(s, fmt.Sprintf("%s=%s", k, v))
}
sort.Strings(s)
return s
}
36 changes: 36 additions & 0 deletions pkg/kubernetes/client/exec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package client

import (
"testing"

"github.com/stretchr/testify/assert"
)

const patchFile = "/tmp/tk-nsPatch.yaml"

func TestPatchKubeconfig(t *testing.T) {

cases := []struct {
name string
env []string
want []string
}{
{
name: "none",
env: []string{},
want: []string{"KUBECONFIG=" + patchFile + ":~/.kube/config"},
},
{
name: "custom",
env: []string{"KUBECONFIG=/home/user/.config/kube"},
want: []string{"KUBECONFIG=" + patchFile + ":/home/user/.config/kube"},
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := patchKubeconfig(patchFile, c.env)
assert.Equal(t, c.want, got)
})
}
}
6 changes: 2 additions & 4 deletions pkg/kubernetes/client/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"encoding/json"
"fmt"
"os/exec"
"strings"

"github.com/grafana/tanka/pkg/kubernetes/manifest"
Expand Down Expand Up @@ -41,12 +40,11 @@ func (k Kubectl) GetByLabels(namespace string, labels map[string]interface{}) (m
}

func (k Kubectl) get(namespace string, sel []string) (manifest.Manifest, error) {
argv := append([]string{"get",
argv := append([]string{
"-o", "json",
"-n", namespace,
"--context", k.context.Get("name").MustStr(),
}, sel...)
cmd := exec.Command("kubectl", argv...)
cmd := k.ctl("get", argv...)

var sout, serr bytes.Buffer
cmd.Stdout = &sout
Expand Down
24 changes: 9 additions & 15 deletions pkg/kubernetes/client/kubectl.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"os"
"os/exec"
"regexp"

"github.com/Masterminds/semver"
Expand All @@ -17,17 +16,20 @@ import (

// Kubectl uses the `kubectl` command to operate on a Kubernetes cluster
type Kubectl struct {
context objx.Map
cluster objx.Map
// kubeconfig
nsPatch string
context objx.Map
cluster objx.Map

APIServer string
}

// New returns a instance of Kubectl with a correct context already discovered.
func New(endpoint string) (*Kubectl, error) {
func New(endpoint, namespace string) (*Kubectl, error) {
k := Kubectl{
APIServer: endpoint,
}
if err := k.setupContext(); err != nil {
if err := k.setupContext(namespace); err != nil {
return nil, errors.Wrap(err, "finding usable context")
}
return &k, nil
Expand All @@ -51,10 +53,7 @@ func (k Kubectl) Info() (*Info, error) {

// Version returns the version of kubectl and the Kubernetes api server
func (k Kubectl) version() (client, server *semver.Version, err error) {
cmd := exec.Command("kubectl", "version",
"-o", "json",
"--context", k.context.Get("name").MustStr(),
)
cmd := k.ctl("version", "-o", "json")
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = os.Stderr
Expand All @@ -69,12 +68,7 @@ func (k Kubectl) version() (client, server *semver.Version, err error) {

// Namespaces of the cluster
func (k Kubectl) Namespaces() (map[string]bool, error) {
argv := []string{"get",
"-o", "json",
"--context", k.context.Get("name").MustStr(),
"namespaces",
}
cmd := exec.Command("kubectl", argv...)
cmd := k.ctl("get", "namespaces", "-o", "json")

var sout bytes.Buffer
cmd.Stdout = &sout
Expand Down
Loading