Compare commits

...

7 Commits

Author SHA1 Message Date
Jeff McCune
3343d226e5 (#14) Fix namespaces "external-secrets" not found
Needed for the `prod-secrets-eso` component to reconcile with flux.

NAME                                    REVISION                SUSPENDED       READY   MESSAGE
flux-system                             main@sha1:28b9ab6b      False           True    Applied revision: main@sha1:28b9ab6b
prod-secrets-eso                        main@sha1:28b9ab6b      False           True    Applied revision: main@sha1:28b9ab6b
prod-secrets-eso-creds-refresher        main@sha1:28b9ab6b      False           True    Applied revision: main@sha1:28b9ab6b
prod-secrets-namespaces                 main@sha1:28b9ab6b      False           True    Applied revision: main@sha1:28b9ab6b
2024-02-26 20:53:43 -08:00
Jeff McCune
f3a9b7cfbc (#10) Additional test coverage for secrets
Also fix a bug, secrets were created with keys that have a sub-directory
which is not a valid kubernetes secret.
2024-02-26 16:58:38 -08:00
Jeff McCune
53b7246d5e (#10) Add tests for holos get secrets command
This patch adds basic test data to run integration level tests on the
holos cli command.  Tests are structured similar to how the go and cue
maintainers test their own cli tools using the testscripts package.

Fixture data is loaded into a fake kubernetes.Clientset.

The holos root command is executed without using a full sub-process so
the fake kubernetes interface persists across multiple holos commands in
the same test case.

The fake kubernetes interface is reset after the testcase script
concludes and a new one starts.

Take care to read and write absolute paths from the test scripts, the
current working directory of the test runner is not set to $WORK when
executing the custom holos command.
2024-02-26 16:16:27 -08:00
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
41 changed files with 1025 additions and 111 deletions

View File

@@ -1,28 +1,10 @@
package main
import (
"context"
"errors"
"github.com/holos-run/holos/pkg/cli"
"github.com/holos-run/holos/pkg/config"
"github.com/holos-run/holos/pkg/wrapper"
"log/slog"
"os"
)
func main() {
cfg := config.New()
slog.SetDefault(cfg.Logger())
ctx := context.Background()
if err := cli.New(cfg).ExecuteContext(ctx); err != nil {
log := cfg.NewTopLevelLogger()
var errAt *wrapper.ErrorAt
const msg = "could not execute"
if ok := errors.As(err, &errAt); ok {
log.ErrorContext(ctx, msg, "err", errAt.Unwrap(), "loc", errAt.Source.Loc())
} else {
log.ErrorContext(ctx, msg, "err", err)
}
os.Exit(1)
}
os.Exit(cli.MakeMain()())
}

20
cmd/holos/main_test.go Normal file
View File

@@ -0,0 +1,20 @@
package main
import (
"github.com/holos-run/holos/pkg/cli"
"github.com/rogpeppe/go-internal/testscript"
"os"
"testing"
)
func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string]func() int{
"holos": cli.MakeMain(),
}))
}
func TestGetSecrets(t *testing.T) {
testscript.Run(t, testscript.Params{
Dir: "testdata",
})
}

5
cmd/holos/testdata/version.txt vendored Normal file
View File

@@ -0,0 +1,5 @@
exec holos --version
# want version with no v on stdout
stdout -count=1 '^\d+\.\d+\.\d+$'
# want nothing on stderr
! stderr .

View File

@@ -1,3 +0,0 @@
package holos
#InputKeys: component: "eso"

View File

@@ -2,6 +2,8 @@ package holos
#TargetNamespace: "external-secrets"
#InputKeys: component: "eso"
#InputKeys: {
project: "secrets"
service: "eso"

View File

@@ -1,6 +1,9 @@
package holos
#Kustomization: spec: dependsOn: [{name: #InstancePrefix + "-namespaces"}]
#Kustomization: spec: {
dependsOn: [{name: #InstancePrefix + "-namespaces"}]
targetNamespace: #TargetNamespace
}
#HelmChart & {
values: installCrds: true

View File

@@ -1,6 +1,7 @@
package holos
#PlatformNamespaces: [
{name: "external-secrets"},
{name: "holos-system"},
{name: "flux-system"},
{name: "ceph-system"},

View File

@@ -79,6 +79,8 @@ _apiVersion: "holos.run/v1alpha1"
kind: string | *"GitRepository"
name: string | *"flux-system"
}
suspend?: bool
targetNamespace?: string
timeout: string | *"3m0s"
wait: bool | *true
}

4
go.mod
View File

@@ -17,6 +17,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/emicklei/proto v1.10.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
@@ -38,7 +39,9 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/oauth2 v0.10.0 // indirect
@@ -54,6 +57,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

8
go.sum
View File

@@ -13,6 +13,8 @@ github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxER
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/proto v1.10.0 h1:pDGyFRVV5RvV+nkBK9iy3q67FBy9Xa7vwrOTE+g5aGw=
github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
@@ -88,12 +90,16 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0=
github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 h1:sadMIsgmHpEOGbUs6VtHBXRR1OHevnj7hLx9ZcdNGW4=
github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c=
github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c h1:fPpdjePK1atuOg28PXfNSqgwf9I/qD1Hlo39JFwKBXk=
github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
@@ -183,6 +189,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
}

35
pkg/cli/main.go Normal file
View File

@@ -0,0 +1,35 @@
package cli
import (
"context"
"errors"
"github.com/holos-run/holos/pkg/holos"
"github.com/holos-run/holos/pkg/wrapper"
"log/slog"
)
// MakeMain makes a main function for the cli or tests.
func MakeMain(options ...holos.Option) func() int {
return func() (exitCode int) {
cfg := holos.New(options...)
slog.SetDefault(cfg.Logger())
ctx := context.Background()
if err := New(cfg).ExecuteContext(ctx); err != nil {
return handleError(ctx, err, cfg)
}
return 0
}
}
// handleError is the top level error handler that unwraps and logs errors.
func handleError(ctx context.Context, err error, hc *holos.Config) (exitCode int) {
log := hc.NewTopLevelLogger()
var errAt *wrapper.ErrorAt
const msg = "could not execute"
if ok := errors.As(err, &errAt); ok {
log.ErrorContext(ctx, msg, "err", errAt.Unwrap(), "loc", errAt.Source.Loc())
} else {
log.ErrorContext(ctx, msg, "err", err)
}
return 1
}

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 {

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

@@ -0,0 +1,128 @@
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{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Labels: map[string]string{NameLabel: secretName},
},
Data: make(secretData),
}
if *cfg.cluster != "" {
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 {
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
}
}

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

@@ -0,0 +1,147 @@
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"
"path/filepath"
"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")
cfg.extractTo = flagSet.String("extract-to", ".", "extract to directory")
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)
}
path := filepath.Join(*cfg.extractTo, name)
if err := os.WriteFile(path, data, 0666); err != nil {
return wrapper.Wrap(fmt.Errorf("could not write %s: %w", path, err))
}
log.InfoContext(ctx, "wrote: "+path, "name", 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
}

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

@@ -0,0 +1,30 @@
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
extractTo *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

@@ -0,0 +1,81 @@
package secret_test
import (
"github.com/holos-run/holos/pkg/cli"
"github.com/holos-run/holos/pkg/holos"
"github.com/rogpeppe/go-internal/testscript"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
"testing"
"time"
)
const clientsetKey = "clientset"
var secret = v1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "k2-talos",
Namespace: "secrets",
Labels: map[string]string{
"holos.run/owner.name": "jeff",
"holos.run/secret.name": "k2-talos",
},
CreationTimestamp: metav1.Time{
Time: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC),
},
},
Data: map[string][]byte{
"secrets.yaml": []byte("content: secret\n"),
},
Type: "Opaque",
}
// cmdHolos executes the holos root command with a kubernetes.Interface that
// persists for the duration of the testscript. holos is NOT executed in a
// subprocess, the current working directory is not and should not be changed.
// Take care to read and write to $WORK in the test scripts using flags.
func cmdHolos(ts *testscript.TestScript, neg bool, args []string) {
clientset, ok := ts.Value(clientsetKey).(kubernetes.Interface)
if clientset == nil || !ok {
ts.Fatalf("missing kubernetes.Interface")
}
cfg := holos.New(
holos.ProvisionerClientset(clientset),
holos.Stdout(ts.Stdout()),
holos.Stderr(ts.Stderr()),
)
cmd := cli.New(cfg)
cmd.SetArgs(args)
err := cmd.Execute()
if neg {
if err == nil {
ts.Fatalf("want: error\nhave: %v", err)
} else {
ts.Logf("want: error\nhave: %v", err)
}
} else {
ts.Check(err)
}
}
func TestSecrets(t *testing.T) {
// Add TestWork: true to the Params to keep the $WORK directory around.
testscript.Run(t, testscript.Params{
Dir: "testdata",
Setup: func(env *testscript.Env) error {
env.Values[clientsetKey] = fake.NewSimpleClientset(&secret)
return nil
},
Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
"holos": cmdHolos,
},
})
}

View File

@@ -0,0 +1,21 @@
# Create the secret
holos create secret directory --from-file=$WORK/fixture --dry-run
# Want no warnings.
! stderr 'WRN'
# Want the data keys
stdout 'one.yaml: Y29udGVudDogb25lCg=='
stdout 'two.yaml: Y29udGVudDogdHdvCg=='
# Want the secret name label.
stdout 'holos.run/secret.name: directory'
# Want the TypeMeta
stdout 'kind: Secret'
stdout 'apiVersion: v1'
-- fixture/one.yaml --
content: one
-- fixture/two.yaml --
content: two

View File

@@ -0,0 +1,22 @@
# Create the secret
holos create secret directory --from-file=$WORK/want
stderr 'created: directory-..........'
stderr 'secret=directory-..........'
stderr 'name=directory'
stderr 'namespace=secrets'
! stderr 'WRN'
# Get the secret back
mkdir have
holos get secret directory --extract-all --extract-to=$WORK/have
stderr 'wrote: .*/have/one.yaml'
stderr 'wrote: .*/have/two.yaml'
# Compare the secrets
cmp want/one.yaml have/one.yaml
cmp want/two.yaml have/two.yaml
-- want/one.yaml --
content: one
-- want/two.yaml --
content: two

View File

@@ -0,0 +1,14 @@
# Create the secret.
holos create secret k3-talos --from-file $WORK/secrets.yaml
# Want info log attributes.
stderr 'created: k3-talos-..........'
stderr 'secret=k3-talos-..........'
stderr 'name=k3-talos'
stderr 'namespace=secrets'
# Want no warnings.
! stderr 'WRN'
-- secrets.yaml --
content: hello

View File

@@ -0,0 +1,14 @@
# Create the secret.
holos create secret k3-talos --namespace=jeff --from-file $WORK/secrets.yaml
stderr 'created: k3-talos-..........'
stderr 'secret=k3-talos-..........'
stderr 'name=k3-talos'
# Want specified namespace.
stderr 'namespace=jeff'
# Want no warnings.
! stderr 'WRN'
-- secrets.yaml --
content: hello

View File

@@ -0,0 +1,24 @@
# Create the secret
holos create secret directory --from-file=$WORK/want
# Get the secret back
mkdir have
holos get secret directory --extract-all --extract-to=$WORK/have
stderr 'wrote: .*/have/one.yaml'
stderr 'wrote: .*/have/two.yaml'
! stderr 'wrote: .*omit.yaml'
# Compare the secrets
cmp want/one.yaml have/one.yaml
cmp want/two.yaml have/two.yaml
# Want no files with depth > 1
! exists have/nope/omit.yaml
! exists have/omit.yaml
-- want/one.yaml --
content: one
-- want/two.yaml --
content: two
-- want/nope/omit.yaml --
content: not included

View File

@@ -0,0 +1,11 @@
# Create the secret.
holos create secret k3-talos --cluster-name=k2 --from-file $WORK/secrets.yaml
stderr 'created: k3-talos-..........'
# Want a warning about the cluster name prefix.
stderr 'missing cluster name prefix'
stderr 'have=k3-talos'
stderr 'want=k2-'
-- secrets.yaml --
content: hello

View File

@@ -0,0 +1,10 @@
# Get and extract the secret
holos get secrets k2-talos --extract-all --extract-to=$WORK
! stdout .
stderr 'wrote: .*/secrets\.yaml'
# Check the secret keys
cmp want.secrets.yaml secrets.yaml
-- want.secrets.yaml --
content: secret

View File

@@ -0,0 +1,3 @@
holos get secrets k2-talos --print-key=secrets.yaml
stdout -count=1 '^content: secret$'
! stderr .

View File

@@ -0,0 +1,3 @@
holos get secrets
stdout '^k2-talos$'
! stderr .

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"
@@ -18,9 +21,11 @@ const DefaultProvisionerNamespace = "secrets"
type Option func(o *options)
type options struct {
stdin io.Reader
stdout io.Writer
stderr io.Writer
stdin io.Reader
stdout io.Writer
stderr io.Writer
provisionerClientset kubernetes.Interface
clientset kubernetes.Interface
}
// Stdin redirects standard input to r, useful for test capture.
@@ -38,6 +43,16 @@ func Stderr(w io.Writer) Option {
return func(o *options) { o.stderr = w }
}
// ProvisionerClientset sets the kubernetes Clientset, useful for test fake.
func ProvisionerClientset(clientset kubernetes.Interface) Option {
return func(o *options) { o.provisionerClientset = clientset }
}
// ClusterClientset sets the kubernetes Clientset, useful for test fake.
func ClusterClientset(clientset *kubernetes.Clientset) Option {
return func(o *options) { o.clientset = clientset }
}
// New returns a new top level cli Config.
func New(opts ...Option) *Config {
cfgOptions := &options{
@@ -53,14 +68,15 @@ func New(opts ...Option) *Config {
kvFlagSet := flag.NewFlagSet("", flag.ContinueOnError)
txFlagSet := flag.NewFlagSet("", flag.ContinueOnError)
cfg := &Config{
logConfig: logger.NewConfig(),
writeTo: getenv("HOLOS_WRITE_TO", "deploy"),
clusterName: getenv("HOLOS_CLUSTER_NAME", ""),
writeFlagSet: writeFlagSet,
clusterFlagSet: clusterFlagSet,
options: cfgOptions,
kvFlagSet: kvFlagSet,
txtarFlagSet: txFlagSet,
logConfig: logger.NewConfig(),
writeTo: getenv("HOLOS_WRITE_TO", "deploy"),
clusterName: getenv("HOLOS_CLUSTER_NAME", ""),
writeFlagSet: writeFlagSet,
clusterFlagSet: clusterFlagSet,
options: cfgOptions,
kvFlagSet: kvFlagSet,
txtarFlagSet: txFlagSet,
provisionerClientset: cfgOptions.provisionerClientset,
}
writeFlagSet.StringVar(&cfg.writeTo, "write-to", cfg.writeTo, "write to directory")
clusterFlagSet.StringVar(&cfg.clusterName, "cluster-name", cfg.clusterName, "cluster name")
@@ -80,19 +96,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.Interface
}
// LogFlagSet returns the logging *flag.FlagSet for use by the command handler.
@@ -224,6 +241,22 @@ func (c *Config) TxtarIndex() int {
return *c.txtarIndex
}
// ProvisionerClientset returns a kubernetes client set for the provisioner cluster.
func (c *Config) ProvisionerClientset() (kubernetes.Interface, 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
3