Compare commits

..

4 Commits

Author SHA1 Message Date
Jeff McCune
c20872c92f v0.45.1 2024-02-24 11:37:03 -08:00
Jeff McCune
ecce1f797e (#8) Get secret subcommand
This patch adds a get secret subcommand.  With no args, lists holos
secrets.  With args, gets each argument.

The use cases are:

 1. Extract specified keys to files with --to-file
 2. Extract all keys to files with --extract-all
 3. Print one key to stdout with --print-key

If no key is specified, the key is implicitly set to the holos secret
name.  This behavior should be preserved as part of the api.
2024-02-24 11:32:48 -08:00
Jeff McCune
0d7033d063 (#8) Create secret subcommand
This patch adds a holos create secret command that behaves like kubectl
create secret, but for the specific use case of provisioning holos
clusters.

```
❯ holos create secret k2-talos --cluster-name=k2 --from-file=secrets.yaml
4:48PM INF secret.go:104 created: k2-talos-49546d9fd7 version=0.45.0 secret=k2-talos-49546d9fd7 name=k2-talos namespace=secrets
```

Once the corresponding `holos get secret` subcommands are implemented
the kv subcommand may be removed.
2024-02-23 16:49:13 -08:00
Jeff McCune
84bf0c8945 (#6) Holos kv put command to create secrets
A "holos secret" is a Secret in the secrets namespace of the provisioner
cluster.  The put command creates a unique secret from files and
directories listed as arguments, or from a txtar archive provided on
standard input.

Secret data may come from any or all of the following sources:

1. Create a secret from raw data on standard input.  --name and --file
   must be specified.
2. Create a secret from txtar data on standard input.  The secret name
   is taken from the --name flag if provided, otherwise is taken from
   the first line of the txtar comment.
3. Create a secret from files and directories specified as arguments.
   The secret name is the base name of the first argument unless it is
   overridden by the --name flag.

This is likely doing too much, really all we care about is this use
case:

holos kv put talosconfig

holos kv get talosconfig | holos txtar

Additionally, I want to get get one command without writing a file:

DATA="$(holos kv get talosconfig --file talosconfig)
2024-02-23 12:03:47 -08:00
23 changed files with 713 additions and 79 deletions

View File

@@ -4,14 +4,14 @@ import (
"context"
"errors"
"github.com/holos-run/holos/pkg/cli"
"github.com/holos-run/holos/pkg/config"
"github.com/holos-run/holos/pkg/holos"
"github.com/holos-run/holos/pkg/wrapper"
"log/slog"
"os"
)
func main() {
cfg := config.New()
cfg := holos.New()
slog.SetDefault(cfg.Logger())
ctx := context.Background()
if err := cli.New(cfg).ExecuteContext(ctx); err != nil {

1
go.mod
View File

@@ -54,6 +54,7 @@ require (
k8s.io/api v0.29.2 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
k8s.io/kubectl v0.29.2 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect

2
go.sum
View File

@@ -183,6 +183,8 @@ k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
k8s.io/kubectl v0.29.2 h1:uaDYaBhumvkwz0S2XHt36fK0v5IdNgL7HyUniwb2IUo=
k8s.io/kubectl v0.29.2/go.mod h1:BhizuYBGcKaHWyq+G7txGw2fXg576QbPrrnQdQDZgqI=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=

View File

@@ -3,7 +3,7 @@ package build
import (
"fmt"
"github.com/holos-run/holos/pkg/cli/command"
"github.com/holos-run/holos/pkg/config"
"github.com/holos-run/holos/pkg/holos"
"github.com/holos-run/holos/pkg/internal/builder"
"github.com/holos-run/holos/pkg/wrapper"
"github.com/spf13/cobra"
@@ -11,7 +11,7 @@ import (
)
// makeBuildRunFunc returns the internal implementation of the build cli command
func makeBuildRunFunc(cfg *config.Config) command.RunFunc {
func makeBuildRunFunc(cfg *holos.Config) command.RunFunc {
return func(cmd *cobra.Command, args []string) error {
build := builder.New(builder.Entrypoints(args), builder.Cluster(cfg.ClusterName()))
results, err := build.Run(cmd.Context())
@@ -31,7 +31,7 @@ func makeBuildRunFunc(cfg *config.Config) command.RunFunc {
}
// New returns the build subcommand for the root command
func New(cfg *config.Config) *cobra.Command {
func New(cfg *holos.Config) *cobra.Command {
cmd := command.New("build [directory...]")
cmd.Args = cobra.MinimumNArgs(1)
cmd.Short = "build kubernetes api objects from a directory"

23
pkg/cli/create/create.go Normal file
View File

@@ -0,0 +1,23 @@
package create
import (
"github.com/holos-run/holos/pkg/cli/command"
"github.com/holos-run/holos/pkg/cli/secret"
"github.com/holos-run/holos/pkg/holos"
"github.com/spf13/cobra"
)
// New returns the create command for the cli
func New(hc *holos.Config) *cobra.Command {
cmd := command.New("create")
cmd.Short = "create resources"
cmd.Flags().SortFlags = false
cmd.RunE = func(c *cobra.Command, args []string) error {
return c.Usage()
}
// flags
cmd.PersistentFlags().SortFlags = false
// commands
cmd.AddCommand(secret.NewCreateCmd(hc))
return cmd
}

23
pkg/cli/get/get.go Normal file
View File

@@ -0,0 +1,23 @@
package get
import (
"github.com/holos-run/holos/pkg/cli/command"
"github.com/holos-run/holos/pkg/cli/secret"
"github.com/holos-run/holos/pkg/holos"
"github.com/spf13/cobra"
)
// New returns the get command for the cli.
func New(hc *holos.Config) *cobra.Command {
cmd := command.New("get")
cmd.Short = "get resources"
cmd.Flags().SortFlags = false
cmd.RunE = func(c *cobra.Command, args []string) error {
return c.Usage()
}
// flags
cmd.PersistentFlags().SortFlags = false
// commands
cmd.AddCommand(secret.NewGetCmd(hc))
return cmd
}

View File

@@ -1,8 +1,11 @@
package kv
import (
"flag"
"fmt"
"github.com/holos-run/holos/pkg/cli/command"
"github.com/holos-run/holos/pkg/config"
"github.com/holos-run/holos/pkg/cli/secret"
"github.com/holos-run/holos/pkg/holos"
"github.com/holos-run/holos/pkg/logger"
"github.com/holos-run/holos/pkg/wrapper"
"github.com/spf13/cobra"
@@ -10,17 +13,28 @@ import (
"sort"
)
func newGetCmd(cfg *config.Config) *cobra.Command {
type getConfig struct {
file *string
}
func newGetCmd(cfg *holos.Config) *cobra.Command {
cmd := command.New("get")
cmd.Args = cobra.MinimumNArgs(1)
cmd.Short = "print secret data in txtar format"
cf := getConfig{}
flagSet := flag.NewFlagSet("", flag.ContinueOnError)
cf.file = flagSet.String("file", "", "file to print to stdout")
cmd.Flags().SortFlags = false
cmd.RunE = makeGetRunFunc(cfg)
cmd.Flags().AddGoFlagSet(cfg.ClusterFlagSet())
cmd.Flags().AddGoFlagSet(flagSet)
cmd.RunE = makeGetRunFunc(cfg, cf)
return cmd
}
func makeGetRunFunc(cfg *config.Config) command.RunFunc {
func makeGetRunFunc(cfg *holos.Config, cf getConfig) command.RunFunc {
return func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
log := logger.FromContext(ctx)
@@ -31,9 +45,12 @@ func makeGetRunFunc(cfg *config.Config) command.RunFunc {
}
for _, name := range args {
nlog := log.With(NameLabel, name)
nlog := log.With(secret.NameLabel, name)
opts := metav1.ListOptions{
LabelSelector: NameLabel + "=" + name,
LabelSelector: secret.NameLabel + "=" + name,
}
if name := cfg.ClusterName(); name != "" {
opts.LabelSelector += fmt.Sprintf(",%s=%s", secret.ClusterLabel, name)
}
list, err := cs.CoreV1().Secrets(cfg.KVNamespace()).List(ctx, opts)
if err != nil {
@@ -51,13 +68,25 @@ func makeGetRunFunc(cfg *config.Config) command.RunFunc {
// most recent secret is the one we want.
secret := list.Items[len(list.Items)-1]
keys := make([]string, 0, len(secret.Data))
for k, v := range secret.Data {
keys = append(keys, k)
nlog.DebugContext(ctx, "data", "name", secret.Name, "key", k, "len", len(v))
}
// Print one file to stdout
if key := *cf.file; key != "" {
if data, found := secret.Data[key]; found {
cfg.Write(command.EnsureNewline(data))
return nil
}
return wrapper.Wrap(fmt.Errorf("not found: %s have %#v", key, keys))
}
if len(secret.Data) > 0 {
cfg.Println(secret.Name)
}
for k, v := range secret.Data {
cfg.Printf("-- %s --\n", k)
cfg.Write(command.EnsureNewline(v))

View File

@@ -2,17 +2,15 @@ package kv
import (
"github.com/holos-run/holos/pkg/cli/command"
"github.com/holos-run/holos/pkg/config"
"github.com/holos-run/holos/pkg/holos"
"github.com/holos-run/holos/pkg/wrapper"
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
const NameLabel = "holos.run/secret.name"
// New returns the kv root command for the cli
func New(cfg *config.Config) *cobra.Command {
func New(cfg *holos.Config) *cobra.Command {
cmd := command.New("kv")
cmd.Short = "work with secrets in the provisioner cluster"
cmd.Flags().SortFlags = false
@@ -25,10 +23,11 @@ func New(cfg *config.Config) *cobra.Command {
// subcommands
cmd.AddCommand(newGetCmd(cfg))
cmd.AddCommand(newListCmd(cfg))
cmd.AddCommand(newPutCmd(cfg))
return cmd
}
func newClientSet(cfg *config.Config) (*kubernetes.Clientset, error) {
func newClientSet(cfg *holos.Config) (*kubernetes.Clientset, error) {
kcfg, err := clientcmd.BuildConfigFromFlags("", cfg.KVKubeconfig())
if err != nil {
return nil, wrapper.Wrap(err)

View File

@@ -2,37 +2,39 @@ package kv
import (
"github.com/holos-run/holos/pkg/cli/command"
"github.com/holos-run/holos/pkg/config"
"github.com/holos-run/holos/pkg/cli/secret"
"github.com/holos-run/holos/pkg/holos"
"github.com/holos-run/holos/pkg/wrapper"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func newListCmd(cfg *config.Config) *cobra.Command {
func newListCmd(cfg *holos.Config) *cobra.Command {
cmd := command.New("list")
cmd.Args = cobra.NoArgs
cmd.Short = "list secrets"
cmd.Flags().SortFlags = false
cmd.Flags().AddGoFlagSet(cfg.ClusterFlagSet())
cmd.RunE = makeListRunFunc(cfg)
return cmd
}
func makeListRunFunc(cfg *config.Config) command.RunFunc {
func makeListRunFunc(cfg *holos.Config) command.RunFunc {
return func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
cs, err := newClientSet(cfg)
if err != nil {
return err
}
selector := metav1.ListOptions{LabelSelector: NameLabel}
selector := metav1.ListOptions{LabelSelector: secret.NameLabel}
secrets, err := cs.CoreV1().Secrets(cfg.KVNamespace()).List(ctx, selector)
if err != nil {
return wrapper.Wrap(err)
}
labels := make(map[string]bool)
for _, secret := range secrets.Items {
if value, ok := secret.Labels[NameLabel]; ok {
for _, s := range secrets.Items {
if value, ok := s.Labels[secret.NameLabel]; ok {
labels[value] = true
}
}

200
pkg/cli/kv/put.go Normal file
View File

@@ -0,0 +1,200 @@
package kv
import (
"bytes"
"context"
"flag"
"fmt"
"github.com/holos-run/holos/pkg/cli/command"
"github.com/holos-run/holos/pkg/cli/secret"
"github.com/holos-run/holos/pkg/holos"
"github.com/holos-run/holos/pkg/logger"
"github.com/holos-run/holos/pkg/wrapper"
"github.com/spf13/cobra"
"golang.org/x/tools/txtar"
"io"
"io/fs"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubectl/pkg/util/hash"
"os"
"path/filepath"
"sigs.k8s.io/yaml"
"strings"
)
type putConfig struct {
secretName *string
file *string
dryRun *bool
}
func newPutCmd(cfg *holos.Config) *cobra.Command {
cmd := command.New("put")
cmd.Args = cobra.MinimumNArgs(0)
cmd.Short = "put a secret from stdin or file args"
cmd.Flags().SortFlags = false
pcfg := putConfig{}
flagSet := flag.NewFlagSet("", flag.ContinueOnError)
pcfg.secretName = flagSet.String("name", "", "secret name to use instead of txtar comment")
pcfg.file = flagSet.String("file", "", "file name to use instead of txtar path")
pcfg.dryRun = flagSet.Bool("dry-run", false, "print to standard output instead of creating")
cmd.Flags().AddGoFlagSet(flagSet)
cmd.Flags().AddGoFlagSet(cfg.ClusterFlagSet())
cmd.RunE = makePutRunFunc(cfg, pcfg)
return cmd
}
func makePutRunFunc(cfg *holos.Config, pcfg putConfig) command.RunFunc {
return func(cmd *cobra.Command, args []string) error {
a := &txtar.Archive{}
// Add stdin to the archive.
if len(args) == 0 {
data, err := io.ReadAll(cfg.Stdin())
if err != nil {
return wrapper.Wrap(err)
}
if *pcfg.file != "" {
file := txtar.File{
Name: *pcfg.file,
Data: data,
}
a.Files = append(a.Files, file)
} else {
a = txtar.Parse(data)
}
}
// Do we have a secret name?
if *pcfg.secretName != "" {
a.Comment = []byte(*pcfg.secretName)
}
if len(a.Comment) == 0 {
// Use the first argument if not
if len(args) > 0 {
a.Comment = []byte(filepath.Base(args[0]))
} else {
err := fmt.Errorf("missing secret name from name, args, or txtar comment")
return wrapper.Wrap(err)
}
}
head, _, _ := bytes.Cut(a.Comment, []byte("\n"))
secretName := string(head)
// Add files from the filesystem to the archive
for _, name := range args {
if err := filepath.WalkDir(name, makeWalkFunc(a, name)); err != nil {
return wrapper.Wrap(err)
}
}
log := logger.FromContext(cmd.Context())
ctx := cmd.Context()
// Nothing to do?
if len(a.Files) == 0 {
log.WarnContext(ctx, "nothing to do")
return nil
}
// Create the secret.
secret, err := createSecret(ctx, cfg, pcfg, a, secretName)
if err != nil {
return wrapper.Wrap(err)
}
if *pcfg.dryRun {
data, err := yaml.Marshal(secret)
if err != nil {
return wrapper.Wrap(err)
}
cfg.Println(string(data))
return nil
}
// Make the API call
cs, err := newClientSet(cfg)
if err != nil {
return wrapper.Wrap(err)
}
secret, err = cs.CoreV1().Secrets(cfg.KVNamespace()).Create(ctx, secret, metav1.CreateOptions{})
if err != nil {
return wrapper.Wrap(err)
}
log.InfoContext(ctx, "created: "+secret.Name, "secret", secret.Name, "name", secretName, "namespace", secret.Namespace)
return nil
}
}
func createSecret(ctx context.Context, cfg *holos.Config, pcfg putConfig, a *txtar.Archive, secretName string) (*v1.Secret, error) {
secretData := make(map[string][]byte)
for _, file := range a.Files {
secretData[file.Name] = file.Data
}
labels := map[string]string{secret.NameLabel: secretName}
if owner := os.Getenv("USER"); owner != "" {
labels[secret.OwnerLabel] = owner
}
if cluster := cfg.ClusterName(); cluster != "" {
labels[secret.ClusterLabel] = cluster
}
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Labels: labels,
},
Data: secretData,
}
secretHash, err := hash.SecretHash(secret)
if err != nil {
return nil, wrapper.Wrap(err)
}
secret.Name = fmt.Sprintf("%s-%s", secret.Name, secretHash)
return secret, nil
}
func makeWalkFunc(a *txtar.Archive, rootDir string) fs.WalkDirFunc {
return func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
// Depth is the count of path separators from the root
depth := strings.Count(path[len(rootDir):], string(filepath.Separator))
if depth > 1 {
if d.IsDir() {
return filepath.SkipDir
}
}
if !d.IsDir() {
if file, err := file(path); err != nil {
return wrapper.Wrap(err)
} else {
file.Name = filepath.Base(path)
a.Files = append(a.Files, file)
}
}
return nil
}
}
func file(path string) (file txtar.File, err error) {
file.Name = path
file.Data, err = os.ReadFile(path)
return
}

View File

@@ -3,14 +3,14 @@ package render
import (
"fmt"
"github.com/holos-run/holos/pkg/cli/command"
"github.com/holos-run/holos/pkg/config"
"github.com/holos-run/holos/pkg/holos"
"github.com/holos-run/holos/pkg/internal/builder"
"github.com/holos-run/holos/pkg/logger"
"github.com/holos-run/holos/pkg/wrapper"
"github.com/spf13/cobra"
)
func makeRenderRunFunc(cfg *config.Config) command.RunFunc {
func makeRenderRunFunc(cfg *holos.Config) command.RunFunc {
return func(cmd *cobra.Command, args []string) error {
if cfg.ClusterName() == "" {
return wrapper.Wrap(fmt.Errorf("missing cluster name"))
@@ -44,7 +44,7 @@ func makeRenderRunFunc(cfg *config.Config) command.RunFunc {
}
// New returns the render subcommand for the root command
func New(cfg *config.Config) *cobra.Command {
func New(cfg *holos.Config) *cobra.Command {
cmd := command.New("render [directory...]")
cmd.Args = cobra.MinimumNArgs(1)
cmd.Short = "write kubernetes api objects to the filesystem"

View File

@@ -2,10 +2,12 @@ package cli
import (
"github.com/holos-run/holos/pkg/cli/build"
"github.com/holos-run/holos/pkg/cli/create"
"github.com/holos-run/holos/pkg/cli/get"
"github.com/holos-run/holos/pkg/cli/kv"
"github.com/holos-run/holos/pkg/cli/render"
"github.com/holos-run/holos/pkg/cli/txtar"
"github.com/holos-run/holos/pkg/config"
"github.com/holos-run/holos/pkg/holos"
"github.com/holos-run/holos/pkg/logger"
"github.com/holos-run/holos/pkg/version"
"github.com/spf13/cobra"
@@ -13,7 +15,7 @@ import (
)
// New returns a new root *cobra.Command for command line execution.
func New(cfg *config.Config) *cobra.Command {
func New(cfg *holos.Config) *cobra.Command {
rootCmd := &cobra.Command{
Use: "holos",
Short: "holos manages a holistic integrated software development platform",
@@ -47,8 +49,14 @@ func New(cfg *config.Config) *cobra.Command {
// subcommands
rootCmd.AddCommand(build.New(cfg))
rootCmd.AddCommand(render.New(cfg))
rootCmd.AddCommand(kv.New(cfg))
rootCmd.AddCommand(get.New(cfg))
rootCmd.AddCommand(create.New(cfg))
// Maybe not needed?
rootCmd.AddCommand(txtar.New(cfg))
// Deprecated, remove?
rootCmd.AddCommand(kv.New(cfg))
return rootCmd
}

View File

@@ -2,7 +2,7 @@ package cli
import (
"bytes"
"github.com/holos-run/holos/pkg/config"
"github.com/holos-run/holos/pkg/holos"
"github.com/holos-run/holos/pkg/logger"
"github.com/holos-run/holos/pkg/version"
"github.com/spf13/cobra"
@@ -13,7 +13,7 @@ import (
func newCommand() (*cobra.Command, *bytes.Buffer) {
var b1, b2 bytes.Buffer
// discard stdout for now, it's a bunch of usage messages.
cmd := New(config.New(config.Stdout(&b1), config.Stderr(&b2)))
cmd := New(holos.New(holos.Stdout(&b1), holos.Stderr(&b2)))
return cmd, &b2
}
@@ -89,7 +89,7 @@ func TestInvalidArgs(t *testing.T) {
}
for _, args := range invalidArgs {
var b bytes.Buffer
cmd := New(config.New(config.Stdout(&b)))
cmd := New(holos.New(holos.Stdout(&b)))
cmd.SetArgs(args)
err := cmd.Execute()
if err == nil {
@@ -114,7 +114,7 @@ func TestLoggerFromContext(t *testing.T) {
func TestVersion(t *testing.T) {
var b bytes.Buffer
cmd := New(config.New(config.Stdout(&b)))
cmd := New(holos.New(holos.Stdout(&b)))
cmd.SetOut(&b)
cmd.SetArgs([]string{"--version"})
if err := cmd.Execute(); err != nil {

124
pkg/cli/secret/create.go Normal file
View File

@@ -0,0 +1,124 @@
package secret
import (
"fmt"
"github.com/holos-run/holos/pkg/cli/command"
"github.com/holos-run/holos/pkg/holos"
"github.com/holos-run/holos/pkg/logger"
"github.com/holos-run/holos/pkg/wrapper"
"github.com/spf13/cobra"
"io/fs"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubectl/pkg/util/hash"
"os"
"path/filepath"
"sigs.k8s.io/yaml"
"strings"
)
func NewCreateCmd(hc *holos.Config) *cobra.Command {
cmd := command.New("secret NAME [--from-file=source]")
cmd.Aliases = []string{"secrets", "sec"}
cmd.Args = cobra.ExactArgs(1)
cmd.Short = "Create a holos secret from files or directories"
cfg, flagSet := newConfig()
flagSet.Var(&cfg.files, "from-file", "store files as keys in the secret")
cfg.dryRun = flagSet.Bool("dry-run", false, "dry run")
cmd.Flags().SortFlags = false
cmd.Flags().AddGoFlagSet(flagSet)
cmd.RunE = makeCreateRunFunc(hc, cfg)
return cmd
}
func makeCreateRunFunc(hc *holos.Config, cfg *config) command.RunFunc {
return func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
log := logger.FromContext(ctx)
secretName := args[0]
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Labels: map[string]string{NameLabel: secretName},
},
Data: make(secretData),
}
clusterPrefix := fmt.Sprintf("%s-", *cfg.cluster)
if !strings.HasPrefix(secretName, clusterPrefix) {
const msg = "missing cluster name prefix"
log.WarnContext(ctx, msg, "have", secretName, "want", clusterPrefix)
}
for _, file := range cfg.files {
if err := filepath.WalkDir(file, makeWalkFunc(secret.Data, file)); err != nil {
return wrapper.Wrap(err)
}
}
if owner := os.Getenv("USER"); owner != "" {
secret.Labels[OwnerLabel] = owner
}
if *cfg.cluster != "" {
secret.Labels[ClusterLabel] = *cfg.cluster
}
if secretHash, err := hash.SecretHash(secret); err != nil {
return wrapper.Wrap(err)
} else {
secret.Name = fmt.Sprintf("%s-%s", secret.Name, secretHash)
}
if *cfg.dryRun {
out, err := yaml.Marshal(secret)
if err != nil {
return wrapper.Wrap(err)
}
hc.Write(out)
return nil
}
cs, err := hc.ProvisionerClientset()
if err != nil {
return wrapper.Wrap(err)
}
secret, err = cs.CoreV1().
Secrets(*cfg.namespace).
Create(ctx, secret, metav1.CreateOptions{})
if err != nil {
return wrapper.Wrap(err)
}
log.InfoContext(ctx, "created: "+secret.Name, "secret", secret.Name, "name", secretName, "namespace", secret.Namespace)
return nil
}
}
func makeWalkFunc(data secretData, root string) fs.WalkDirFunc {
return func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
// Depth is the count of path separators from the root
depth := strings.Count(path[len(root):], string(filepath.Separator))
if depth > 1 {
if d.IsDir() {
return filepath.SkipDir
}
}
if !d.IsDir() {
key := filepath.Base(path)
if data[key], err = os.ReadFile(path); err != nil {
return wrapper.Wrap(err)
}
}
return nil
}
}

144
pkg/cli/secret/get.go Normal file
View File

@@ -0,0 +1,144 @@
package secret
import (
"context"
"fmt"
"github.com/holos-run/holos/pkg/cli/command"
"github.com/holos-run/holos/pkg/holos"
"github.com/holos-run/holos/pkg/logger"
"github.com/holos-run/holos/pkg/wrapper"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"os"
"sort"
)
const printFlagName = "print-key"
func NewGetCmd(hc *holos.Config) *cobra.Command {
cmd := command.New("secrets NAME [--to-file=destination]")
cmd.Aliases = []string{"secret"}
cmd.Args = cobra.MinimumNArgs(0)
cmd.Short = "Get holos secrets from the provisioner cluster"
cfg, flagSet := newConfig()
flagSet.Var(&cfg.files, "to-file", "extract files from the secret")
cfg.printFile = flagSet.String(printFlagName, "", "print one key from the secret")
cfg.extract = flagSet.Bool("extract-all", false, "extract all files from the secret")
cmd.Flags().SortFlags = false
cmd.Flags().AddGoFlagSet(flagSet)
cmd.RunE = makeGetRunFunc(hc, cfg)
return cmd
}
func makeGetRunFunc(hc *holos.Config, cfg *config) command.RunFunc {
return func(cmd *cobra.Command, args []string) error {
namespace := *cfg.namespace
ctx := cmd.Context()
log := logger.FromContext(ctx).With("namespace", namespace)
cs, err := hc.ProvisionerClientset()
if err != nil {
return err
}
// List secrets if no arguments.
if len(args) == 0 {
return listSecrets(cmd.Context(), hc, namespace)
}
// Get each secret.
for _, secretName := range args {
log := log.With(NameLabel, secretName)
opts := metav1.ListOptions{
LabelSelector: fmt.Sprintf("%s=%s", NameLabel, secretName),
}
list, err := cs.CoreV1().Secrets(namespace).List(ctx, opts)
if err != nil {
return wrapper.Wrap(err)
}
log.DebugContext(ctx, "results", "len", len(list.Items))
if len(list.Items) < 1 {
continue
}
// Sort oldest first.
sort.Slice(list.Items, func(i, j int) bool {
return list.Items[i].CreationTimestamp.Before(&list.Items[j].CreationTimestamp)
})
// Get the most recent.
secret := list.Items[len(list.Items)-1]
log = log.With("secret", secret.Name)
// Extract the data keys (file names).
keys := make([]string, 0, len(secret.Data))
for k, v := range secret.Data {
keys = append(keys, k)
log.DebugContext(ctx, "data", "name", secret.Name, "key", k, "len", len(v))
}
// Extract specified files or all files.
toExtract := cfg.files
if *cfg.extract {
toExtract = keys
}
printFile := *cfg.printFile
if len(toExtract) == 0 {
if printFile == "" {
printFile = secretName
}
}
if printFile != "" {
if data, found := secret.Data[printFile]; found {
hc.Write(data)
} else {
err := fmt.Errorf("cannot print: want %s have %v: did you mean --extract-all or --%s=name", printFile, keys, printFlagName)
return wrapper.Wrap(err)
}
}
// Iterate over --to-file values.
for _, name := range toExtract {
data, found := secret.Data[name]
if !found {
err := fmt.Errorf("%s not found in %v", name, keys)
return wrapper.Wrap(err)
}
if err := os.WriteFile(name, data, 0666); err != nil {
return wrapper.Wrap(fmt.Errorf("could not write %s: %w", name, err))
}
log.InfoContext(ctx, "wrote: "+name, "bytes", len(data))
}
}
return nil
}
}
// listSecrets lists holos secrets in the provisioner cluster
func listSecrets(ctx context.Context, hc *holos.Config, namespace string) error {
cs, err := hc.ProvisionerClientset()
if err != nil {
return err
}
selector := metav1.ListOptions{LabelSelector: NameLabel}
secrets, err := cs.CoreV1().Secrets(namespace).List(ctx, selector)
if err != nil {
return wrapper.Wrap(err)
}
secretNames := make(map[string]bool)
for _, secret := range secrets.Items {
if labelValue, ok := secret.Labels[NameLabel]; ok {
secretNames[labelValue] = true
}
}
for secretName := range secretNames {
hc.Println(secretName)
}
return nil
}

29
pkg/cli/secret/secret.go Normal file
View File

@@ -0,0 +1,29 @@
package secret
import (
"flag"
"github.com/holos-run/holos/pkg/holos"
)
const NameLabel = "holos.run/secret.name"
const OwnerLabel = "holos.run/owner.name"
const ClusterLabel = "holos.run/cluster.name"
type secretData map[string][]byte
type config struct {
files holos.StringSlice
printFile *string
extract *bool
dryRun *bool
cluster *string
namespace *string
}
func newConfig() (*config, *flag.FlagSet) {
cfg := &config{}
flagSet := flag.NewFlagSet("", flag.ContinueOnError)
cfg.namespace = flagSet.String("namespace", holos.DefaultProvisionerNamespace, "namespace in the provisioner cluster")
cfg.cluster = flagSet.String("cluster-name", "", "cluster name selector")
return cfg, flagSet
}

View File

@@ -4,19 +4,19 @@ import (
"bytes"
"fmt"
"github.com/holos-run/holos/pkg/cli/command"
"github.com/holos-run/holos/pkg/config"
"github.com/holos-run/holos/pkg/holos"
"github.com/holos-run/holos/pkg/util"
"github.com/holos-run/holos/pkg/wrapper"
"github.com/spf13/cobra"
"golang.org/x/tools/txtar"
"io"
"io/fs"
"log/slog"
"os"
"path/filepath"
)
// New returns a new txtar command.
func New(cfg *config.Config) *cobra.Command {
func New(cfg *holos.Config) *cobra.Command {
cmd := command.New("txtar")
cmd.Short = "trivial text-based file archives"
cmd.Long = "writes arguments to stdout otherwise extracts"
@@ -27,7 +27,7 @@ func New(cfg *config.Config) *cobra.Command {
return cmd
}
func makeRunFunc(cfg *config.Config) command.RunFunc {
func makeRunFunc(cfg *holos.Config) command.RunFunc {
return func(cmd *cobra.Command, args []string) error {
// extract an archive
if len(args) == 0 {
@@ -36,7 +36,7 @@ func makeRunFunc(cfg *config.Config) command.RunFunc {
// create an archive
a := &txtar.Archive{}
for _, name := range args {
if err := filepath.WalkDir(name, makeWalkFunc(a)); err != nil {
if err := filepath.WalkDir(name, util.MakeWalkFunc(a)); err != nil {
return wrapper.Wrap(err)
}
}
@@ -47,32 +47,8 @@ func makeRunFunc(cfg *config.Config) command.RunFunc {
}
}
func makeWalkFunc(a *txtar.Archive) fs.WalkDirFunc {
return func(path string, d os.DirEntry, err error) error {
if err != nil {
return wrapper.Wrap(err)
}
if !d.IsDir() {
if file, err := file(path); err != nil {
return wrapper.Wrap(err)
} else {
a.Files = append(a.Files, file)
}
}
return nil
}
}
func file(path string) (file txtar.File, err error) {
file.Name = path
file.Data, err = os.ReadFile(path)
return
}
// extract files from the configured Stdin to Stdout or the filesystem.
func extract(cfg *config.Config) error {
func extract(cfg *holos.Config) error {
input, err := io.ReadAll(cfg.Stdin())
if err != nil {
return wrapper.Wrap(fmt.Errorf("could not read stdin: %w", err))

View File

@@ -1,10 +1,13 @@
package config
package holos
import (
"flag"
"fmt"
"github.com/holos-run/holos/pkg/logger"
"github.com/holos-run/holos/pkg/wrapper"
"io"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
"log/slog"
"os"
@@ -80,19 +83,20 @@ func New(opts ...Option) *Config {
// should be initialized early at a well known location in the program lifecycle
// then remain immutable.
type Config struct {
logConfig *logger.Config
writeTo string
clusterName string
logger *slog.Logger
options *options
finalized bool
writeFlagSet *flag.FlagSet
clusterFlagSet *flag.FlagSet
kvKubeconfig *string
kvNamespace *string
kvFlagSet *flag.FlagSet
txtarIndex *int
txtarFlagSet *flag.FlagSet
logConfig *logger.Config
writeTo string
clusterName string
logger *slog.Logger
options *options
finalized bool
writeFlagSet *flag.FlagSet
clusterFlagSet *flag.FlagSet
kvKubeconfig *string
kvNamespace *string
kvFlagSet *flag.FlagSet
txtarIndex *int
txtarFlagSet *flag.FlagSet
provisionerClientset *kubernetes.Clientset
}
// LogFlagSet returns the logging *flag.FlagSet for use by the command handler.
@@ -224,6 +228,22 @@ func (c *Config) TxtarIndex() int {
return *c.txtarIndex
}
// ProvisionerClientset returns a kubernetes client set for the provisioner cluster.
func (c *Config) ProvisionerClientset() (*kubernetes.Clientset, error) {
if c.provisionerClientset == nil {
kcfg, err := clientcmd.BuildConfigFromFlags("", c.KVKubeconfig())
if err != nil {
return nil, wrapper.Wrap(err)
}
clientset, err := kubernetes.NewForConfig(kcfg)
if err != nil {
return nil, wrapper.Wrap(err)
}
c.provisionerClientset = clientset
}
return c.provisionerClientset, nil
}
// getenv is equivalent to os.LookupEnv with a default value.
func getenv(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists {

View File

@@ -1,4 +1,4 @@
package config
package holos
import (
"bytes"

22
pkg/holos/types.go Normal file
View File

@@ -0,0 +1,22 @@
package holos
import (
"fmt"
"strings"
)
// StringSlice represents zero or more flag values.
type StringSlice []string
// String implements the flag.Value interface.
func (i *StringSlice) String() string {
return fmt.Sprint(*i)
}
// Set implements the flag.Value interface.
func (i *StringSlice) Set(value string) error {
for _, str := range strings.Split(value, ",") {
*i = append(*i, str)
}
return nil
}

32
pkg/util/txtar.go Normal file
View File

@@ -0,0 +1,32 @@
package util
import (
"github.com/holos-run/holos/pkg/wrapper"
"golang.org/x/tools/txtar"
"io/fs"
"os"
)
func MakeWalkFunc(a *txtar.Archive) fs.WalkDirFunc {
return func(path string, d os.DirEntry, err error) error {
if err != nil {
return wrapper.Wrap(err)
}
if !d.IsDir() {
if file, err := file(path); err != nil {
return wrapper.Wrap(err)
} else {
a.Files = append(a.Files, file)
}
}
return nil
}
}
func file(path string) (file txtar.File, err error) {
file.Name = path
file.Data, err = os.ReadFile(path)
return
}

View File

@@ -1 +1 @@
43
45

View File

@@ -1 +1 @@
2
1