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

Secrets are decoded upon describe #2461

Merged
Merged
Show file tree
Hide file tree
Changes from all 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 internal/client/gvr.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ func (g GVR) G() string {
return g.g
}

// IsDecodable checks if the k8s resource has a decodable view
func (g GVR) IsDecodable() bool {
return g.GVK().Kind == "secrets"
}

// GVRs represents a collection of gvr.
type GVRs []GVR

Expand Down
33 changes: 0 additions & 33 deletions internal/dao/alias_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ import (
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/watch"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/informers"
)

func TestAsGVR(t *testing.T) {
Expand Down Expand Up @@ -88,32 +84,3 @@ func makeAliases() *dao.Alias {
},
}
}

type testFactory struct{}

func makeFactory() dao.Factory {
return testFactory{}
}

var _ dao.Factory = testFactory{}

func (f testFactory) Client() client.Connection {
return nil
}
func (f testFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
return nil, nil
}
func (f testFactory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) {
return nil, nil
}
func (f testFactory) ForResource(ns, gvr string) (informers.GenericInformer, error) {
return nil, nil
}
func (f testFactory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) {
return nil, nil
}
func (f testFactory) WaitForCacheSync() {}
func (f testFactory) Forwarders() watch.Forwarders {
return nil
}
func (f testFactory) DeleteForwarder(string) {}
2 changes: 1 addition & 1 deletion internal/dao/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (c *conn) IsActiveNamespace(string) bool { return fal

type podFactory struct{}

var _ dao.Factory = testFactory{}
var _ dao.Factory = &testFactory{}

func (f podFactory) Client() client.Connection {
return makeConn()
Expand Down
101 changes: 101 additions & 0 deletions internal/dao/secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright Authors of K9s

package dao

import (
"fmt"
"strings"

v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
)

// Secret represents a secret K8s resource.
type Secret struct {
Table
decode bool
}

// Describe describes a secret that can be encoded or decoded.
func (s *Secret) Describe(path string) (string, error) {
encodedDescription, err := s.Table.Describe(path)

if err != nil {
return "", err
}

if !s.decode {
return encodedDescription, nil
}

return s.Decode(encodedDescription, path)
}

// SetDecode sets the decode flag.
func (s *Secret) SetDecode(flag bool) {
s.decode = flag
}

// Decode removes the encoded part from the secret's description and appends the
// secret's decoded data.
func (s *Secret) Decode(encodedDescription, path string) (string, error) {
o, err := s.getFactory().Get(s.GVR(), path, true, labels.Everything())

if err != nil {
return "", err
}

dataEndIndex := strings.Index(encodedDescription, "====")

if dataEndIndex == -1 {
placintaalexandru marked this conversation as resolved.
Show resolved Hide resolved
return "", fmt.Errorf("Unable to find data section in secret description")
}

dataEndIndex += 4

if dataEndIndex >= len(encodedDescription) {
return "", fmt.Errorf("Data section in secret description is invalid")
}

// Remove the encoded part from k8s's describe API
// More details about the reasoning of index: https://github.com/kubernetes/kubectl/blob/v0.29.0/pkg/describe/describe.go#L2542
body := encodedDescription[0:dataEndIndex]

d, err := ExtractSecrets(o.(*unstructured.Unstructured))

if err != nil {
return "", err
}

decodedSecrets := []string{}

for k, v := range d {
placintaalexandru marked this conversation as resolved.
Show resolved Hide resolved
decodedSecrets = append(decodedSecrets, "\n", k, ":\t", v)
}

return body + strings.Join(decodedSecrets, ""), nil
}

// ExtractSecrets takes an unstructured object and attempts to convert it into a
// Kubernetes Secret.
// It returns a map where the keys are the secret data keys and the values are
// the corresponding secret data values.
// If the conversion fails, it returns an error.
func ExtractSecrets(o *unstructured.Unstructured) (map[string]string, error) {
var secret v1.Secret
derailed marked this conversation as resolved.
Show resolved Hide resolved
err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.Object, &secret)

if err != nil {
return nil, err
}

secretData := make(map[string]string, len(secret.Data))

for k, val := range secret.Data {
secretData[k] = string(val)
}

return secretData, nil
}
44 changes: 44 additions & 0 deletions internal/dao/secret_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s

package dao_test

import (
"testing"

"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/stretchr/testify/assert"
)

func TestEncodedSecretDescribe(t *testing.T) {
s := dao.Secret{}
s.Init(makeFactory(), client.NewGVR("v1/secrets"))

encodedString :=
`
Name: bootstrap-token-abcdef
Namespace: kube-system
Labels: <none>
Annotations: <none>

Type: generic

Data
====
token-secret: 24 bytes`

expected := "\nName: bootstrap-token-abcdef\n" +
"Namespace: kube-system\n" +
"Labels: <none>\n" +
"Annotations: <none>\n" +
"\n" +
"Type: generic\n" +
"\n" +
"Data\n" +
"====\n" +
"token-secret:\t0123456789abcdef"

decodedDescription, _ := s.Decode(encodedString, "kube-system/bootstrap-token-abcdef")
assert.Equal(t, expected, decodedDescription)
}
15 changes: 15 additions & 0 deletions internal/dao/testdata/secret.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"apiVersion": "v1",
"data": {
"token-secret": "MDEyMzQ1Njc4OWFiY2RlZg=="
},
"kind": "Secret",
"metadata": {
"creationTimestamp": "2024-01-15T18:19:00Z",
"name": "bootstrap-token-abcdef",
"namespace": "kube-system",
"resourceVersion": "243",
"uid": "6f5695d4-c0f4-4b65-890a-b1115ffd1f3b"
},
"type": "bootstrap.kubernetes.io/token"
}
77 changes: 77 additions & 0 deletions internal/dao/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package dao_test

