From 52d142ee32d51f99e4a7e1b61a4106971bd4c886 Mon Sep 17 00:00:00 2001 From: Lewis Marshall Date: Fri, 12 Oct 2018 17:25:24 +0100 Subject: [PATCH] Add capability to lookup values from kubernetes objects --- README.md | 33 ++++++++++++++++++++++++++++++++ k8api-kubectl.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ k8api-noop.go | 16 ++++++++++++++++ k8api.go | 7 +++++++ main.go | 8 +++++++- render.go | 17 +++++++++++++++-- render_test.go | 5 +++-- 7 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 k8api-kubectl.go create mode 100644 k8api-noop.go create mode 100644 k8api.go diff --git a/README.md b/README.md index e4d3064..ce254ce 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ kd specific template functions: - [file](#file) - [secret](#secret) +- [k8lookup](#k8lookup) ### split @@ -282,6 +283,38 @@ data: username: {{ "my-username" | b64enc }} ``` +### k8lookup + +`k8lookup` function allows retrieval of an Kubernetes object value using the parameters: +- `kind` - a Kubernetes object kind e.g. `pv` or `PersistentVolume` +- `name` - an object name e.g. `sysdig-mysql-a` +- `path` - a object path reference e.g. `.spec.capacity.storage` + +Example: + +With manually provisioned storage (e.g. iSCSI or NFS) a PV is typically managed +using a separate repository. Using lookup, we can discover the appropriate +storage size for a given cluster automatically: + +``` +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + name: sysdig-galera + name: data-sysdig-galera-0 +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ lookup "pv" "sysdig-mysql-a" ".spec.capacity.storage" }} + selector: + matchLabels: + name: sysdig-mysql + storageClassName: manual +``` + ## Configuration Configuration can be provided via cli flags and arguments as well as diff --git a/k8api-kubectl.go b/k8api-kubectl.go new file mode 100644 index 0000000..ad7e2ed --- /dev/null +++ b/k8api-kubectl.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + "io/ioutil" + "strings" + + "github.com/urfave/cli" +) + +// K8ApiKubectl is a kubectl implimentation of K8Api interface +type K8ApiKubectl struct { + K8Api + Cx *cli.Context +} + +// NewK8ApiKubectl creates a concrete class bound to use kubectl +func NewK8ApiKubectl(c *cli.Context) K8Api { + api := &K8ApiKubectl{ + Cx: c, + } + return api +} + +// Lookup will get data from a specified kubernetes object +func (a K8ApiKubectl) Lookup(kind, name, path string) (string, error) { + args := []string{"get", kind + "/" + name, "-o", "custom-columns=:" + path, "--no-headers"} + + cmd, err := newKubeCmd(a.Cx, args, false) + if err != nil { + return "", err + } + stderr, _ := cmd.StderrPipe() + stdout, _ := cmd.StdoutPipe() + if err := cmd.Start(); err != nil { + logDebug.Printf("error starting kubectl: %s", err) + return "", err + } + data, _ := ioutil.ReadAll(stdout) + if err := cmd.Wait(); err != nil { + logDebug.Printf("error with kubectl: %s", err) + errData, _ := ioutil.ReadAll(stderr) + if strings.Contains("NotFound", string(errData[:])) { + return "", fmt.Errorf("Error object %s/%s not found", kind, name) + } + return "", err + } + return strings.TrimSpace(string(data[:])), nil +} diff --git a/k8api-noop.go b/k8api-noop.go new file mode 100644 index 0000000..f4952c8 --- /dev/null +++ b/k8api-noop.go @@ -0,0 +1,16 @@ +package main + +// K8ApiNoop is a noop API runner used when not connected to a server +type K8ApiNoop struct { + K8Api +} + +// NewK8ApiNoop creats a new K8Api implimentaion based on K8ApiNoop +func NewK8ApiNoop() K8Api { + return &K8ApiNoop{} +} + +// Lookup will pretentd to get data from a specified kubernetes object +func (a K8ApiNoop) Lookup(kind, name, path string) (string, error) { + return "noop", nil +} diff --git a/k8api.go b/k8api.go new file mode 100644 index 0000000..365cd38 --- /dev/null +++ b/k8api.go @@ -0,0 +1,7 @@ +package main + +// K8Api is an abstraction to allow the migration to the real API not kubectl +type K8Api interface { + // Lookup abstract interface for finding kuberneets api data by kind, name and path + Lookup(kind, name, path string) (string, error) +} diff --git a/main.go b/main.go index de10d78..e4a2e5e 100644 --- a/main.go +++ b/main.go @@ -337,7 +337,13 @@ func run(c *cli.Context) error { return err } for _, d := range splitYamlDocs(string(data)) { - rendered, genSecret, err := Render(string(d), EnvToMap()) + var k8api K8Api + if dryRun { + k8api = NewK8ApiNoop() + } else { + k8api = NewK8ApiKubectl(c) + } + rendered, genSecret, err := Render(k8api, string(d), EnvToMap()) if err != nil { return err } diff --git a/render.go b/render.go index c259542..5a8ab8f 100644 --- a/render.go +++ b/render.go @@ -14,10 +14,11 @@ import ( var ( secretUsed = false + k8Api K8Api ) // Render - the function used for rendering templates (with Sprig support) -func Render(tmpl string, vars map[string]string) (string, bool, error) { +func Render(k K8Api, tmpl string, vars map[string]string) (string, bool, error) { fm := sprig.TxtFuncMap() // Preserve old KD functionality (strings param order vs sprig) fm["contains"] = strings.Contains @@ -27,6 +28,9 @@ func Render(tmpl string, vars map[string]string) (string, bool, error) { fm["secret"] = secret // Add file function to map fm["file"] = fileRender + // Required for lookup function + k8Api = k + fm["k8lookup"] = k8lookup secretUsed = false defer func() { if err := recover(); err != nil { @@ -88,10 +92,19 @@ func fileRender(key string) string { if err != nil { panic(err.Error()) } - render, wasSecret, err := Render(string(data), EnvToMap()) + render, wasSecret, err := Render(k8Api, string(data), EnvToMap()) if err != nil { panic(err.Error()) } secretUsed = wasSecret return render } + +// k8lookup find a value from a kubernetes object +func k8lookup(kind, name, path string) string { + data, err := k8Api.Lookup(kind, name, path) + if err != nil { + panic(err.Error()) + } + return data +} diff --git a/render_test.go b/render_test.go index 184a994..3406d19 100644 --- a/render_test.go +++ b/render_test.go @@ -68,9 +68,10 @@ func TestRender(t *testing.T) { }, } + api := NewK8ApiNoop() for _, c := range cases { t.Run(c.name, func(t *testing.T) { - got, _, err := Render(c.inputdata, c.inputvars) + got, _, err := Render(api, c.inputdata, c.inputvars) if err != nil { fmt.Println("Testing if folder doesnt exist") } @@ -83,7 +84,7 @@ func TestRender(t *testing.T) { // Test of secret functions: t.Run("Check secret is parsed and detected", func(t *testing.T) { c := readfile("test/secret.yaml") - _, isSecret, err := Render(c, testData) + _, isSecret, err := Render(api, c, testData) if err != nil { fmt.Printf("unexpected problem rendering:%v\n", err) }