Compare commits

..

1 Commits

Author SHA1 Message Date
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
16 changed files with 264 additions and 61 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 {

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
}

View File

@@ -4,7 +4,8 @@ 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"
@@ -16,7 +17,7 @@ type getConfig struct {
file *string
}
func newGetCmd(cfg *config.Config) *cobra.Command {
func newGetCmd(cfg *holos.Config) *cobra.Command {
cmd := command.New("get")
cmd.Args = cobra.MinimumNArgs(1)
cmd.Short = "print secret data in txtar format"
@@ -33,7 +34,7 @@ func newGetCmd(cfg *config.Config) *cobra.Command {
return cmd
}
func makeGetRunFunc(cfg *config.Config, cf getConfig) 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)
@@ -44,12 +45,12 @@ func makeGetRunFunc(cfg *config.Config, cf getConfig) 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", ClusterLabel, name)
opts.LabelSelector += fmt.Sprintf(",%s=%s", secret.ClusterLabel, name)
}
list, err := cs.CoreV1().Secrets(cfg.KVNamespace()).List(ctx, opts)
if err != nil {

View File

@@ -2,19 +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"
const OwnerLabel = "holos.run/secret.owner"
const ClusterLabel = "holos.run/cluster.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
@@ -31,7 +27,7 @@ func New(cfg *config.Config) *cobra.Command {
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,13 +2,14 @@ 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"
@@ -19,21 +20,21 @@ func newListCmd(cfg *config.Config) *cobra.Command {
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
}
}

View File

@@ -6,7 +6,8 @@ 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"
@@ -28,7 +29,7 @@ type putConfig struct {
dryRun *bool
}
func newPutCmd(cfg *config.Config) *cobra.Command {
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"
@@ -47,7 +48,7 @@ func newPutCmd(cfg *config.Config) *cobra.Command {
return cmd
}
func makePutRunFunc(cfg *config.Config, pcfg putConfig) command.RunFunc {
func makePutRunFunc(cfg *holos.Config, pcfg putConfig) command.RunFunc {
return func(cmd *cobra.Command, args []string) error {
a := &txtar.Archive{}
@@ -133,18 +134,18 @@ func makePutRunFunc(cfg *config.Config, pcfg putConfig) command.RunFunc {
}
}
func createSecret(ctx context.Context, cfg *config.Config, pcfg putConfig, a *txtar.Archive, secretName string) (*v1.Secret, error) {
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{NameLabel: secretName}
labels := map[string]string{secret.NameLabel: secretName}
if owner := os.Getenv("USER"); owner != "" {
labels[OwnerLabel] = owner
labels[secret.OwnerLabel] = owner
}
if cluster := cfg.ClusterName(); cluster != "" {
labels[ClusterLabel] = cluster
labels[secret.ClusterLabel] = cluster
}
secret := &v1.Secret{

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,11 @@ 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/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 +14,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 +48,13 @@ 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(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 {

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

@@ -0,0 +1,133 @@
package secret
import (
"flag"
"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"
)
const NameLabel = "holos.run/secret.name"
const OwnerLabel = "holos.run/secret.owner"
const ClusterLabel = "holos.run/cluster.name"
type secretData map[string][]byte
type config struct {
files holos.StringSlice
dryRun *bool
cluster *string
namespace *string
}
func NewCreateCmd(hc *holos.Config) *cobra.Command {
cmd := command.New("secret NAME [--from-file=source]")
cmd.Args = cobra.ExactArgs(1)
cmd.Short = "Create a holos secret from files or directories"
cmd.Flags().SortFlags = false
cfg := &config{}
flagSet := flag.NewFlagSet("", flag.ContinueOnError)
flagSet.Var(&cfg.files, "from-file", "store files as keys in the secret")
cfg.namespace = flagSet.String("namespace", holos.DefaultProvisionerNamespace, "namespace in the provisioner cluster where the secret is created")
cfg.cluster = flagSet.String("cluster-name", "", "cluster name")
cfg.dryRun = flagSet.Bool("dry-run", false, "dry run")
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 {
secretName := args[0]
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Labels: map[string]string{NameLabel: secretName},
},
Data: make(secretData),
}
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)
}
ctx := cmd.Context()
secret, err = cs.CoreV1().
Secrets(*cfg.namespace).
Create(ctx, secret, metav1.CreateOptions{})
if err != nil {
return wrapper.Wrap(err)
}
log := logger.FromContext(ctx)
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
}
}

View File

@@ -4,7 +4,7 @@ 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"
@@ -16,7 +16,7 @@ import (
)
// 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 {
@@ -48,7 +48,7 @@ func makeRunFunc(cfg *config.Config) command.RunFunc {
}
// 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
}

View File

@@ -1 +1 @@
44
45