import (
"encoding/json"
"fmt"
"os"
"path"
"strings"

"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/watch"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/informers"
)

type testFactory struct {
inventory map[string]map[string][]runtime.Object
}

func makeFactory() dao.Factory {
return &testFactory{
inventory: map[string]map[string][]runtime.Object{
"kube-system": {
"v1/secrets": {
load("secret"),
},
},
},
}
}

var _ dao.Factory = &testFactory{}

func (f *testFactory) Client() client.Connection {
return nil
}
func (f *testFactory) Get(gvr, fqn string, wait bool, sel labels.Selector) (runtime.Object, error) {
ns, po := path.Split(fqn)
ns = strings.Trim(ns, "/")

for _, o := range f.inventory[ns][gvr] {
if o.(*unstructured.Unstructured).GetName() == po {
return o, nil
}
}

return nil, nil
}
func (f *testFactory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) {
return f.inventory[ns][gvr], nil
}

func (f *testFactory) ForResource(ns, gvr string) (informers.GenericInformer, error) {
return nil, nil
}
func (f *testFactory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) {
return nil, nil
}
func (f *testFactory) WaitForCacheSync() {}
func (f *testFactory) Forwarders() watch.Forwarders {
return nil
}
func (f *testFactory) DeleteForwarder(string) {}

type testResource struct{}

func load(n string) *unstructured.Unstructured {
raw, _ := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))

var o unstructured.Unstructured
json.Unmarshal(raw, &o)

return &o
}
10 changes: 10 additions & 0 deletions internal/model/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Describe struct {
lines []string
refreshRate time.Duration
listeners []ResourceViewerListener
decode bool
}

// NewDescribe returns a new describe resource model.
Expand Down Expand Up @@ -180,6 +181,10 @@ func (d *Describe) describe(ctx context.Context, gvr client.GVR, path string) (s
return "", fmt.Errorf("no describer for %q", meta.DAO.GVR())
}

if desc, ok := meta.DAO.(*dao.Secret); ok {
desc.SetDecode(d.decode)
}

return desc.Describe(path)
}

Expand All @@ -202,3 +207,8 @@ func (d *Describe) RemoveListener(l ResourceViewerListener) {
d.listeners = append(d.listeners[:victim], d.listeners[victim+1:]...)
}
}

// Toggle toggles the decode flag.
func (d *Describe) Toggle() {
d.decode = !d.decode
}
4 changes: 4 additions & 0 deletions internal/model/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ var Registry = map[string]ResourceMeta{
DAO: &dao.Table{},
Renderer: &render.Event{},
},
"v1/secrets": {
DAO: &dao.Secret{},
Renderer: &render.Generic{},
},

// Apps...
"apps/v1/deployments": {
Expand Down
8 changes: 8 additions & 0 deletions internal/model/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ type ResourceViewer interface {
RemoveListener(ResourceViewerListener)
}

// EncDecResourceViewer interface extends the ResourceViewer interface and
// adds a `Toggle` that allows the user to switch between encoded or decoded
// state of the view.
type EncDecResourceViewer interface {
ResourceViewer
Toggle()
}

// Igniter represents a runnable view.
type Igniter interface {
// Start starts a component.
Expand Down
17 changes: 17 additions & 0 deletions internal/view/live_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,23 @@ func (v *LiveView) bindKeys() {
ui.KeyM: ui.NewKeyAction("Toggle ManagedFields", v.toggleManagedCmd, true),
})
}
if v.model != nil && v.model.GVR().IsDecodable() {
v.actions.Add(ui.KeyActions{
ui.KeyT: ui.NewKeyAction("Toggle Encoded / Decoded", v.toggleEncodedDecodedCmd, true),
})
}
}

func (v *LiveView) toggleEncodedDecodedCmd(evt *tcell.EventKey) *tcell.EventKey {
m, ok := v.model.(model.EncDecResourceViewer)

if !ok {
return evt
}

m.Toggle()
v.Start()
return nil
}

func (v *LiveView) editCmd(evt *tcell.EventKey) *tcell.EventKey {
Expand Down
Loading
Loading