Compare commits

...

7 Commits

Author SHA1 Message Date
Jeff McCune
466b48966a (#3) holos kv list command
Simple list command that finds the unique holos.run/secret.name label
values and prints them out.

    holos kv list
    k2-flux-system
    k2-talos
    test
2024-02-22 22:06:23 -08:00
Jeff McCune
84bcf4b2d0 Handle write errors when creating an archive 2024-02-22 21:46:41 -08:00
Jeff McCune
bdd76c78a7 Refactor txtar package for readability 2024-02-22 21:42:07 -08:00
Jeff McCune
95e0dfa44a Refactor render cli to a package
Tidy up the structure of the cli package, keep subcommand related
functions grouped together in a package.
2024-02-22 21:20:51 -08:00
Jeff McCune
90d70a6afa Refactor build cli to a package
Tidy up the structure of the cli package, keep subcommand related
functions grouped together in a package.
2024-02-22 21:20:45 -08:00
Jeff McCune
d0c2d85246 (#3) Refactor txtar cli to a package
Tidy up the structure of the cli package, keep txtar related functions
grouped together in a package.
2024-02-22 21:13:40 -08:00
Jeff McCune
7e637b4647 (#3) Refactor kv command to kv package
The structure of the cli package was getting to be a bit of a mess, time
to clean it up.  The structure is much easier to read with each command
in a separate package of related functionality.
2024-02-22 21:09:45 -08:00
10 changed files with 273 additions and 180 deletions

View File

@@ -1,7 +1,8 @@
package cli
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/internal/builder"
"github.com/holos-run/holos/pkg/wrapper"
@@ -10,7 +11,7 @@ import (
)
// makeBuildRunFunc returns the internal implementation of the build cli command
func makeBuildRunFunc(cfg *config.Config) runFunc {
func makeBuildRunFunc(cfg *config.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())
@@ -29,9 +30,9 @@ func makeBuildRunFunc(cfg *config.Config) runFunc {
}
}
// newBuildCmd returns the build subcommand for the root command
func newBuildCmd(cfg *config.Config) *cobra.Command {
cmd := newCmd("build [directory...]")
// New returns the build subcommand for the root command
func New(cfg *config.Config) *cobra.Command {
cmd := command.New("build [directory...]")
cmd.Args = cobra.MinimumNArgs(1)
cmd.Short = "build kubernetes api objects from a directory"
cmd.RunE = makeBuildRunFunc(cfg)

37
pkg/cli/command/cmd.go Normal file
View File

@@ -0,0 +1,37 @@
package command
import (
"fmt"
"github.com/holos-run/holos/pkg/version"
"github.com/holos-run/holos/pkg/wrapper"
"github.com/spf13/cobra"
)
// RunFunc is a cobra.Command RunE function.
type RunFunc func(c *cobra.Command, args []string) error
// New returns a new subcommand
func New(name string) *cobra.Command {
cmd := &cobra.Command{
Use: name,
Version: version.Version,
Args: cobra.NoArgs,
CompletionOptions: cobra.CompletionOptions{
HiddenDefaultCmd: true,
},
RunE: func(c *cobra.Command, args []string) error {
return wrapper.Wrap(fmt.Errorf("could not run %v: not implemented", c.Name()))
},
SilenceUsage: true,
SilenceErrors: true,
}
return cmd
}
// EnsureNewline adds a trailing newline if not already there.
func EnsureNewline(b []byte) []byte {
if len(b) > 0 && b[len(b)-1] != '\n' {
b = append(b, '\n')
}
return b
}

View File

@@ -1,55 +1,33 @@
package cli
package kv
import (
"github.com/holos-run/holos/pkg/cli/command"
"github.com/holos-run/holos/pkg/config"
"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"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"sort"
)
const NameLabel = "holos.run/secret.name"
// newKVRootCmd returns the kv root command for the cli
func newKVRootCmd(cfg *config.Config) *cobra.Command {
cmd := newCmd("kv")
cmd.Short = "work with secrets in the provisioner cluster"
cmd.Flags().SortFlags = false
cmd.RunE = func(c *cobra.Command, args []string) error {
return c.Usage()
}
// flags
cmd.PersistentFlags().SortFlags = false
cmd.PersistentFlags().AddGoFlagSet(cfg.KVFlagSet())
// subcommands
cmd.AddCommand(newKVGetCmd(cfg))
return cmd
}
func newKVGetCmd(cfg *config.Config) *cobra.Command {
cmd := newCmd("get")
func newGetCmd(cfg *config.Config) *cobra.Command {
cmd := command.New("get")
cmd.Args = cobra.MinimumNArgs(1)
cmd.Short = "print secret data in txtar format"
cmd.Flags().SortFlags = false
cmd.RunE = makeKVGetRunFunc(cfg)
cmd.RunE = makeGetRunFunc(cfg)
return cmd
}
func makeKVGetRunFunc(cfg *config.Config) runFunc {
func makeGetRunFunc(cfg *config.Config) command.RunFunc {
return func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
log := logger.FromContext(ctx)
kcfg, err := clientcmd.BuildConfigFromFlags("", cfg.KVKubeconfig())
cs, err := newClientSet(cfg)
if err != nil {
return wrapper.Wrap(err)
}
clientset, err := kubernetes.NewForConfig(kcfg)
if err != nil {
return wrapper.Wrap(err)
return err
}
for _, name := range args {
@@ -57,7 +35,7 @@ func makeKVGetRunFunc(cfg *config.Config) runFunc {
opts := metav1.ListOptions{
LabelSelector: NameLabel + "=" + name,
}
list, err := clientset.CoreV1().Secrets(cfg.KVNamespace()).List(ctx, opts)
list, err := cs.CoreV1().Secrets(cfg.KVNamespace()).List(ctx, opts)
if err != nil {
return wrapper.Wrap(err)
}
@@ -82,7 +60,7 @@ func makeKVGetRunFunc(cfg *config.Config) runFunc {
}
for k, v := range secret.Data {
cfg.Printf("-- %s --\n", k)
cfg.Write(ensureNewline(v))
cfg.Write(command.EnsureNewline(v))
}
}
return nil

41
pkg/cli/kv/kv.go Normal file
View File

@@ -0,0 +1,41 @@
package kv
import (
"github.com/holos-run/holos/pkg/cli/command"
"github.com/holos-run/holos/pkg/config"
"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 {
cmd := command.New("kv")
cmd.Short = "work with secrets in the provisioner cluster"
cmd.Flags().SortFlags = false
cmd.RunE = func(c *cobra.Command, args []string) error {
return c.Usage()
}
// flags
cmd.PersistentFlags().SortFlags = false
cmd.PersistentFlags().AddGoFlagSet(cfg.KVFlagSet())
// subcommands
cmd.AddCommand(newGetCmd(cfg))
cmd.AddCommand(newListCmd(cfg))
return cmd
}
func newClientSet(cfg *config.Config) (*kubernetes.Clientset, error) {
kcfg, err := clientcmd.BuildConfigFromFlags("", cfg.KVKubeconfig())
if err != nil {
return nil, wrapper.Wrap(err)
}
clientset, err := kubernetes.NewForConfig(kcfg)
if err != nil {
return nil, wrapper.Wrap(err)
}
return clientset, nil
}

44
pkg/cli/kv/list.go Normal file
View File

@@ -0,0 +1,44 @@
package kv
import (
"github.com/holos-run/holos/pkg/cli/command"
"github.com/holos-run/holos/pkg/config"
"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 {
cmd := command.New("list")
cmd.Args = cobra.NoArgs
cmd.Short = "list secrets"
cmd.Flags().SortFlags = false
cmd.RunE = makeListRunFunc(cfg)
return cmd
}
func makeListRunFunc(cfg *config.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}
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 {
labels[value] = true
}
}
for label := range labels {
cfg.Println(label)
}
return nil
}
}

View File

@@ -1,7 +1,8 @@
package cli
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/internal/builder"
"github.com/holos-run/holos/pkg/logger"
@@ -9,7 +10,7 @@ import (
"github.com/spf13/cobra"
)
func makeRenderRunFunc(cfg *config.Config) runFunc {
func makeRenderRunFunc(cfg *config.Config) command.RunFunc {
return func(cmd *cobra.Command, args []string) error {
if cfg.ClusterName() == "" {
return wrapper.Wrap(fmt.Errorf("missing cluster name"))
@@ -42,9 +43,9 @@ func makeRenderRunFunc(cfg *config.Config) runFunc {
}
}
// newRenderCmd returns the render subcommand for the root command
func newRenderCmd(cfg *config.Config) *cobra.Command {
cmd := newCmd("render [directory...]")
// New returns the render subcommand for the root command
func New(cfg *config.Config) *cobra.Command {
cmd := command.New("render [directory...]")
cmd.Args = cobra.MinimumNArgs(1)
cmd.Short = "write kubernetes api objects to the filesystem"
cmd.Flags().SortFlags = false

View File

@@ -1,17 +1,17 @@
package cli
import (
"fmt"
"github.com/holos-run/holos/pkg/cli/build"
"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/logger"
"github.com/holos-run/holos/pkg/version"
"github.com/holos-run/holos/pkg/wrapper"
"github.com/spf13/cobra"
"log/slog"
)
type runFunc func(c *cobra.Command, args []string) error
// New returns a new root *cobra.Command for command line execution.
func New(cfg *config.Config) *cobra.Command {
rootCmd := &cobra.Command{
@@ -45,35 +45,10 @@ func New(cfg *config.Config) *cobra.Command {
rootCmd.PersistentFlags().AddGoFlagSet(cfg.LogFlagSet())
// subcommands
rootCmd.AddCommand(newBuildCmd(cfg))
rootCmd.AddCommand(newRenderCmd(cfg))
rootCmd.AddCommand(newKVRootCmd(cfg))
rootCmd.AddCommand(newTxtarCmd(cfg))
rootCmd.AddCommand(build.New(cfg))
rootCmd.AddCommand(render.New(cfg))
rootCmd.AddCommand(kv.New(cfg))
rootCmd.AddCommand(txtar.New(cfg))
return rootCmd
}
// newCmd returns a new subcommand
func newCmd(name string) *cobra.Command {
cmd := &cobra.Command{
Use: name,
Version: version.Version,
Args: cobra.NoArgs,
CompletionOptions: cobra.CompletionOptions{
HiddenDefaultCmd: true,
},
RunE: func(c *cobra.Command, args []string) error {
return wrapper.Wrap(fmt.Errorf("could not run %v: not implemented", c.Name()))
},
SilenceUsage: true,
SilenceErrors: true,
}
return cmd
}
func ensureNewline(b []byte) []byte {
if len(b) > 0 && b[len(b)-1] != '\n' {
b = append(b, '\n')
}
return b
}

View File

@@ -1,103 +0,0 @@
package cli
import (
"bytes"
"fmt"
"github.com/holos-run/holos/pkg/config"
"github.com/holos-run/holos/pkg/wrapper"
"github.com/spf13/cobra"
"golang.org/x/tools/txtar"
"io"
"io/fs"
"os"
"path/filepath"
)
func newTxtarCmd(cfg *config.Config) *cobra.Command {
cmd := newCmd("txtar")
cmd.Short = "trivial text-based file archives"
cmd.Long = "writes arguments to stdout otherwise extracts"
cmd.Args = cobra.MinimumNArgs(0)
cmd.RunE = makeTxtarRun(cfg)
cmd.Flags().SortFlags = false
cmd.Flags().AddGoFlagSet(cfg.TxtarFlagSet())
return cmd
}
func makeTxtarRun(cfg *config.Config) runFunc {
return func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return txExtract(cfg)
}
a := &txtar.Archive{}
for _, name := range args {
if err := filepath.WalkDir(name, makeWalkFunc(a)); err != nil {
return wrapper.Wrap(err)
}
}
cfg.Write(txtar.Format(a))
return nil
}
}
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 := txFile(path); err != nil {
return wrapper.Wrap(err)
} else {
a.Files = append(a.Files, file)
}
}
return nil
}
}
func txFile(path string) (file txtar.File, err error) {
file.Name = path
file.Data, err = os.ReadFile(path)
return
}
func txExtract(cfg *config.Config) error {
input, err := io.ReadAll(cfg.Stdin())
if err != nil {
return wrapper.Wrap(fmt.Errorf("could not read stdin: %w", err))
}
archive := txtar.Parse(input)
header := bytes.Split(archive.Comment, []byte{'\n'})[:1]
if len(header) == 0 {
header = append(header, []byte{})
}
// Print one file to stdout
idx := cfg.TxtarIndex()
if idx > 0 {
cfg.Write(ensureNewline(archive.Files[idx-1].Data))
return nil
}
if idx < 0 {
tail := len(archive.Files)
cfg.Write(ensureNewline(archive.Files[tail+idx].Data))
return nil
}
// Write all files
for _, file := range archive.Files {
log := cfg.Logger().With("header", string(header[0]), "path", file.Name, "bytes", len(file.Data))
path := filepath.Join(".", file.Name)
log.Info("writing: " + file.Name)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return wrapper.Wrap(fmt.Errorf("could not make directory: %w", err))
}
if err := os.WriteFile(path, file.Data, 0644); err != nil {
return wrapper.Wrap(fmt.Errorf("could not write file: %w", err))
}
}
return nil
}

119
pkg/cli/txtar/txtar.go Normal file
View File

@@ -0,0 +1,119 @@
package txtar
import (
"bytes"
"fmt"
"github.com/holos-run/holos/pkg/cli/command"
"github.com/holos-run/holos/pkg/config"
"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 {
cmd := command.New("txtar")
cmd.Short = "trivial text-based file archives"
cmd.Long = "writes arguments to stdout otherwise extracts"
cmd.Args = cobra.MinimumNArgs(0)
cmd.RunE = makeRunFunc(cfg)
cmd.Flags().SortFlags = false
cmd.Flags().AddGoFlagSet(cfg.TxtarFlagSet())
return cmd
}
func makeRunFunc(cfg *config.Config) command.RunFunc {
return func(cmd *cobra.Command, args []string) error {
// extract an archive
if len(args) == 0 {
return extract(cfg)
}
// create an archive
a := &txtar.Archive{}
for _, name := range args {
if err := filepath.WalkDir(name, makeWalkFunc(a)); err != nil {
return wrapper.Wrap(err)
}
}
if _, err := cfg.Stdout().Write(txtar.Format(a)); err != nil {
return wrapper.Wrap(err)
}
return nil
}
}
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 {
input, err := io.ReadAll(cfg.Stdin())
if err != nil {
return wrapper.Wrap(fmt.Errorf("could not read stdin: %w", err))
}
archive := txtar.Parse(input)
if idx := cfg.TxtarIndex(); idx != 0 {
return printFile(cfg.Stdout(), idx, archive)
}
return writeFiles(cfg.Logger(), archive)
}
// printFile prints one file from the txtar archive by index.
func printFile(w io.Writer, idx int, a *txtar.Archive) (err error) {
if idx == 0 {
return wrapper.Wrap(fmt.Errorf("idx cannot be 0"))
}
if idx > 0 {
_, err = w.Write(command.EnsureNewline(a.Files[idx-1].Data))
} else {
_, err = w.Write(command.EnsureNewline(a.Files[len(a.Files)+idx].Data))
}
return
}
// writeFiles writes all files in the archive.
func writeFiles(logger *slog.Logger, a *txtar.Archive) (err error) {
var header string
if h := bytes.Split(a.Comment, []byte{'\n'})[:1]; len(h) > 0 {
header = string(h[0])
}
for _, file := range a.Files {
log := logger.With("header", header, "path", file.Name, "bytes", len(file.Data))
path := filepath.Join(".", file.Name)
log.Info("writing: " + file.Name)
if err = os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return wrapper.Wrap(fmt.Errorf("could not make directory: %w", err))
}
if err = os.WriteFile(path, file.Data, 0644); err != nil {
return wrapper.Wrap(fmt.Errorf("could not write file: %w", err))
}
}
return
}

View File

@@ -1 +1 @@
1
2