Skip to content

Commit

Permalink
add get secrets command (#193)
Browse files Browse the repository at this point in the history
Signed-off-by: Manabu McCloskey <[email protected]>
  • Loading branch information
nabuskey authored Apr 17, 2024
1 parent 74dd98d commit 8b1549b
Show file tree
Hide file tree
Showing 6 changed files with 374 additions and 0 deletions.
4 changes: 4 additions & 0 deletions api/v1alpha1/localbuild_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const (
// CliStartTimeAnnotation indicates when the CLI was invoked.
CliStartTimeAnnotation = "cnoe.io/cli-start-time"
FieldManager = "idpbuilder"
// If GetSecretLabelKey is set to GetSecretLabelValue on a kubernetes secret, secret key and values can be used by the get command.
CLISecretLabelKey = "cnoe.io/cli-secret"
CLISecretLabelValue = "true"
PackageNameLabelKey = "cnoe.io/package-name"
)

// ArgoPackageConfigSpec Allows for configuration of the ArgoCD Installation.
Expand Down
25 changes: 25 additions & 0 deletions pkg/cmd/get/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package get

import (
"fmt"

"github.com/spf13/cobra"
)

var GetCmd = &cobra.Command{
Use: "get",
Short: "get information from the cluster",
Long: ``,
RunE: exportE,
}

var packages []string

func init() {
GetCmd.AddCommand(SecretsCmd)
GetCmd.PersistentFlags().StringSliceVarP(&packages, "packages", "p", []string{}, "names of packages.")
}

func exportE(cmd *cobra.Command, args []string) error {
return fmt.Errorf("specify subcommand")
}
198 changes: 198 additions & 0 deletions pkg/cmd/get/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package get

import (
"context"
"embed"
"fmt"
"io"
"os"
"path/filepath"
"text/template"

"github.com/cnoe-io/idpbuilder/api/v1alpha1"
"github.com/cnoe-io/idpbuilder/pkg/build"
"github.com/cnoe-io/idpbuilder/pkg/k8s"
"github.com/cnoe-io/idpbuilder/pkg/util"
"github.com/spf13/cobra"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/client-go/util/homedir"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
secretTemplatePath = "templates/secrets.tmpl"
)

//go:embed templates
var templates embed.FS

var SecretsCmd = &cobra.Command{
Use: "secrets",
Short: "retrieve secrets from the cluster",
Long: ``,
RunE: getSecretsE,
}

// well known secrets that are part of the core packages
var corePkgSecrets = map[string][]string{
"argocd": []string{"argocd-initial-admin-secret"},
"gitea": []string{"gitea-admin-secret"},
}

type TemplateData struct {
Name string
Namespace string
Data map[string]string
}

func getSecretsE(cmd *cobra.Command, args []string) error {
ctx, ctxCancel := context.WithCancel(ctrl.SetupSignalHandler())
defer ctxCancel()
kubeConfigPath := filepath.Join(homedir.HomeDir(), ".kube", "config")

b := build.NewBuild("", "", kubeConfigPath, "", "", util.CorePackageTemplateConfig{}, []string{}, false, k8s.GetScheme(), ctxCancel)

kubeConfig, err := b.GetKubeConfig()
if err != nil {
return fmt.Errorf("getting kube config: %w", err)
}

kubeClient, err := b.GetKubeClient(kubeConfig)
if err != nil {
return fmt.Errorf("getting kube client: %w", err)
}

if len(packages) == 0 {
return printAllPackageSecrets(ctx, os.Stdout, kubeClient)
}

return printPackageSecrets(ctx, os.Stdout, kubeClient)
}

func printAllPackageSecrets(ctx context.Context, outWriter io.Writer, kubeClient client.Client) error {
selector := labels.NewSelector()
secretsToPrint := make([]any, 0, 2)

for k, v := range corePkgSecrets {
for i := range v {
secret, sErr := getSecretByName(ctx, kubeClient, k, v[i])
if sErr != nil {
if errors.IsNotFound(sErr) {
continue
}
return fmt.Errorf("getting secret %s in %s: %w", v[i], k, sErr)
}
secretsToPrint = append(secretsToPrint, secretToTemplateData(secret))
}
}

secrets, err := getSecretsByCNOELabel(ctx, kubeClient, selector)
if err != nil {
return fmt.Errorf("listing secrets: %w", err)
}

for i := range secrets.Items {
secretsToPrint = append(secretsToPrint, secretToTemplateData(secrets.Items[i]))
}

if len(secretsToPrint) == 0 {
fmt.Println("no secrets found")
return nil
}
return renderTemplate(secretTemplatePath, outWriter, secretsToPrint)
}

func printPackageSecrets(ctx context.Context, outWriter io.Writer, kubeClient client.Client) error {
selector := labels.NewSelector()
secretsToPrint := make([]any, 0, 2)

for i := range packages {
p := packages[i]
secretNames, ok := corePkgSecrets[p]
if ok {
for j := range secretNames {
secret, sErr := getSecretByName(ctx, kubeClient, p, secretNames[j])
if sErr != nil {
if errors.IsNotFound(sErr) {
continue
}
return fmt.Errorf("getting secret %s in %s: %w", secretNames[j], p, sErr)
}
secretsToPrint = append(secretsToPrint, secretToTemplateData(secret))
}
continue
}

req, pErr := labels.NewRequirement(v1alpha1.PackageNameLabelKey, selection.Equals, []string{p})
if pErr != nil {
return fmt.Errorf("building requirement for %s: %w", p, pErr)
}

pkgSelector := selector.Add(*req)

secrets, pErr := getSecretsByCNOELabel(ctx, kubeClient, pkgSelector)
if pErr != nil {
return fmt.Errorf("listing secrets: %w", pErr)
}

for j := range secrets.Items {
secretsToPrint = append(secretsToPrint, secretToTemplateData(secrets.Items[j]))
}
}

return renderTemplate(secretTemplatePath, outWriter, secretsToPrint)
}

func renderTemplate(templatePath string, outWriter io.Writer, data []any) error {
tmpl, err := templates.ReadFile(templatePath)
if err != nil {
return fmt.Errorf("failed to read template: %w", err)
}

t, err := template.New("secrets").Parse(string(tmpl))
if err != nil {
return fmt.Errorf("parsing template: %w", err)
}
for i := range data {
tErr := t.Execute(outWriter, data[i])
if tErr != nil {
return fmt.Errorf("executing template for data %s : %w", data[i], tErr)
}
}
return nil
}

func secretToTemplateData(s v1.Secret) TemplateData {
data := TemplateData{
Name: s.Name,
Namespace: s.Namespace,
Data: make(map[string]string),
}
for k, v := range s.Data {
data.Data[k] = string(v)
}
return data
}

func getSecretsByCNOELabel(ctx context.Context, kubeClient client.Client, l labels.Selector) (v1.SecretList, error) {
req, err := labels.NewRequirement(v1alpha1.CLISecretLabelKey, selection.Equals, []string{v1alpha1.CLISecretLabelValue})
if err != nil {
return v1.SecretList{}, fmt.Errorf("building labels with key %s and value %s : %w", v1alpha1.CLISecretLabelKey, v1alpha1.CLISecretLabelValue, err)
}

secrets := v1.SecretList{}
opts := client.ListOptions{
LabelSelector: l.Add(*req),
Namespace: "", // find in all namespaces
}
return secrets, kubeClient.List(ctx, &secrets, &opts)
}

func getSecretByName(ctx context.Context, kubeClient client.Client, ns, name string) (v1.Secret, error) {
s := v1.Secret{}
return s, kubeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: ns}, &s)
}
138 changes: 138 additions & 0 deletions pkg/cmd/get/secrets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package get

