Skip to content

Commit

Permalink
Ensure backwards-compatibility to existing view-secret plugin
Browse files Browse the repository at this point in the history
Remove cli runtime dependency
Optimize build
Add test target to Makefile
  • Loading branch information
elsesiy committed Oct 24, 2019
1 parent ce75f9b commit f4fac7a
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 340 deletions.
4 changes: 3 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ go: '1.13'
env:
- GO111MODULE=on

script: make
script:
- make test
- make

deploy:
- provider: script
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ BINARY := kubectl-view-secret

build: kubectl-view-secret

test: $(SOURCES)
go test -v -short -race -timeout 30s ./...

$(BINARY): $(SOURCES)
CGO_ENABLED=0 go build -o $(BINARY) ./cmd/$(BINARY).go
CGO_ENABLED=0 go build -o $(BINARY) -ldflags="-s -w" ./cmd/$(BINARY).go
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ This plugin allows for easy secret decoding. Useful if you want to see what's in

Instead you can now do:

kubectl view-secret <secret>
kubectl view-secret <secret> # prints secret keys
kubectl view-secret <secret> <key> # decodes specific entry
kubeclt view-secret <secret> -a/--all # decodes all contents
kubectl view-secret <secret> -n/--namespace <ns> # override namespace

## Build
Expand Down
3 changes: 1 addition & 2 deletions cmd/kubectl-view-secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ package main

import (
"github.com/elsesiy/kubectl-view-secret/pkg/cmd"
"k8s.io/cli-runtime/pkg/genericclioptions"
"os"
)

func main() {
command := cmd.NewCmdViewSecret(genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr})
command := cmd.NewCmdViewSecret()
if err := command.Execute(); err != nil {
os.Exit(1)
}
Expand Down
24 changes: 1 addition & 23 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,7 @@ module github.com/elsesiy/kubectl-view-secret
go 1.13

require (
github.com/emicklei/go-restful v2.11.0+incompatible // indirect
github.com/evanphx/json-patch v4.5.0+incompatible // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-openapi/jsonreference v0.19.3 // indirect
github.com/go-openapi/spec v0.19.4 // indirect
github.com/gogo/protobuf v1.3.1 // indirect
github.com/google/btree v1.0.0 // indirect
github.com/googleapis/gnostic v0.3.1 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/imdario/mergo v0.3.8 // indirect
github.com/mailru/easyjson v0.7.0 // indirect
github.com/pkg/errors v0.8.1 // indirect
github.com/magiconair/properties v1.8.0
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582 // indirect
golang.org/x/sys v0.0.0-20191018095205-727590c5006e // indirect
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0 // indirect
google.golang.org/appengine v1.6.5 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/api v0.0.0-20191016225839-816a9b7df678 // indirect
k8s.io/apimachinery v0.0.0-20191017185446-6e68a40eebf9 // indirect
k8s.io/cli-runtime v0.0.0-20191016113839-5e0efc75cd33
k8s.io/kube-openapi v0.0.0-20190918143330-0270cf2f1c1d // indirect
k8s.io/utils v0.0.0-20191010214722-8d271d903fe4
)
270 changes: 1 addition & 269 deletions go.sum

Large diffs are not rendered by default.

102 changes: 59 additions & 43 deletions pkg/cmd/view-secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,45 @@ import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/spf13/cobra"
clioptions "k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/utils/pointer"
"io"
"os"
"os/exec"
)

const example = `
# decode secret by name
# print secret keys
%[1]s view-secret <secret>
# decode secret by name in different namespace
%[1]s view-secret <secret> -n/--namespace ns
# decode secret specific key
%[1]s view-secret <secret> <key>
# decode all contents of a secret
%[1]s view-secret <secret> -a/--all
# print keys for secret in different namespace
%[1]s view-secret <secret> -n/--namespace <ns>
`

var ErrSecretKeyNotFound = errors.New("provided key not found in secret")

// CommandOpts is the struct holding common properties
type CommandOpts struct {
configFlags *clioptions.ConfigFlags
clioptions.IOStreams

cmdArgs []string
customNamespace string
decodeAll bool
secretName string
secretKey string
}

