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): properly handle diff namespaces #237

Merged
merged 7 commits into from
Mar 17, 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
5 changes: 5 additions & 0 deletions docs/docs/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ route: "/env-vars"

**Description**: Path to the `kubectl` tool executable
**Default**: `$PATH/kubectl`

### TANKA_KUBECTL_TRACE

**Description**: Print all calls to `kubectl`
**Default**: `false`
1 change: 1 addition & 0 deletions pkg/kubernetes/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Client interface {

// Namespaces the cluster currently has
Namespaces() (map[string]bool, error)
Resources() (Resources, error)

// Info returns known informational data about the client. Best effort based,
// fields of `Info` that cannot be stocked with valuable data, e.g.
Expand Down
38 changes: 5 additions & 33 deletions pkg/kubernetes/client/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,11 @@ import (
"github.com/Masterminds/semver"

"github.com/grafana/tanka/pkg/kubernetes/manifest"
"github.com/grafana/tanka/pkg/kubernetes/util"
)

// DiffServerSide takes the desired state and computes the differences on the
// server, returning them in `diff(1)` format
func (k Kubectl) DiffServerSide(data manifest.List) (*string, error) {
ns, err := k.Namespaces()
if err != nil {
return nil, err
}

ready, missing := separateMissingNamespace(data, ns)
cmd := k.ctl("diff", "-f", "-")

raw := bytes.Buffer{}
Expand All @@ -29,28 +22,19 @@ func (k Kubectl) DiffServerSide(data manifest.List) (*string, error) {
fw := FilterWriter{filters: []*regexp.Regexp{regexp.MustCompile(`exit status \d`)}}
cmd.Stderr = &fw

cmd.Stdin = strings.NewReader(ready.String())
cmd.Stdin = strings.NewReader(data.String())

err = cmd.Run()
err := cmd.Run()
if diffErr := parseDiffErr(err, fw.buf, k.Info().ClientVersion); diffErr != nil {
return nil, diffErr
}

s := raw.String()
for _, r := range missing {
d, err := util.DiffStr(util.DiffName(r), "", r.String())
if err != nil {
return nil, err
}
s += d
}

if s != "" {
return &s, nil
if s == "" {
return nil, nil
}

// no diff -> nil
return nil, nil
return &s, nil
}

// parseDiffErr handles the exit status code of `kubectl diff`. It returns err
Expand Down Expand Up @@ -85,15 +69,3 @@ func parseDiffErr(err error, stderr string, version *semver.Version) error {
// differences found is not an error
return nil
}

func separateMissingNamespace(in manifest.List, exists map[string]bool) (ready, missingNamespace manifest.List) {
for _, r := range in {
// namespace does not exist, also ignore implicit default ("")
if ns := r.Metadata().Namespace(); ns != "" && !exists[ns] {
missingNamespace = append(missingNamespace, r)
continue
}
ready = append(ready, r)
}
return
}
92 changes: 0 additions & 92 deletions pkg/kubernetes/client/diff_test.go

This file was deleted.

4 changes: 4 additions & 0 deletions pkg/kubernetes/client/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ func (k Kubectl) ctl(action string, args ...string) *exec.Cmd {
cmd := kubectlCmd(argv...)
cmd.Env = patchKubeconfig(k.nsPatch, os.Environ())

if os.Getenv("TANKA_KUBECTL_TRACE") != "" {
fmt.Println(cmd.String())
}

return cmd
}

Expand Down
110 changes: 110 additions & 0 deletions pkg/kubernetes/client/resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package client

import (
"bytes"
"encoding/json"
"fmt"
"os"
"regexp"
"strings"

"github.com/pkg/errors"

"github.com/grafana/tanka/pkg/kubernetes/manifest"
)

// Resources the Kubernetes API knows
type Resources []Resource

// Namespaced returns whether a resource is namespace-specific or cluster-wide
func (r Resources) Namespaced(m manifest.Manifest) bool {
for _, res := range r {
if m.Kind() == res.Kind {
return res.Namespaced
}
}

return false
}

// Resource is a Kubernetes API Resource
type Resource struct {
ApiGroup string `json:"APIGROUP"`
Kind string `json:"KIND"`
Name string `json:"NAME"`
Namespaced bool `json:"NAMESPACED,string"`
Shortnames string `json:"SHORTNAMES"`
}

// Resources returns all API resources known to the server
func (k Kubectl) Resources() (Resources, error) {
cmd := k.ctl("api-resources", "--cached")
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
return nil, err
}

var res Resources
if err := UnmarshalTable(out.String(), &res); err != nil {
return nil, errors.Wrap(err, "parsing table")
}

return res, nil
}

// UnmarshalTable unmarshals a raw CLI table into ptr. `json` package is used
// for getting the dat into the ptr, `json:` struct tags can be used.
func UnmarshalTable(raw string, ptr interface{}) error {
sh0rez marked this conversation as resolved.
Show resolved Hide resolved
raw = strings.TrimSpace(raw)

lines := strings.Split(raw, "\n")
if len(lines) < 2 {
return errors.New("table has less than 2 lines. No content found")
}

headerStr := lines[0]
lines = lines[1:]

spc := regexp.MustCompile(`[A-Z]+\s*`)
header := spc.FindAllString(headerStr, -1)

var tbl []map[string]string
for _, l := range lines {
elems := splitRow(l, header)
if len(elems) != len(header) {
return fmt.Errorf("header and row have different element count: %v != %v", len(header), len(elems))
}

row := make(map[string]string)
for i, e := range elems {
key := strings.TrimSpace(header[i])
row[key] = strings.TrimSpace(e)
}
tbl = append(tbl, row)
}

j, err := json.Marshal(tbl)
if err != nil {
return err
}

return json.Unmarshal(j, ptr)
}

func splitRow(s string, header []string) (elems []string) {
pos := 0
for i, h := range header {
if i == len(header)-1 {
elems = append(elems, s[pos:])
continue
}

lim := len(h)
elems = append(elems, s[pos:pos+lim])
pos += lim
}
return elems
}
Loading