import (
"context"
"io"
"testing"

"github.com/cnoe-io/idpbuilder/api/v1alpha1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"sigs.k8s.io/controller-runtime/pkg/client"
)

type fakeKubeClient struct {
mock.Mock
client.Client
}

func (f *fakeKubeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
args := f.Called(ctx, key, obj, opts)
return args.Error(0)
}

func (f *fakeKubeClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
args := f.Called(ctx, list, opts)
return args.Error(0)
}

type cases struct {
err error
packages []string
getKeys []client.ObjectKey
listLabelSelector []labels.Selector
}

func selector(pkgName string) labels.Selector {
r1, _ := labels.NewRequirement(v1alpha1.CLISecretLabelKey, selection.Equals, []string{v1alpha1.CLISecretLabelValue})
r2, _ := labels.NewRequirement(v1alpha1.PackageNameLabelKey, selection.Equals, []string{pkgName})
return labels.NewSelector().Add(*r1).Add(*r2)
}

func TestPrintPackageSecrets(t *testing.T) {
ctx := context.Background()

cs := []cases{
{
err: nil,
packages: []string{"abc"},
listLabelSelector: []labels.Selector{selector("abc")},
},
{
err: nil,
packages: []string{"argocd", "gitea", "abc"},
listLabelSelector: []labels.Selector{selector("abc")},
getKeys: []client.ObjectKey{
{Name: "argocd-initial-admin-secret", Namespace: "argocd"},
{Name: "gitea-admin-secret", Namespace: "gitea"},
},
},
{
err: nil,
packages: []string{"argocd", "gitea"},
getKeys: []client.ObjectKey{
{Name: "argocd-initial-admin-secret", Namespace: "argocd"},
{Name: "gitea-admin-secret", Namespace: "gitea"},
},
},
{
err: nil,
packages: []string{"argocd"},
getKeys: []client.ObjectKey{
{Name: "argocd-initial-admin-secret", Namespace: "argocd"},
},
},
}

for i := range cs {
c := cs[i]
fClient := new(fakeKubeClient)
packages = c.packages

for j := range c.listLabelSelector {
opts := client.ListOptions{
LabelSelector: c.listLabelSelector[j],
Namespace: "",
}
fClient.On("List", ctx, mock.Anything, []client.ListOption{&opts}).Return(c.err)
}

for j := range c.getKeys {
fClient.On("Get", ctx, c.getKeys[j], mock.Anything, mock.Anything).Return(c.err)
}

err := printPackageSecrets(ctx, io.Discard, fClient)
fClient.AssertExpectations(t)
assert.Nil(t, err)
}
}

