Secrets are decoded upon describe (#2461)
parent
c76703a6e8
commit
1a2ee1028c
|
|
@ -135,6 +135,11 @@ func (g GVR) G() string {
|
||||||
return g.g
|
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.
|
// GVRs represents a collection of gvr.
|
||||||
type GVRs []GVR
|
type GVRs []GVR
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,7 @@ import (
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/k9s/internal/dao"
|
"github.com/derailed/k9s/internal/dao"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/derailed/k9s/internal/watch"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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) {
|
func TestAsGVR(t *testing.T) {
|
||||||
|
|
@ -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) {}
|
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ func (c *conn) IsActiveNamespace(string) bool { return fal
|
||||||
|
|
||||||
type podFactory struct{}
|
type podFactory struct{}
|
||||||
|
|
||||||
var _ dao.Factory = testFactory{}
|
var _ dao.Factory = &testFactory{}
|
||||||
|
|
||||||
func (f podFactory) Client() client.Connection {
|
func (f podFactory) Client() client.Connection {
|
||||||
return makeConn()
|
return makeConn()
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ type Describe struct {
|
||||||
lines []string
|
lines []string
|
||||||
refreshRate time.Duration
|
refreshRate time.Duration
|
||||||
listeners []ResourceViewerListener
|
listeners []ResourceViewerListener
|
||||||
|
decode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDescribe returns a new describe resource model.
|
// NewDescribe returns a new describe resource model.
|
||||||
|
|
@ -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())
|
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)
|
return desc.Describe(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,3 +207,8 @@ func (d *Describe) RemoveListener(l ResourceViewerListener) {
|
||||||
d.listeners = append(d.listeners[:victim], d.listeners[victim+1:]...)
|
d.listeners = append(d.listeners[:victim], d.listeners[victim+1:]...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Toggle toggles the decode flag.
|
||||||
|
func (d *Describe) Toggle() {
|
||||||
|
d.decode = !d.decode
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,10 @@ var Registry = map[string]ResourceMeta{
|
||||||
DAO: &dao.Table{},
|
DAO: &dao.Table{},
|
||||||
Renderer: &render.Event{},
|
Renderer: &render.Event{},
|
||||||
},
|
},
|
||||||
|
"v1/secrets": {
|
||||||
|
DAO: &dao.Secret{},
|
||||||
|
Renderer: &render.Generic{},
|
||||||
|
},
|
||||||
|
|
||||||
// Apps...
|
// Apps...
|
||||||
"apps/v1/deployments": {
|
"apps/v1/deployments": {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,14 @@ type ResourceViewer interface {
|
||||||
RemoveListener(ResourceViewerListener)
|
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.
|
// Igniter represents a runnable view.
|
||||||
type Igniter interface {
|
type Igniter interface {
|
||||||
// Start starts a component.
|
// Start starts a component.
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,23 @@ func (v *LiveView) bindKeys() {
|
||||||
ui.KeyM: ui.NewKeyAction("Toggle ManagedFields", v.toggleManagedCmd, true),
|
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 {
|
func (v *LiveView) editCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,8 @@ import (
|
||||||
"github.com/derailed/k9s/internal/dao"
|
"github.com/derailed/k9s/internal/dao"
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/ui"
|
||||||
"github.com/derailed/tcell/v2"
|
"github.com/derailed/tcell/v2"
|
||||||
v1 "k8s.io/api/core/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
"sigs.k8s.io/yaml"
|
"sigs.k8s.io/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -53,17 +51,12 @@ func (s *Secret) decodeCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var secret v1.Secret
|
d, err := dao.ExtractSecrets(o.(*unstructured.Unstructured))
|
||||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &secret)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.App().Flash().Err(err)
|
s.App().Flash().Err(err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
d := make(map[string]string, len(secret.Data))
|
|
||||||
for k, val := range secret.Data {
|
|
||||||
d[k] = string(val)
|
|
||||||
}
|
|
||||||
raw, err := yaml.Marshal(d)
|
raw, err := yaml.Marshal(d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.App().Flash().Errf("Error decoding secret %s", err)
|
s.App().Flash().Errf("Error decoding secret %s", err)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue