mirror of
https://github.com/holos-run/holos.git
synced 2026-03-19 16:54:58 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84bf0c8945 | ||
|
|
466b48966a | ||
|
|
84bcf4b2d0 | ||
|
|
bdd76c78a7 | ||
|
|
95e0dfa44a | ||
|
|
90d70a6afa | ||
|
|
d0c2d85246 | ||
|
|
7e637b4647 |
1
go.mod
1
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
||||
@@ -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
37
pkg/cli/command/cmd.go
Normal 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
|
||||
}
|
||||
@@ -1,55 +1,46 @@
|
||||
package cli
|
||||
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/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
|
||||
type getConfig struct {
|
||||
file *string
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
cf := getConfig{}
|
||||
flagSet := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
cf.file = flagSet.String("file", "", "file to print to stdout")
|
||||
|
||||
cmd.Flags().SortFlags = false
|
||||
cmd.RunE = makeKVGetRunFunc(cfg)
|
||||
cmd.Flags().AddGoFlagSet(cfg.ClusterFlagSet())
|
||||
cmd.Flags().AddGoFlagSet(flagSet)
|
||||
cmd.RunE = makeGetRunFunc(cfg, cf)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func makeKVGetRunFunc(cfg *config.Config) runFunc {
|
||||
func makeGetRunFunc(cfg *config.Config, cf getConfig) 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 +48,10 @@ func makeKVGetRunFunc(cfg *config.Config) runFunc {
|
||||
opts := metav1.ListOptions{
|
||||
LabelSelector: NameLabel + "=" + name,
|
||||
}
|
||||
list, err := clientset.CoreV1().Secrets(cfg.KVNamespace()).List(ctx, opts)
|
||||
if name := cfg.ClusterName(); name != "" {
|
||||
opts.LabelSelector += fmt.Sprintf(",%s=%s", ClusterLabel, name)
|
||||
}
|
||||
list, err := cs.CoreV1().Secrets(cfg.KVNamespace()).List(ctx, opts)
|
||||
if err != nil {
|
||||
return wrapper.Wrap(err)
|
||||
}
|
||||
@@ -73,16 +67,28 @@ func makeKVGetRunFunc(cfg *config.Config) 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(ensureNewline(v))
|
||||
cfg.Write(command.EnsureNewline(v))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
44
pkg/cli/kv/kv.go
Normal file
44
pkg/cli/kv/kv.go
Normal 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"
|
||||
"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 {
|
||||
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))
|
||||
cmd.AddCommand(newPutCmd(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
|
||||
}
|
||||
45
pkg/cli/kv/list.go
Normal file
45
pkg/cli/kv/list.go
Normal file
@@ -0,0 +1,45 @@
|
||||
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.Flags().AddGoFlagSet(cfg.ClusterFlagSet())
|
||||
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
|
||||
}
|
||||
}
|
||||
199
pkg/cli/kv/put.go
Normal file
199
pkg/cli/kv/put.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"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"
|
||||
"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 *config.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 *config.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 *config.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}
|
||||
if owner := os.Getenv("USER"); owner != "" {
|
||||
labels[OwnerLabel] = owner
|
||||
}
|
||||
if cluster := cfg.ClusterName(); cluster != "" {
|
||||
labels[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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
103
pkg/cli/txtar.go
103
pkg/cli/txtar.go
@@ -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
|
||||
}
|
||||
95
pkg/cli/txtar/txtar.go
Normal file
95
pkg/cli/txtar/txtar.go
Normal file
@@ -0,0 +1,95 @@
|
||||
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/util"
|
||||
"github.com/holos-run/holos/pkg/wrapper"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/tools/txtar"
|
||||
"io"
|
||||
"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, util.MakeWalkFunc(a)); err != nil {
|
||||
return wrapper.Wrap(err)
|
||||
}
|
||||
}
|
||||
if _, err := cfg.Stdout().Write(txtar.Format(a)); err != nil {
|
||||
return wrapper.Wrap(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
32
pkg/util/txtar.go
Normal file
32
pkg/util/txtar.go
Normal 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
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
43
|
||||
44
|
||||
|
||||
@@ -1 +1 @@
|
||||
1
|
||||
0
|
||||
|
||||
Reference in New Issue
Block a user