func TestPrintAllPackageSecrets(t *testing.T) {
ctx := context.Background()

r, _ := labels.NewRequirement(v1alpha1.CLISecretLabelKey, selection.Equals, []string{v1alpha1.CLISecretLabelValue})

cs := []cases{
{
err: nil,
listLabelSelector: []labels.Selector{labels.NewSelector().Add(*r)},
getKeys: []client.ObjectKey{
{Name: "argocd-initial-admin-secret", Namespace: "argocd"},
{Name: "gitea-admin-secret", Namespace: "gitea"},
},
},
}

for i := range cs {
c := cs[i]
fClient := new(fakeKubeClient)

for j := range c.listLabelSelector {
opts := client.ListOptions{
LabelSelector: c.listLabelSelector[j],
Namespace: "",
}
fClient.On("List", ctx, mock.Anything, []client.ListOption{&opts}).Return(c.err)
}

for j := range c.getKeys {
fClient.On("Get", ctx, c.getKeys[j], mock.Anything, mock.Anything).Return(c.err)
}

err := printAllPackageSecrets(ctx, io.Discard, fClient)
fClient.AssertExpectations(t)
assert.Nil(t, err)
}
}
7 changes: 7 additions & 0 deletions pkg/cmd/get/templates/secrets.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---------------------------
Name: {{ .Name }}
Namespace: {{ .Namespace }}
Data:
{{- range $key, $value := .Data }}
{{ $key }} : {{ $value }}
{{- end }}
2 changes: 2 additions & 0 deletions pkg/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"

"github.com/cnoe-io/idpbuilder/pkg/cmd/create"
"github.com/cnoe-io/idpbuilder/pkg/cmd/get"
"github.com/cnoe-io/idpbuilder/pkg/cmd/helpers"
"github.com/cnoe-io/idpbuilder/pkg/cmd/version"
"github.com/spf13/cobra"
Expand All @@ -19,6 +20,7 @@ var rootCmd = &cobra.Command{
func init() {
rootCmd.PersistentFlags().StringVarP(&helpers.LogLevel, "log-level", "l", "info", helpers.LogLevelMsg)
rootCmd.AddCommand(create.CreateCmd)
rootCmd.AddCommand(get.GetCmd)
rootCmd.AddCommand(version.VersionCmd)
}

Expand Down

0 comments on commit 8b1549b

Please sign in to comment.