// NewCmdViewSecret creates the cobra command to be executed
func NewCmdViewSecret(streams clioptions.IOStreams) *cobra.Command {
res := &CommandOpts{
configFlags: &clioptions.ConfigFlags{Namespace: pointer.StringPtr("")},
IOStreams: streams,
}
func NewCmdViewSecret() *cobra.Command {
res := &CommandOpts{}

cmd := &cobra.Command{
Use: "view-secret [secret-name]",
Short: "Decode a kubernetes secret by name in the current context/cluster/namespace",
Use: "view-secret [secret-name] [secret-key]",
Short: "Decode a kubernetes secret by name & key in the current context/cluster/namespace",
Example: fmt.Sprintf(example, "kubectl"),
SilenceUsage: true,
RunE: func(c *cobra.Command, args []string) error {
Expand All @@ -51,64 +57,74 @@ func NewCmdViewSecret(streams clioptions.IOStreams) *cobra.Command {
},
}

res.configFlags.AddFlags(cmd.Flags())
cmd.Flags().BoolVarP(&res.decodeAll, "all", "a", res.decodeAll, "if true, decodes all secrets without specifying the individual secret keys")
cmd.Flags().StringVarP(&res.customNamespace, "namespace", "n", res.customNamespace, "override the namespace defined in the current context")

return cmd
}

// Validate ensures proper command usage
func (d *CommandOpts) Validate(args []string) error {
if len(args) != 1 {
return fmt.Errorf("\nplease provide only the secret name to be decoded, see --help for usage instructions")
func (c *CommandOpts) Validate(args []string) error {
argLen := len(args)
if argLen < 1 || argLen > 2 {
return fmt.Errorf("\nincorrect number or arguments, see --help for usage instructions")
}

d.cmdArgs = args
c.secretName = args[0]
if argLen == 2 {
c.secretKey = args[1]
}

return nil
}

// Retrieve reads the kubeconfig and decodes the secret
func (d *CommandOpts) Retrieve(c *cobra.Command) error {
kubeCfg, err := d.configFlags.ToRawKubeConfigLoader().RawConfig()
if err != nil {
return err
}
func (c *CommandOpts) Retrieve(cmd *cobra.Command) error {
nsOverride, _ := cmd.Flags().GetString("namespace")

currCtx := kubeCfg.Contexts[kubeCfg.CurrentContext]
currCluster := currCtx.Cluster
currNs := currCtx.Namespace

nsOverride, _ := c.Flags().GetString("namespace")
var res, cmdErr bytes.Buffer
commandArgs := []string{"get", "secret", c.secretName, "-o", "json"}
if nsOverride != "" {
currNs = nsOverride
commandArgs = append(commandArgs, "-n", nsOverride)
}

var res, cmdErr bytes.Buffer
commandArgs := []string{"get", "secret", d.cmdArgs[0], "-n", currNs, "-o", "json"}
out := exec.Command("kubectl", commandArgs...)
out.Stdout = &res
out.Stderr = &cmdErr
err = out.Run()
err := out.Run()
if err != nil {
fmt.Print(cmdErr.String())
return nil
}

var secJson map[string]interface{}
if err := json.Unmarshal(res.Bytes(), &secJson); err != nil {
var secret map[string]interface{}
if err := json.Unmarshal(res.Bytes(), &secret); err != nil {
return err
}

secrets := secJson["data"].(map[string]interface{})
return ProcessSecret(os.Stdout, secret, c.secretKey, c.decodeAll)
}

// ProcessSecret takes the secret and user input to determine the output
func ProcessSecret(w io.Writer, secret map[string]interface{}, secretKey string, decodeAll bool) error {
data := secret["data"].(map[string]interface{})

if len(secrets) > 0 {
fmt.Printf("Decoded secret '%s' in namespace '%s' for cluster '%s'\n\n", d.cmdArgs[0], currNs, currCluster)
for k, v := range secrets {
if decodeAll {
for k, v := range data {
b64d, _ := base64.URLEncoding.DecodeString(v.(string))
_, _ = fmt.Fprintf(w, "%s=%s\n", k, b64d)
}
} else if secretKey != "" {
if v, ok := data[secretKey]; ok {
b64d, _ := base64.URLEncoding.DecodeString(v.(string))
fmt.Printf("%s=%s\n", k, b64d)
_, _ = fmt.Fprintln(w, string(b64d))
} else {
return ErrSecretKeyNotFound
}
} else {
fmt.Println("the provided secret is empty")
for k := range data {
_, _ = fmt.Fprintln(w, k)
}
}

return nil
Expand Down
76 changes: 76 additions & 0 deletions pkg/cmd/view-secret_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cmd

import (
"bufio"
"bytes"
"encoding/json"
"github.com/magiconair/properties/assert"
"reflect"
"sort"
"strings"
"testing"
)

var testSecret = `
{
"apiVersion": "v1",
"data": {
"TEST_PASSWORD": "c2VjcmV0Cg==",
"TEST_PASSWORD_2": "dmVyeXNlY3JldAo="
},
"kind": "Secret",
"metadata": {
"creationTimestamp": "2019-09-16T17:57:46Z",
"labels": {
"app": "test"
},
"name": "test",
"namespace": "test",
"resourceVersion": "1",
"selfLink": "/api/v1/namespaces/test/secrets/test",
"uid": "00000000-0000-0000-0000-000000000000"
},
"type": "Opaque"
}
`

func TestProcessSecret(t *testing.T) {
var secret map[string]interface{}
_ = json.Unmarshal([]byte(testSecret), &secret)

tests := map[string]struct {
want []string
secretKey string
decodeAll bool
err error
}{
"view-secret <secret>": {[]string{"TEST_PASSWORD", "TEST_PASSWORD_2"}, "", false, nil,},
"view-secret test TEST_PASSWORD": {[]string{"", "secret"}, "TEST_PASSWORD", false, nil},
"view-secret test -a": {[]string{"", "", "TEST_PASSWORD=secret", "TEST_PASSWORD_2=verysecret"}, "", true, nil},
"view-secret test NONE": {nil, "NONE", false, ErrSecretKeyNotFound},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
got := bytes.Buffer{}
err := ProcessSecret(&got, secret, test.secretKey, test.decodeAll)

if test.err != nil {
assert.Equal(t, err, test.err)
} else {
var gotArr []string
scanner := bufio.NewScanner(strings.NewReader(got.String()))
for scanner.Scan() {
gotArr = append(gotArr, scanner.Text())
}

sort.Strings(gotArr)

if !reflect.DeepEqual(gotArr, test.want) {
t.Errorf("got %v, want %v", gotArr, test.want)
}
}

})
}
}

0 comments on commit f4fac7a

Please sign in to comment.