Compare commits

...

9 Commits

Author SHA1 Message Date
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
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
Jeff McCune
9bc96d0783 (#3) holos txtar command for provisioner secrets
This patch makes it easy to fetch one or multiple files from a Secret in
the provisioner cluster to address two primary use cases:

 1. Extract files into a temporary directory to provide to other tools.
 2. Print one file to stdout.

For example, the secrets.yaml file necessary to reset a talos cluster is
printed to stdout in txtar format with one command:

    holos kv get k2-talos

The output has the secret name as the comment, then the value of each key of the data
field is printed as the txtar name and data.

    k2-talos-49546d9fd7
    -- secrets.yaml --
    ...

Extracting all of the files in the secret is also simple:

    holos kv get k2-talos | holos txtar
    8:34PM INF txtar.go:94 writing: secrets.yaml version=0.43.0 header=k2-talos-49546d9fd7 path=secrets.yaml bytes=4841

Extracting one file to stdout is also simple:

    holos kv get k2-talos | holos txtar --index=1
2024-02-22 20:38:44 -08:00
14 changed files with 588 additions and 98 deletions

16
go.mod
View File

@@ -6,6 +6,9 @@ require (
cuelang.org/go v0.7.0
github.com/mattn/go-isatty v0.0.20
github.com/spf13/cobra v1.7.0
golang.org/x/tools v0.18.0
k8s.io/apimachinery v0.29.2
k8s.io/client-go v0.29.2
)
require (
@@ -21,7 +24,6 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/imdario/mergo v0.3.6 // indirect
@@ -38,25 +40,21 @@ require (
github.com/opencontainers/image-spec v1.1.0-rc4 // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/oauth2 v0.10.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/term v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.16.1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.29.2 // indirect
k8s.io/apimachinery v0.29.2 // indirect
k8s.io/client-go 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

33
go.sum
View File

@@ -23,6 +23,8 @@ github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -38,6 +40,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
@@ -76,10 +80,15 @@ github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
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/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=
@@ -97,6 +106,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -106,15 +117,15 @@ golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -124,10 +135,10 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -139,8 +150,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -172,6 +183,8 @@ k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
k8s.io/kubectl v0.29.2 h1:uaDYaBhumvkwz0S2XHt36fK0v5IdNgL7HyUniwb2IUo=
k8s.io/kubectl v0.29.2/go.mod h1:BhizuYBGcKaHWyq+G7txGw2fXg576QbPrrnQdQDZgqI=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=

View File

@@ -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,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("secrets").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,25 +67,30 @@ 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
}
}
func ensureNewline(b []byte) []byte {
if len(b) > 0 && b[len(b)-1] != '\n' {
b = append(b, '\n')
}
return b
}

44
pkg/cli/kv/kv.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"
"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
View 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
View 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
}

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,27 +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(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
}

95
pkg/cli/txtar/txtar.go Normal file
View 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
}

View File

@@ -11,15 +11,23 @@ import (
"path/filepath"
)
const DefaultProvisionerNamespace = "secrets"
// An Option configures a Config using [functional
// options](https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html).
type Option func(o *options)
type options struct {
stdin io.Reader
stdout io.Writer
stderr io.Writer
}
// Stdin redirects standard input to r, useful for test capture.
func Stdin(r io.Reader) Option {
return func(o *options) { o.stdin = r }
}
// Stdout redirects standard output to w, useful for test capture.
func Stdout(w io.Writer) Option {
return func(o *options) { o.stdout = w }
@@ -33,6 +41,7 @@ func Stderr(w io.Writer) Option {
// New returns a new top level cli Config.
func New(opts ...Option) *Config {
cfgOptions := &options{
stdin: os.Stdin,
stdout: os.Stdout,
stderr: os.Stderr,
}
@@ -42,6 +51,7 @@ func New(opts ...Option) *Config {
writeFlagSet := flag.NewFlagSet("", flag.ContinueOnError)
clusterFlagSet := flag.NewFlagSet("", flag.ContinueOnError)
kvFlagSet := flag.NewFlagSet("", flag.ContinueOnError)
txFlagSet := flag.NewFlagSet("", flag.ContinueOnError)
cfg := &Config{
logConfig: logger.NewConfig(),
writeTo: getenv("HOLOS_WRITE_TO", "deploy"),
@@ -50,6 +60,7 @@ func New(opts ...Option) *Config {
clusterFlagSet: clusterFlagSet,
options: cfgOptions,
kvFlagSet: kvFlagSet,
txtarFlagSet: txFlagSet,
}
writeFlagSet.StringVar(&cfg.writeTo, "write-to", cfg.writeTo, "write to directory")
clusterFlagSet.StringVar(&cfg.clusterName, "cluster-name", cfg.clusterName, "cluster name")
@@ -57,8 +68,11 @@ func New(opts ...Option) *Config {
if home := homedir.HomeDir(); home != "" {
kvDefault = filepath.Join(home, ".holos", "kubeconfig.provisioner")
}
kvDefault = getenv("HOLOS_KUBECONFIG_PROVISIONER", kvDefault)
cfg.kvKubeconfig = kvFlagSet.String("kubeconfig-provisioner", kvDefault, "absolute path to the provisioner cluster kubeconfig file")
kvDefault = getenv("HOLOS_PROVISIONER_KUBECONFIG", kvDefault)
cfg.kvKubeconfig = kvFlagSet.String("provisioner-kubeconfig", kvDefault, "absolute path to the provisioner cluster kubeconfig file")
ns := getenv("HOLOS_PROVISIONER_NAMESPACE", DefaultProvisionerNamespace)
cfg.kvNamespace = kvFlagSet.String("provisioner-namespace", ns, "namespace in the provisioner cluster")
cfg.txtarIndex = txFlagSet.Int("index", 0, "file number to print if not 0")
return cfg
}
@@ -75,7 +89,10 @@ type Config struct {
writeFlagSet *flag.FlagSet
clusterFlagSet *flag.FlagSet
kvKubeconfig *string
kvNamespace *string
kvFlagSet *flag.FlagSet
txtarIndex *int
txtarFlagSet *flag.FlagSet
}
// LogFlagSet returns the logging *flag.FlagSet for use by the command handler.
@@ -98,6 +115,11 @@ func (c *Config) KVFlagSet() *flag.FlagSet {
return c.kvFlagSet
}
// TxtarFlagSet returns the *flag.FlagSet for txtar related commands.
func (c *Config) TxtarFlagSet() *flag.FlagSet {
return c.txtarFlagSet
}
// Finalize validates the config and finalizes the startup lifecycle based on user configuration.
func (c *Config) Finalize() error {
if c.finalized {
@@ -132,9 +154,9 @@ func (c *Config) NewTopLevelLogger() *slog.Logger {
return c.logConfig.NewTopLevelLogger(c.options.stderr)
}
// Stderr should be used instead of os.Stderr to capture output for tests.
func (c *Config) Stderr() io.Writer {
return c.options.stderr
// Stdin should be used instead of os.Stdin to capture input from tests.
func (c *Config) Stdin() io.Reader {
return c.options.stdin
}
// Stdout should be used instead of os.Stdout to capture output for tests.
@@ -142,6 +164,11 @@ func (c *Config) Stdout() io.Writer {
return c.options.stdout
}
// Stderr should be used instead of os.Stderr to capture output for tests.
func (c *Config) Stderr() io.Writer {
return c.options.stderr
}
// WriteTo returns the write to path configured by flags.
func (c *Config) WriteTo() string {
return c.writeTo
@@ -181,6 +208,22 @@ func (c *Config) KVKubeconfig() string {
return *c.kvKubeconfig
}
// KVNamespace returns the configured namespace to operate against in the provisioner cluster.
func (c *Config) KVNamespace() string {
if c.kvNamespace == nil {
return DefaultProvisionerNamespace
}
return *c.kvNamespace
}
// TxtarIndex returns the
func (c *Config) TxtarIndex() int {
if c.txtarIndex == nil {
return 0
}
return *c.txtarIndex
}
// getenv is equivalent to os.LookupEnv with a default value.
func getenv(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists {

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
44