diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..03c48ef --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e8b364a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: ci + +on: + push: + branches: + - "*" + tags: + - 'v*' + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version: '^1.21' + + - run: make release + env: + GH_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..078a5ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +.build/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3bf4124 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +PIPER = go run ./cmd/piper + +DEBUG = 0 +ifeq ($(DEBUG),1) + PIPER := $(PIPER) --log-level=debug +endif + + +build: + $(PIPER) do go build + +release: + $(PIPER) do release diff --git a/cmd/piper/do.go b/cmd/piper/do.go new file mode 100644 index 0000000..53c2521 --- /dev/null +++ b/cmd/piper/do.go @@ -0,0 +1,18 @@ +package main + +import ( + "github.com/innoai-tech/infra/pkg/cli" + + "github.com/octohelm/piper/pkg/engine" + "github.com/octohelm/piper/pkg/logutil" +) + +func init() { + cli.AddTo(App, &Do{}) +} + +type Do struct { + cli.C + logutil.Logger + engine.Pipeline +} diff --git a/cmd/piper/main.go b/cmd/piper/main.go new file mode 100644 index 0000000..6cb4a50 --- /dev/null +++ b/cmd/piper/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "context" + "os" + + "github.com/innoai-tech/infra/pkg/cli" + + "github.com/octohelm/piper/internal/version" +) + +var App = cli.NewApp("piper", version.Version()) + +func main() { + if err := cli.Execute(context.Background(), App, os.Args[1:]); err != nil { + os.Exit(1) + } +} diff --git a/cue.mod/.gitignore b/cue.mod/.gitignore new file mode 100755 index 0000000..f5d0b6d --- /dev/null +++ b/cue.mod/.gitignore @@ -0,0 +1,3 @@ +gen/ +pkg/ +module.sum \ No newline at end of file diff --git a/cue.mod/module.cue b/cue.mod/module.cue new file mode 100755 index 0000000..f2f3442 --- /dev/null +++ b/cue.mod/module.cue @@ -0,0 +1,5 @@ +module: "github.com/octohelm/piper" + +require: { + "piper.octohelm.tech": "v0.0.0" +} diff --git a/cuepkg/piper.octohelm.tech/github/release.cue b/cuepkg/piper.octohelm.tech/github/release.cue new file mode 100644 index 0000000..375485b --- /dev/null +++ b/cuepkg/piper.octohelm.tech/github/release.cue @@ -0,0 +1,148 @@ +package github + +import ( + "path" + "strings" + "strconv" + "encoding/json" + + "piper.octohelm.tech/http" + "piper.octohelm.tech/file" + "piper.octohelm.tech/client" + "piper.octohelm.tech/flow" +) + +#GithubAPI: { + core: "https://api.github.com" + uploads: "https://uploads.github.com" +} + +#Release: { + token: client.#Secret + + owner: string + repo: string + + name: string | *"latest" + notes: string | *"" + prerelease: bool | *true + draft: bool | *false + + assets: [...file.#File] + + _client: #Client & {"token": token} + + _get_or_create_release: { + _ret: flow.#First & { + tasks: [ + _client.#Do & { + method: "GET" + url: "\(#GithubAPI.core)/repos/\(owner)/\(repo)/releases/tags/\(name)" + }, + _client.#Do & { + "method": "POST" + "url": "\(#GithubAPI.core)/repos/\(owner)/\(repo)/releases" + "header": { + "Content-Type": "application/json" + } + "body": json.Marshal({ + "tag_name": name + "name": name + "body": notes + "prerelease": prerelease + "draft": draft + }) + }, + ] + } + + id: _ret.result.data.id + } + + _list_assets: { + _req: _client.#Do & { + method: "GET" + url: "\(#GithubAPI.core)/repos/\(owner)/\(repo)/releases/\(_get_or_create_release.id)/assets" + } + + assets: { + for asset in _req.result.data { + "\(asset.name)": strconv.FormatFloat(asset.id, strings.ByteAt("f", 0), 0, 64) + } + } + } + + _upload_assets: flow.#All & { + tasks: [ + for f in assets { + let assetName = path.Base(f.filename) + + flow.#All & { + tasks: [ + // if asset name exists, delete first + if _list_assets.assets[assetName] != _|_ { + _client.#Do & { + "method": "DELETE" + "url": "\(#GithubAPI.core)/repos/\(owner)/\(repo)/releases/assets/\(_list_assets.assets[assetName])" + } + }, + // then upload + _client.#Do & { + "method": "POST" + "url": "\(#GithubAPI.uploads)/repos/\(owner)/\(repo)/releases/\(_get_or_create_release.id)/assets" + "header": { + "Content-Type": "application/octet-stream" + } + "query": { + "name": "\(assetName)" + } + "body": f + }, + ] + } + }, + ] + } + result: _upload_assets.result +} + +#Client: { + token: client.#Secret + + _token: client.#ReadSecret & { + secret: token + } + + #Do: { + method: string + url: string + body?: file.#StringOrFile + header: [Name=string]: string | [...string] + query: [Name=string]: string | [...string] + + _default_header: { + "Accept": "application/vnd.github+json" + "Authorization": "Bearer \(_token.value)" + "X-GitHub-Api-Version": "2022-11-28" + } + + _req: http.#Do & { + "method": method + "url": url + "header": { + for k, vv in header { + "\(k)": vv + } + for k, vv in _default_header if header[k] == _|_ { + "\(k)": vv + } + } + "query": query + if body != _|_ { + "body": body + } + } + + result: _req.result + } +} diff --git a/cuepkg/pkg.go b/cuepkg/pkg.go new file mode 100644 index 0000000..69ffe4b --- /dev/null +++ b/cuepkg/pkg.go @@ -0,0 +1,96 @@ +package cuepkg + +import ( + "context" + "embed" + "github.com/octohelm/piper/pkg/engine/task" + "github.com/pkg/errors" + "io" + "io/fs" + "os" + "path/filepath" + + "github.com/octohelm/cuemod/pkg/cuemod/stdlib" + "github.com/octohelm/unifs/pkg/filesystem" +) + +//go:embed piper.octohelm.tech +var externalModules embed.FS + +var ( + PiperModule = "piper.octohelm.tech" +) + +func RegistryCueStdlibs() error { + source, err := task.Factory.Sources(context.Background()) + if err != nil { + return err + } + + module, err := createModule(externalModules, filesystem.AsReadDirFS(source)) + if err != nil { + return err + } + + // ugly lock embed version + if err := registerStdlib(filesystem.AsReadDirFS(module), "v0.0.0", PiperModule); err != nil { + return err + } + + return nil +} + +func registerStdlib(fs fs.ReadDirFS, ver string, modules ...string) error { + stdlib.Register(fs, ver, modules...) + return nil +} + +func createModule(otherFs ...fs.ReadDirFS) (filesystem.FileSystem, error) { + mfs := filesystem.NewMemFS() + + ctx := context.Background() + + for _, f := range otherFs { + if err := listFile(f, ".", func(filename string) error { + src, err := f.Open(filename) + if err != nil { + return errors.Wrap(err, "open source file failed") + } + defer src.Close() + + if err := filesystem.MkdirAll(ctx, mfs, filepath.Dir(filename)); err != nil { + return err + } + dest, err := mfs.OpenFile(ctx, filename, os.O_RDWR|os.O_CREATE, os.ModePerm) + if err != nil { + return errors.Wrap(err, "open dest file failed") + } + defer dest.Close() + + if _, err := io.Copy(dest, src); err != nil { + return err + } + return nil + }); err != nil { + return nil, err + } + } + + return mfs, nil +} + +func listFile(f fs.ReadDirFS, root string, each func(filename string) error) error { + return fs.WalkDir(f, root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + rel := path + if root != "" && root != "." { + rel, _ = filepath.Rel(root, path) + } + return each(rel) + }) +} diff --git a/cuepkg/pkg_test.go b/cuepkg/pkg_test.go new file mode 100644 index 0000000..c73388a --- /dev/null +++ b/cuepkg/pkg_test.go @@ -0,0 +1,17 @@ +package cuepkg + +import ( + "github.com/octohelm/piper/pkg/wd" + "testing" +) + +func TestCuePkg(t *testing.T) { + cuepkgs, err := createModule(externalModules) + if err != nil { + t.Fatal(err) + } + + _ = wd.ListFile(cuepkgs, "", func(filename string) error { + return nil + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ed24b0a --- /dev/null +++ b/go.mod @@ -0,0 +1,84 @@ +module github.com/octohelm/piper + +go 1.21 + +replace github.com/k0sproject/rig => github.com/k0sproject/rig v0.15.2-0.20231214092155-eaf96adb3d48 + +require ( + cuelang.org/go v0.7.0 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc + github.com/go-courier/logr v0.3.0 + github.com/innoai-tech/infra v0.0.0-20231124085701-81f94504013c + github.com/k0sproject/rig v0.15.1 + github.com/kevinburke/ssh_config v1.2.0 + github.com/mattn/go-colorable v0.1.13 + github.com/octohelm/cuemod v0.9.0 + github.com/octohelm/gengo v0.0.0-20230809023313-1339e47458a4 + github.com/octohelm/storage v0.0.0-20231213035828-4a91cdd3f879 + github.com/octohelm/unifs v0.0.0-20231219081842-bde4a9d9600a + github.com/octohelm/x v0.0.0-20231115103341-17be3238221d + github.com/opencontainers/go-digest v1.0.0 + github.com/opencontainers/image-spec v1.1.0-rc5 + github.com/pkg/errors v0.9.1 + golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 + golang.org/x/net v0.19.0 +) + +require ( + cuelabs.dev/go/oci/ociregistry v0.0.0-20231205091233-3bb0ee105e4a // indirect + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect + github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/alessio/shellescape v1.4.2 // indirect + github.com/cockroachdb/apd/v3 v3.2.1 // indirect + github.com/creasty/defaults v1.7.0 // indirect + github.com/davidmz/go-pageant v1.0.2 // indirect + github.com/emicklei/proto v1.13.0 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/gofrs/uuid v4.4.0+incompatible // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/goidentity/v6 v6.0.1 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect + github.com/masterzen/winrm v0.0.0-20220917170901-b07f6cb0598d // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect + github.com/onsi/gomega v1.30.0 // indirect + github.com/protocolbuffers/txtpbfmt v0.0.0-20231025115547-084445ff1adf // indirect + github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.16.1 // 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/apiextensions-apiserver v0.29.0 // indirect + k8s.io/apimachinery v0.29.0 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b17ab5c --- /dev/null +++ b/go.sum @@ -0,0 +1,265 @@ +cuelabs.dev/go/oci/ociregistry v0.0.0-20231205091233-3bb0ee105e4a h1:A56kS+m+WAytLYx0MIkFJ/rEA+yI/0LZyIB8SVHOhYA= +cuelabs.dev/go/oci/ociregistry v0.0.0-20231205091233-3bb0ee105e4a/go.mod h1:XGKYSMtsJWfqQYPwq51ZygxAPqpEUj/9bdg16iDPTAA= +cuelang.org/go v0.7.0 h1:gMztinxuKfJwMIxtboFsNc6s8AxwJGgsJV+3CuLffHI= +cuelang.org/go v0.7.0/go.mod h1:ix+3dM/bSpdG9xg6qpCgnJnpeLtciZu+O/rDbywoMII= +github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 h1:w0E0fgc1YafGEh5cROhlROMWXiNoZqApk2PDN0M1+Ns= +github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= +github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA= +github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= +github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= +github.com/emicklei/proto v1.13.0 h1:YtC/om6EdkJ0me1JPw4h2g10k+ELITjYFb7tpzm8i8k= +github.com/emicklei/proto v1.13.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/go-courier/logr v0.3.0 h1:0VEQB1b53EmYQ+ZehrIgD8l2IO+WX7TY+CqzlykIFmo= +github.com/go-courier/logr v0.3.0/go.mod h1:OI7f/JCFZ1ZMD5qG3bIJr5WMNnGzd24+II1D9D9w5x4= +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-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/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk= +github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/innoai-tech/infra v0.0.0-20231124085701-81f94504013c h1:Jw7BA/VwQRArnqrkRI/+CfbEs4f0sNWccOH06VaAkOg= +github.com/innoai-tech/infra v0.0.0-20231124085701-81f94504013c/go.mod h1:FJSk27gnNYlNG6+GT633/EFC5yi05njFeefSFeAhqdw= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/k0sproject/rig v0.15.2-0.20231214092155-eaf96adb3d48 h1:+ZAUH6fgyq4OYj8pMoWpwhoiWH+c54n1mQ2t/ZlrWlc= +github.com/k0sproject/rig v0.15.2-0.20231214092155-eaf96adb3d48/go.mod h1:RE1LUWtdogpfRAu0MS932EJ5bDEjEKK40fF3P68eQkY= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 h1:2ZKn+w/BJeL43sCxI2jhPLRv73oVVOjEKZjKkflyqxg= +github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= +github.com/masterzen/winrm v0.0.0-20220917170901-b07f6cb0598d h1:GXlX1g/AjI3/izilmeMvP/aHWYCuwOZXpJsS0XdGVls= +github.com/masterzen/winrm v0.0.0-20220917170901-b07f6cb0598d/go.mod h1:Iju3u6NzoTAvjuhsGCZc+7fReNnr/Bd6DsWj3WTokIU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9ZPiLVHXz3UFw2+psEX+gYcto= +github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY= +github.com/octohelm/cuemod v0.9.0 h1:1pLT0KKALaGCyX9qEl9/RI2hoRhZBi+9aRI02we8YFg= +github.com/octohelm/cuemod v0.9.0/go.mod h1:tbJx/QnncW0rFFEg8mczmAOg7b+GcBK9UhVzNDfn2tc= +github.com/octohelm/gengo v0.0.0-20230809023313-1339e47458a4 h1:wmmT8Xn6kUwKhzJva3JlFRPVnCTMrdSyVZTp6K0NAtg= +github.com/octohelm/gengo v0.0.0-20230809023313-1339e47458a4/go.mod h1:FmonJDnRb/0KoFSDB/OgptioHK/RBm820MJ7cjeIMaE= +github.com/octohelm/storage v0.0.0-20231213035828-4a91cdd3f879 h1:jUnrUj80QMXGL/NYVd2jWS23qOK9c2STB5QHhzZsEKg= +github.com/octohelm/storage v0.0.0-20231213035828-4a91cdd3f879/go.mod h1:2ITEqPIAZVfceuRSjn/fzxhVdCQN9d+yHJ8TY5cutEs= +github.com/octohelm/unifs v0.0.0-20231219081842-bde4a9d9600a h1:jOwMFyHTfx3HuDZcLm3+SzGxARmm7flj4g63mqyafRM= +github.com/octohelm/unifs v0.0.0-20231219081842-bde4a9d9600a/go.mod h1:xdBavyPaqY7EPcFUHE7rIedDkZNcQCUDA5YfkbCRiSE= +github.com/octohelm/x v0.0.0-20231115103341-17be3238221d h1:lycsfOkujgyuJd1ImFAXDd2fhDgF0R9XpK9m5t2AJYY= +github.com/octohelm/x v0.0.0-20231115103341-17be3238221d/go.mod h1:9rwJtDb1mgLuBdLyG4qRdaJ+gceJ/9vMtTwZffujHqo= +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.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.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-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= +github.com/opencontainers/image-spec v1.1.0-rc5/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/protocolbuffers/txtpbfmt v0.0.0-20231025115547-084445ff1adf h1:014O62zIzQwvoD7Ekj3ePDF5bv9Xxy0w6AZk0qYbjUk= +github.com/protocolbuffers/txtpbfmt v0.0.0-20231025115547-084445ff1adf/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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4= +golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/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.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +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/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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0= +k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc= +k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= +k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= +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/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI= +k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..e955c6d --- /dev/null +++ b/install.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +set -e + +_detect_os() { + os="$(uname)" + case "$os" in + Darwin) echo "darwin" ;; + Linux) echo "linux" ;; + *) + echo "Unsupported system: $os" 1>&2 + return 1 + ;; + esac + unset arch +} + +_detect_arch() { + arch="$(uname -m)" + case "$arch" in + amd64 | x86_64) echo "amd64" ;; + arm64 | aarch64) echo "arm64" ;; + armv7l | armv8l | arm) echo "arm" ;; + *) + echo "Unsupported processor architecture: $arch" 1>&2 + return 1 + ;; + esac + unset arch +} + +_download_url() { + echo "https://github.com/octohelm/piper/releases/download/latest/piper_${OS}_${ARCH}.tar.gz" +} + +OS="$(_detect_os)" +ARCH="$(_detect_arch)" +DOWNLOAD_URL="$(_download_url)" +INSTALL_PATH=/usr/local/bin + +rm -rf /tmp/piper +mkdir -p /tmp/piper +echo "Downloading piper from ${DOWNLOAD_URL}" +wget -c "${DOWNLOAD_URL}" -O - | tar -xz -C "/tmp/piper" +chmod 755 /tmp/piper/piper + +mkdir -p -- "${INSTALL_PATH}" +mv -f /tmp/piper/piper "${INSTALL_PATH}/piper" +echo "$(piper --version) is now executable in ${INSTALL_PATH}" diff --git a/internal/cmd/tool/main.go b/internal/cmd/tool/main.go new file mode 100644 index 0000000..f7c21aa --- /dev/null +++ b/internal/cmd/tool/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "context" + "log/slog" + "os" + + "github.com/innoai-tech/infra/devpkg/gengo" + "github.com/innoai-tech/infra/pkg/cli" + + _ "github.com/octohelm/gengo/devpkg/deepcopygen" + _ "github.com/octohelm/gengo/devpkg/runtimedocgen" + _ "github.com/octohelm/storage/devpkg/enumgen" +) + +var App = cli.NewApp("gengo", "dev") + +func init() { + cli.AddTo(App, &struct { + cli.C `name:"gen"` + gengo.Gengo + }{}) +} + +func main() { + ctx := logr.WithLogger(context.Background(), slog.Logger(slog.Default())) + + if err := cli.Execute(context.Background(), App, os.Args[1:]); err != nil { + panic(err) + } +} diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 0000000..aa2e1c7 --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,9 @@ +package version + +var ( + version = "v0.0.0" +) + +func Version() string { + return version +} diff --git a/piper.cue b/piper.cue new file mode 100644 index 0000000..30f3943 --- /dev/null +++ b/piper.cue @@ -0,0 +1,99 @@ +package main + +import ( + "path" + "strconv" + + "piper.octohelm.tech/wd" + "piper.octohelm.tech/client" + "piper.octohelm.tech/exec" + "piper.octohelm.tech/file" + "piper.octohelm.tech/archive" + "piper.octohelm.tech/github" +) + +hosts: { + local: wd.#Local & { + } +} + +ver: client.#RevInfo & { +} + +actions: go: { + main: "./cmd/piper" + os: [ + "darwin", + "linux", + "windows", + ] + arch: [ + "amd64", + "arm64", + ] + bin: string | *path.Base(main) + + build: { + for _os in os for _arch in arch { + "\(_os)/\(_arch)": { + _build: { + _filename: "./.build/\(bin)_\(_os)_\(_arch)/\(bin)" + + _run: exec.#Run & { + cwd: hosts.local.wd + env: { + CGO_ENABLED: "0" + GOOS: _os + GOARCH: _arch + } + cmd: [ + "go", "build", + "-ldflags", strconv.Quote("-s -w -X github.com/octohelm/piper/internal/version.version=\(ver.version)"), + "-o", _filename, + "\(main)", + ] + } + + output: file.#File & { + cwd: _run.cwd + + if _run.result.ok { + filename: _filename + } + } + } + + _tar_files: wd.#Sub & { + cwd: _build.output.cwd + dir: "./.build/\(bin)_\(_os)_\(_arch)" + } + + _tar: archive.#Tar & { + cwd: hosts.local.wd + filename: "./.build/\(bin)_\(_os)_\(_arch).tar.gz" + dir: _tar_files.wd + } + + output: _tar.output + } + } + } +} + +actions: release: { + _env: client.#Env & { + GH_PASSWORD: client.#Secret + } + + github.#Release & { + owner: "octohelm" + repo: "piper" + token: _env.GH_PASSWORD + prerelease: true + assets: [ + for f in actions.go.build { + f.output + }, + ] + } +} diff --git a/pkg/cueflow/controller.go b/pkg/cueflow/controller.go new file mode 100644 index 0000000..e6f49c3 --- /dev/null +++ b/pkg/cueflow/controller.go @@ -0,0 +1,92 @@ +package cueflow + +import ( + "context" + "cuelang.org/go/cue" + cueerrors "cuelang.org/go/cue/errors" + "cuelang.org/go/tools/flow" + "github.com/go-courier/logr" + "github.com/pkg/errors" + "strings" +) + +type RunTaskOptionFunc func(c *taskController) + +func RunTasks(ctx context.Context, scope Scope, opts ...RunTaskOptionFunc) error { + tr := &taskController{ + taskRunnerResolver: TaskRunnerFactoryContext.From(ctx), + shouldRun: func(value cue.Value) bool { + return value.LookupPath(TaskPath).Exists() + }, + } + + tr.Build(opts...) + + if err := tr.Run(ctx, scope); err != nil { + return err + } + + return nil +} + +func WithShouldRunFunc(shouldRun func(value cue.Value) bool) RunTaskOptionFunc { + return func(c *taskController) { + c.shouldRun = shouldRun + } +} + +func WithPrefix(path cue.Path) RunTaskOptionFunc { + return func(c *taskController) { + c.prefix = path + } +} + +type taskController struct { + taskRunnerResolver TaskRunnerResolver + shouldRun func(value cue.Value) bool + prefix cue.Path +} + +func (fc *taskController) Build(optFns ...RunTaskOptionFunc) { + for _, optFn := range optFns { + optFn(fc) + } +} + +func (fc *taskController) Run(ctx context.Context, scope Scope) error { + return NewFlow(scope, func(cueValue cue.Value) (flow.Runner, error) { + if !(fc.shouldRun(cueValue)) { + return nil, nil + } + + return flow.RunnerFunc(func(t *flow.Task) (err error) { + tk := WrapTask(t, scope) + + tr, err := fc.taskRunnerResolver.ResolveTaskRunner(tk) + if err != nil { + return errors.Wrap(err, "resolve task failed") + } + + ctx := t.Context() + + l := logr.FromContext(ctx). + WithValues("task", fc.trimPrefix(tk.String())) + + ctx = logr.WithLogger(ctx, l) + + if err := tr.Run(ctx); err != nil { + return cueerrors.Wrapf(err, tk.Value().Pos(), "%s run failed", tk.Name()) + } + + return nil + }), nil + }).Run(ctx) +} + +func (fc *taskController) trimPrefix(p string) string { + prefix := fc.prefix.String() + if strings.HasPrefix(p, prefix) { + return p[len(prefix):] + } + return p +} diff --git a/pkg/cueflow/cueify.go b/pkg/cueflow/cueify.go new file mode 100644 index 0000000..ef53bae --- /dev/null +++ b/pkg/cueflow/cueify.go @@ -0,0 +1 @@ +package cueflow diff --git a/pkg/cueflow/cueify/cueify.go b/pkg/cueflow/cueify/cueify.go new file mode 100644 index 0000000..9c0002f --- /dev/null +++ b/pkg/cueflow/cueify/cueify.go @@ -0,0 +1,315 @@ +package cueify + +import ( + "bytes" + "encoding" + "fmt" + "github.com/octohelm/gengo/pkg/camelcase" + "go/ast" + "reflect" + "strings" +) + +type Decl struct { + PkgPath string + Name string + Source []byte + Imports map[string]string +} + +func WithPkgPathReplaceFunc(replace func(pkgPath string) string) OptionFunc { + return func(s *scanner) { + s.pkgPathReplace = replace + } +} + +func WithRegister(register func(t reflect.Type)) OptionFunc { + return func(s *scanner) { + s.register = register + } +} + +type OptionFunc func(s *scanner) + +func FromType(t reflect.Type, optFns ...OptionFunc) *Decl { + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + + s := &scanner{ + pkgPath: t.PkgPath(), + defs: map[reflect.Type]bool{}, + imports: map[string]string{}, + pkgPathReplace: func(pkgPath string) string { + return pkgPath + }, + register: func(t reflect.Type) {}, + } + + for _, optFn := range optFns { + optFn(s) + } + + c := &Decl{ + Name: "#" + t.Name(), + PkgPath: s.pkgPathReplace(t.PkgPath()), + } + + c.Source = s.CueDecl(t, opt{ + naming: c.Name, + }) + + c.Imports = s.imports + + return c +} + +type scanner struct { + pkgPath string + defs map[reflect.Type]bool + imports map[string]string + pkgPathReplace func(pkgPath string) string + register func(t reflect.Type) +} + +type opt struct { + naming string + embed string +} + +var oneOfType = reflect.TypeOf((*OneOfType)(nil)).Elem() +var textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem() + +func (s *scanner) Named(name string, pkgPath string) string { + if pkgPath == s.pkgPath { + return "#" + name + } + + replaced := s.pkgPathReplace(pkgPath) + alias := camelcase.LowerSnakeCase(replaced) + s.imports[replaced] = alias + return alias + ".#" + name +} + +func (s *scanner) CueDecl(tpe reflect.Type, o opt) []byte { + if o.naming == "" && tpe.PkgPath() != "" { + if _, ok := s.defs[tpe]; !ok { + s.defs[tpe] = true + s.register(tpe) + } + + if o.embed != "" { + return []byte(fmt.Sprintf(`%s & { + %s +}`, s.Named(tpe.Name(), tpe.PkgPath()), o.embed)) + } + return []byte(s.Named(tpe.Name(), tpe.PkgPath())) + } + + if tpe.Implements(textMarshalerType) { + return []byte("string") + } + + if tpe.Implements(oneOfType) { + if ot, ok := reflect.New(tpe).Interface().(OneOfType); ok { + types := ot.OneOf() + b := bytes.NewBuffer(nil) + + for i := range types { + t := reflect.TypeOf(types[i]) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if i > 0 { + b.WriteString(" | ") + } + b.Write(s.CueDecl(t, opt{embed: o.embed})) + } + + return b.Bytes() + } + } + + switch tpe.Kind() { + case reflect.Ptr: + return []byte(fmt.Sprintf("%s | null", s.CueDecl(tpe.Elem(), opt{embed: o.embed}))) + case reflect.Map: + return []byte(fmt.Sprintf("[X=%s]: %s", s.CueDecl(tpe.Key(), opt{embed: o.embed}), s.CueDecl(tpe.Elem(), opt{embed: o.embed}))) + case reflect.Slice: + if tpe.Elem().Kind() == reflect.Uint8 { + return []byte("bytes") + } + return []byte(fmt.Sprintf("[...%s]", s.CueDecl(tpe.Elem(), opt{embed: o.embed}))) + case reflect.Struct: + b := bytes.NewBuffer(nil) + + _, _ = fmt.Fprintf(b, `{ +`) + + WalkFields(tpe, func(field *Field) { + t := field.Type + + if field.Inline { + if t.Kind() == reflect.Map { + _, _ = fmt.Fprintf(b, `[!~"\\$\\$task"]: %s`, s.CueDecl(t.Elem(), opt{ + embed: field.Embed, + })) + } + return + } + + if field.Optional { + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + _, _ = fmt.Fprintf(b, "%s?: ", field.Name) + } else { + _, _ = fmt.Fprintf(b, "%s: ", field.Name) + } + + cueType := s.CueDecl(t, opt{ + embed: field.Embed, + }) + + if len(field.Enum) > 0 { + for i, e := range field.Enum { + if i > 0 { + _, _ = fmt.Fprint(b, " | ") + } + _, _ = fmt.Fprintf(b, `%q`, e) + } + } else { + _, _ = fmt.Fprintf(b, "%s", cueType) + } + + if field.DefaultValue != nil { + switch string(cueType) { + case "bytes": + _, _ = fmt.Fprintf(b, ` | *'%s'`, *field.DefaultValue) + case "string": + _, _ = fmt.Fprintf(b, ` | *%q`, *field.DefaultValue) + default: + _, _ = fmt.Fprintf(b, ` | *%v`, *field.DefaultValue) + } + } + + if field.AsOutput { + _, _ = fmt.Fprintf(b, " @generated()") + } + + _, _ = fmt.Fprint(b, "\n") + }) + + if strings.HasSuffix(o.naming, "Interface") { + _, _ = fmt.Fprintf(b, ` +... +`) + } + + _, _ = fmt.Fprintf(b, `}`) + + return b.Bytes() + case reflect.Interface: + return []byte("_") + default: + return []byte(tpe.Kind().String()) + } +} + +type Field struct { + Name string + Embed string + Idx int + Type reflect.Type + AsOutput bool + Optional bool + Inline bool + DefaultValue *string + Enum []string +} + +func (i *Field) EmptyDefaults() (string, bool) { + if i.Type.PkgPath() != "" { + return "", false + } + + switch i.Type.Kind() { + case reflect.Slice: + return "", false + case reflect.Map: + return "", false + case reflect.Interface: + return "", false + default: + return fmt.Sprintf("%v", reflect.New(i.Type).Elem()), true + } +} + +func WalkFields(s reflect.Type, each func(info *Field)) { + if s.Kind() != reflect.Struct { + return + } + + for i := 0; i < s.NumField(); i++ { + f := s.Field(i) + + if !ast.IsExported(f.Name) { + continue + } + + info := &Field{} + info.Idx = i + info.Name = f.Name + info.Type = f.Type + + jsonTag, hasJsonTag := f.Tag.Lookup("json") + if !hasJsonTag { + if f.Anonymous && f.Type.Kind() == reflect.Struct { + WalkFields(f.Type, each) + } + continue + } + + if strings.Contains(jsonTag, ",omitempty") { + info.Optional = true + } + + if embed, hasEmbedTag := f.Tag.Lookup("embed"); hasEmbedTag { + info.Embed = embed + } + + taskTag, hasOutput := f.Tag.Lookup("output") + if jsonTag == "-" && !hasOutput { + continue + } + + if jsonName := strings.SplitN(jsonTag, ",", 2)[0]; jsonName != "" { + info.Name = jsonName + } + + if hasOutput { + attrs := strings.SplitN(taskTag, ",", 2) + + info.AsOutput = true + + if name := attrs[0]; name != "" { + info.Name = name + } + } + + if defaultValue, ok := f.Tag.Lookup("default"); ok { + info.DefaultValue = &defaultValue + } + + if enumValue, ok := f.Tag.Lookup("enum"); ok { + info.Enum = strings.Split(enumValue, ",") + } + + if strings.Contains(jsonTag, ",inline") { + info.Inline = true + info.Name = "" + } + + each(info) + } +} diff --git a/pkg/cueflow/cueify/one_of.go b/pkg/cueflow/cueify/one_of.go new file mode 100644 index 0000000..b736021 --- /dev/null +++ b/pkg/cueflow/cueify/one_of.go @@ -0,0 +1,5 @@ +package cueify + +type OneOfType interface { + OneOf() []any +} diff --git a/pkg/cueflow/flow.go b/pkg/cueflow/flow.go new file mode 100644 index 0000000..6887a6d --- /dev/null +++ b/pkg/cueflow/flow.go @@ -0,0 +1,19 @@ +package cueflow + +import ( + "cuelang.org/go/tools/flow" +) + +func NewFlow(v Scope, taskFunc flow.TaskFunc) *flow.Controller { + return flow.New(&flow.Config{ + FindHiddenTasks: true, + UpdateFunc: func(c *flow.Controller, t *flow.Task) error { + if t != nil { + // when task value changes + // need to put back value to root for using by child tasks + return v.Fill(t.Path(), WrapValue(t.Value())) + } + return nil + }, + }, CueValue(v.Value()), taskFunc) +} diff --git a/pkg/cueflow/runner.go b/pkg/cueflow/runner.go new file mode 100644 index 0000000..fbcd7d9 --- /dev/null +++ b/pkg/cueflow/runner.go @@ -0,0 +1,250 @@ +package cueflow + +import ( + "bytes" + "compress/zlib" + "context" + "encoding/base64" + "fmt" + "io" + "os" + "sort" + "strconv" + "strings" + "sync/atomic" + "text/tabwriter" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/ast" + cueerrors "cuelang.org/go/cue/errors" + "cuelang.org/go/tools/flow" + "github.com/go-courier/logr" + "github.com/pkg/errors" +) + +func NewRunner(value Value) *Runner { + r := &Runner{} + r.root.Store(&scope{Value: value}) + return r +} + +type scope struct { + Value Value +} + +type Runner struct { + root atomic.Pointer[scope] + + target cue.Path + output string + + setups map[string][]string + targets map[string][]string +} + +func (r *Runner) printAllowedTasksTo(w io.Writer, tasks []*flow.Task) { + scope := r.target + + _, _ = fmt.Fprintf(w, ` +Undefined action: + +`) + printSelectors(w, scope.Selectors()[1:]...) + + _, _ = fmt.Fprintf(w, ` +Allowed action: + +`) + + taskSelectors := map[string][]cue.Selector{} + + for _, t := range tasks { + selectors := t.Path().Selectors() + + if selectors[0].String() == "actions" { + publicSelectors := make([]cue.Selector, 0, len(selectors)-1) + + func() { + for _, selector := range selectors[1:] { + if selector.String()[0] == '_' { + return + } + publicSelectors = append(publicSelectors, selector) + } + }() + + taskSelectors[cue.MakePath(publicSelectors...).String()] = publicSelectors + } + } + + keys := make([]string, 0, len(taskSelectors)) + for k := range taskSelectors { + keys = append(keys, k) + } + sort.Strings(keys) + + tw := tabwriter.NewWriter(w, 0, 0, 1, ' ', tabwriter.TabIndent) + defer func() { + _ = tw.Flush() + }() + + for _, k := range keys { + printSelectors(tw, taskSelectors[k]...) + + taskValue := r.Value().(CueValueWrapper).CueValue().LookupPath(cue.ParsePath("actions." + k)) + + if n := taskValue.Source(); n != nil { + for _, c := range ast.Comments(n) { + _, _ = fmt.Fprintf(tw, "\t\t%s", strings.TrimSpace(c.Text())) + } + } + + _, _ = fmt.Fprintln(tw) + } +} + +func printSelectors(w io.Writer, selectors ...cue.Selector) { + for i, s := range selectors { + if i > 0 { + _, _ = fmt.Fprintf(w, ` `) + } + _, _ = fmt.Fprintf(w, `%s`, s.String()) + } +} + +func (r *Runner) resolveDependencies(t *flow.Task, collection map[string][]string) { + p := t.Path().String() + if _, ok := collection[p]; ok { + return + } + + // avoid cycle + collection[p] = make([]string, 0) + + deps := make([]string, 0) + for _, d := range t.Dependencies() { + deps = append(deps, d.Path().String()) + r.resolveDependencies(d, collection) + } + + collection[p] = deps +} + +func (r *Runner) Value() Value { + return r.root.Load().Value +} + +func (r *Runner) Fill(p cue.Path, v Value) error { + r.root.Store(&scope{Value: r.Value().FillPath(p, v)}) + return nil +} + +func (r *Runner) Run(ctx context.Context, action []string) error { + actions := append([]string{"actions"}, action...) + for i := range actions { + actions[i] = strconv.Quote(actions[i]) + } + + r.target = cue.ParsePath(strings.Join(actions, ".")) + + logr.FromContext(ctx).Info("Resolving Tasks") + + f := NewFlow(r, noOpRunner) + + if err := r.prepareTasks(ctx, f.Tasks()); err != nil { + return err + } + + logr.FromContext(ctx).Info("Running") + + if err := RunTasks(ctx, r, WithShouldRunFunc(func(value cue.Value) bool { + _, ok := r.setups[value.Path().String()] + return ok + })); err != nil { + return err + } + + if err := RunTasks(ctx, r, WithShouldRunFunc(func(value cue.Value) bool { + _, ok := r.targets[value.Path().String()] + return ok + })); err != nil { + return err + } + + return nil +} + +func (r *Runner) prepareTasks(ctx context.Context, tasks []*flow.Task) error { + taskRunnerFactory := TaskRunnerFactoryContext.From(ctx) + + r.setups = map[string][]string{} + r.targets = map[string][]string{} + + for i := range tasks { + tk := WrapTask(tasks[i], r) + + t, err := taskRunnerFactory.ResolveTaskRunner(tk) + if err != nil { + return cueerrors.Wrapf(err, tk.Value().Pos(), "resolve task failed") + } + + if _, ok := t.Underlying().(interface{ Setup() bool }); ok { + r.resolveDependencies(tasks[i], r.setups) + } + + if strings.HasPrefix(tk.Path().String(), r.target.String()) { + r.resolveDependencies(tasks[i], r.targets) + } + } + + if r.target.String() != "actions" && len(r.targets) > 0 { + if os.Getenv("GRAPH") != "" { + fmt.Println(printGraph(r.targets)) + } + return nil + } + + buf := bytes.NewBuffer(nil) + r.printAllowedTasksTo(buf, tasks) + return errors.New(buf.String()) +} + +func noOpRunner(cueValue cue.Value) (flow.Runner, error) { + v := cueValue.LookupPath(TaskPath) + + if !v.Exists() { + return nil, nil + } + + // task in slice not be valid task + for _, s := range v.Path().Selectors() { + if s.Type() == cue.IndexLabel { + return nil, nil + } + } + + return flow.RunnerFunc(func(t *flow.Task) error { + return nil + }), nil +} + +func printGraph(targets map[string][]string) (string, error) { + buffer := bytes.NewBuffer(nil) + + w, err := zlib.NewWriterLevel(buffer, 9) + if err != nil { + return "", errors.Wrap(err, "fail to create the w") + } + + _, _ = fmt.Fprintf(w, "direction: right\n") + for name, deps := range targets { + for _, d := range deps { + _, _ = fmt.Fprintf(w, "%q -> %q\n", d, name) + } + } + _ = w.Close() + if err != nil { + return "", errors.Wrap(err, "fail to create the payload") + } + return fmt.Sprintf("https://kroki.io/d2/svg/%s?theme=101", base64.URLEncoding.EncodeToString(buffer.Bytes())), nil +} diff --git a/pkg/cueflow/scope.go b/pkg/cueflow/scope.go new file mode 100644 index 0000000..21a572b --- /dev/null +++ b/pkg/cueflow/scope.go @@ -0,0 +1,8 @@ +package cueflow + +import "cuelang.org/go/cue" + +type Scope interface { + Value() Value + Fill(path cue.Path, value Value) error +} diff --git a/pkg/cueflow/task.go b/pkg/cueflow/task.go new file mode 100644 index 0000000..4c69cb0 --- /dev/null +++ b/pkg/cueflow/task.go @@ -0,0 +1,82 @@ +package cueflow + +import ( + "fmt" + "os" + + "cuelang.org/go/cue" + "cuelang.org/go/tools/flow" +) + +var TaskPath = cue.ParsePath("$$task.name") + +type Task interface { + fmt.Stringer + Name() string + Path() cue.Path + Scope() Scope + Value() Value + Decode(inputs any) error + Fill(values map[string]any) error +} + +type SubTask interface { + SetParent(t Task) + Parent() Task +} + +func WrapTask(t *flow.Task, scope Scope) Task { + name, _ := t.Value().LookupPath(TaskPath).String() + + return &task{ + name: name, + scope: scope, + task: t, + } +} + +type task struct { + name string + scope Scope + task *flow.Task +} + +func (t *task) Path() cue.Path { + return t.task.Path() +} + +func (t *task) Scope() Scope { + return t.scope +} + +func (t *task) Decode(input any) error { + if lv, ok := input.(SubTask); ok { + lv.SetParent(t) + return nil + } + + if err := t.Value().Decode(input); err != nil { + _, _ = fmt.Fprint(os.Stdout, t.Value().Source()) + _, _ = fmt.Fprintln(os.Stdout) + return err + } + + return nil +} + +func (t *task) Name() string { + return t.name +} + +func (t *task) Value() Value { + // always pick value from root + return t.scope.Value().LookupPath(t.task.Path()) +} + +func (t *task) String() string { + return fmt.Sprintf("%s:%s", t.Path(), t.Name()) +} + +func (t *task) Fill(values map[string]any) error { + return t.task.Fill(values) +} diff --git a/pkg/cueflow/task_impl.go b/pkg/cueflow/task_impl.go new file mode 100644 index 0000000..8311e4e --- /dev/null +++ b/pkg/cueflow/task_impl.go @@ -0,0 +1,19 @@ +package cueflow + +type FlowTask interface { + flowTask() +} + +type TaskImpl struct { +} + +func (TaskImpl) flowTask() { +} + +type TaskImplRegister interface { + Register(t any) +} + +type Result interface { + Success() bool +} diff --git a/pkg/cueflow/task_runner.go b/pkg/cueflow/task_runner.go new file mode 100644 index 0000000..62d19ef --- /dev/null +++ b/pkg/cueflow/task_runner.go @@ -0,0 +1,123 @@ +package cueflow + +import ( + "context" + "cuelang.org/go/cue" + "github.com/go-courier/logr" + contextx "github.com/octohelm/x/context" + "github.com/pkg/errors" + "reflect" +) + +var TaskRunnerFactoryContext = contextx.New[TaskRunnerResolver]() + +type TaskRunnerResolver interface { + ResolveTaskRunner(task Task) (TaskRunner, error) +} + +type TaskRunner interface { + Path() cue.Path + Underlying() any + Run(ctx context.Context) error +} + +type StepRunner interface { + Do(ctx context.Context) error +} + +type WithScopeName interface { + ScopeName(ctx context.Context) string +} + +type taskRunner struct { + task Task + inputTaskRunner reflect.Value + outputFields map[string]int +} + +func (t *taskRunner) Underlying() any { + return t.inputTaskRunner.Interface() +} + +func (t *taskRunner) Path() cue.Path { + return t.task.Path() +} + +func (t *taskRunner) Task() Task { + return t.task +} + +func (t *taskRunner) resultValues() map[string]any { + values := map[string]any{} + + rv := t.inputTaskRunner + + if rv.Kind() == reflect.Ptr { + rv = rv.Elem() + } + + for name, i := range t.outputFields { + if name == "" { + f := rv.Field(i) + if f.Kind() == reflect.Map { + for _, k := range f.MapKeys() { + key := k.String() + if key == "$$task" { + continue + } + values[key] = f.MapIndex(k).Interface() + } + } + continue + } + values[name] = rv.Field(i).Interface() + } + + return values +} + +func (t *taskRunner) Run(ctx context.Context) (err error) { + inputStepRunner := t.inputTaskRunner.Interface().(StepRunner) + + if err := t.task.Decode(inputStepRunner); err != nil { + return errors.Wrapf(err, "decode failed") + } + + l := logr.FromContext(ctx) + + if n, ok := inputStepRunner.(WithScopeName); ok { + l = l.WithValues("name", n.ScopeName(ctx)) + } + + l.WithValues("inputs", CueLogValue(inputStepRunner)).Debug("starting") + + if err := inputStepRunner.Do(ctx); err != nil { + + return errors.Wrapf(err, "%T do failed", inputStepRunner) + } + + values := t.resultValues() + + defer func() { + if err != nil { + l.Error(err) + } else { + if result, ok := values["result"].(Result); ok { + if result.Success() { + l.WithValues("result", CueLogValue(result)).Debug("completed.") + } else { + l.WithValues("result", CueLogValue(result)).Error(errors.New("failed.")) + } + } else { + l.Debug("completed.") + } + + } + }() + + if err := t.task.Fill(values); err != nil { + return errors.Wrap(err, "fill result failed") + } + + return nil +} diff --git a/pkg/cueflow/task_runner_factory.go b/pkg/cueflow/task_runner_factory.go new file mode 100644 index 0000000..2cc39c0 --- /dev/null +++ b/pkg/cueflow/task_runner_factory.go @@ -0,0 +1,241 @@ +package cueflow + +import ( + "bytes" + "context" + "fmt" + "github.com/octohelm/unifs/pkg/filesystem" + "github.com/pkg/errors" + "io" + "os" + "path" + "path/filepath" + "reflect" + "sort" + + cueformat "cuelang.org/go/cue/format" + "github.com/octohelm/piper/pkg/cueflow/cueify" +) + +type TaskRunnerFactory interface { + New(task Task) (TaskRunner, error) +} + +type TaskFactory interface { + TaskRunnerResolver + TaskImplRegister + Sources(ctx context.Context) (filesystem.FileSystem, error) +} + +func NewTaskFactory(domain string) TaskFactory { + return &factory{ + domain: domain, + named: map[string]*namedType{}, + } +} + +type factory struct { + domain string + named map[string]*namedType +} + +func (f *factory) Register(t any) { + tpe := reflect.TypeOf(t) + for tpe.Kind() == reflect.Ptr { + tpe = tpe.Elem() + } + f.register(tpe) +} + +func (f *factory) register(tpe reflect.Type) { + block := cueify.FromType(tpe, + cueify.WithPkgPathReplaceFunc(func(pkgPath string) string { + return fmt.Sprintf("%s/%s", f.domain, filepath.Base(pkgPath)) + }), + cueify.WithRegister(f.register), + ) + + pt := &namedType{ + tpe: tpe, + outputFields: map[string]int{}, + decl: block, + } + + if _, ok := reflect.New(tpe).Interface().(FlowTask); ok { + pt.flowTask = true + } + + cueify.WalkFields(tpe, func(info *cueify.Field) { + if info.AsOutput { + pt.outputFields[info.Name] = info.Idx + } + }) + + f.named[pt.FullName()] = pt +} + +func (f *factory) ResolveTaskRunner(task Task) (TaskRunner, error) { + if found, ok := f.named[task.Name()]; ok { + return found.New(task) + } + return nil, fmt.Errorf("unknown task `%s`", task) +} + +type source struct { + pkgName string + imports map[string]string + bytes.Buffer +} + +func (s *source) WriteDecl(named *namedType) { + for k, v := range named.decl.Imports { + s.imports[k] = v + } + + s.WriteString("\n") + + if named.flowTask { + _, _ = fmt.Fprintf(s, `%s: $$task: name: %q +`, named.decl.Name, named.FullName()) + } + + _, _ = fmt.Fprintf(s, `%s: %s +`, named.decl.Name, named.decl.Source) +} + +func (s *source) Source() ([]byte, error) { + b := bytes.NewBufferString("package " + s.pkgName) + + if len(s.imports) > 0 { + _, _ = fmt.Fprintf(b, ` + +import ( +`) + + for e := range SortedIter(context.Background(), s.imports) { + + _, _ = fmt.Fprintf(b, `%s %q +`, e.Value, e.Key) + } + + _, _ = fmt.Fprintf(b, `) +`) + } + + _, _ = io.Copy(b, s) + + data, err := cueformat.Source(b.Bytes(), cueformat.Simplify()) + if err != nil { + return nil, errors.Wrapf(err, `format invalid: + +%s`, b.Bytes()) + } + return data, nil +} + +func (f *factory) Sources(ctx context.Context) (filesystem.FileSystem, error) { + sources := map[string]*source{} + + for nt := range SortedIter(ctx, f.named) { + s, ok := sources[nt.Value.decl.PkgPath] + if !ok { + s = &source{ + pkgName: filepath.Base(nt.Value.decl.PkgPath), + imports: map[string]string{}, + } + sources[nt.Value.decl.PkgPath] = s + } + + s.WriteDecl(nt.Value) + } + + fs := filesystem.NewMemFS() + + for pathPath, s := range sources { + code, err := s.Source() + if err != nil { + return nil, err + } + + if err := WriteFile(ctx, fs, path.Join(pathPath, s.pkgName+".cue"), code); err != nil { + return nil, err + } + } + + return fs, nil +} + +func WriteFile(ctx context.Context, fs filesystem.FileSystem, filename string, data []byte) error { + if err := filesystem.MkdirAll(ctx, fs, filepath.Dir(filename)); err != nil { + return err + } + file, err := fs.OpenFile(ctx, filename, os.O_RDWR|os.O_CREATE, os.ModePerm) + if err != nil { + return err + } + defer file.Close() + if _, err := file.Write(data); err != nil { + return err + } + return nil +} + +type namedType struct { + tpe reflect.Type + flowTask bool + outputFields map[string]int + decl *cueify.Decl +} + +func (nt *namedType) New(planTask Task) (TaskRunner, error) { + r := &taskRunner{ + task: planTask, + inputTaskRunner: reflect.New(nt.tpe), + outputFields: map[string]int{}, + } + + for f, i := range nt.outputFields { + r.outputFields[f] = i + } + + return r, nil +} + +func (nt *namedType) FullName() string { + return fmt.Sprintf("%s.%s", nt.decl.PkgPath, nt.decl.Name) +} + +func SortedIter[V any](ctx context.Context, m map[string]V) <-chan *Element[V] { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + ch := make(chan *Element[V]) + + go func() { + defer func() { + close(ch) + }() + + for _, key := range keys { + select { + case <-ctx.Done(): + return + default: + ch <- &Element[V]{ + Key: key, + Value: m[key], + } + } + } + }() + + return ch +} + +type Element[V any] struct { + Key string + Value V +} diff --git a/pkg/cueflow/value.go b/pkg/cueflow/value.go new file mode 100644 index 0000000..cb3151b --- /dev/null +++ b/pkg/cueflow/value.go @@ -0,0 +1,101 @@ +package cueflow + +import ( + "log/slog" + + "cuelang.org/go/cue" + cueformat "cuelang.org/go/cue/format" + "cuelang.org/go/cue/token" + "encoding/json" +) + +type Value interface { + Path() cue.Path + Pos() token.Pos + + Exists() bool + LookupPath(p cue.Path) Value + FillPath(p cue.Path, v any) Value + + Decode(target any) error + Source(opts ...cue.Option) string +} + +func CueValue(v Value) cue.Value { + if w, ok := v.(CueValueWrapper); ok { + return w.CueValue() + } + return cue.Value{} +} + +type CueValueWrapper interface { + CueValue() cue.Value +} + +func WrapValue(cueValue cue.Value) Value { + return &value{cueValue: cueValue} +} + +type value struct { + cueValue cue.Value +} + +func (val *value) CueValue() cue.Value { + return val.cueValue +} + +func (val *value) Path() cue.Path { + return val.cueValue.Path() +} + +func (val *value) Pos() token.Pos { + return val.cueValue.Pos() +} + +func (val *value) Decode(target any) error { + return val.cueValue.Decode(target) +} + +func (val *value) Source(opts ...cue.Option) string { + syn := val.cueValue.Syntax( + append(opts, + cue.Concrete(false), // allow incomplete values + cue.DisallowCycles(true), + cue.Docs(true), + cue.All(), + )..., + ) + data, _ := cueformat.Node(syn, cueformat.Simplify()) + return string(data) +} + +func (val *value) Exists() bool { + return val.cueValue.Exists() +} + +func (val *value) LookupPath(p cue.Path) Value { + return WrapValue(val.cueValue.LookupPath(p)) +} + +func (val *value) FillPath(p cue.Path, v any) Value { + switch x := v.(type) { + case CueValueWrapper: + return WrapValue(val.cueValue.FillPath(p, x.CueValue())) + default: + return WrapValue(val.cueValue.FillPath(p, x)) + } +} + +func CueLogValue(v any) slog.LogValuer { + return &logValue{v: v} +} + +type logValue struct { + v any +} + +func (c *logValue) LogValue() slog.Value { + data, _ := json.MarshalIndent(c.v, "", " ") + data, _ = cueformat.Source(data, cueformat.Simplify()) + return slog.AnyValue(data) +} diff --git a/pkg/engine/pipleline.go b/pkg/engine/pipleline.go new file mode 100644 index 0000000..7e1905c --- /dev/null +++ b/pkg/engine/pipleline.go @@ -0,0 +1,39 @@ +package engine + +import ( + "bytes" + cueerrors "cuelang.org/go/cue/errors" + "github.com/pkg/errors" + "golang.org/x/net/context" +) + +type Pipeline struct { + Action []string `arg:""` + Plan string `flag:",omitempty" alias:"p"` +} + +func (c *Pipeline) SetDefaults() { + if c.Plan == "" { + c.Plan = "./piper.cue" + } +} + +func (c *Pipeline) Run(ctx context.Context) error { + p, err := New(ctx, WithPlan(c.Plan)) + if err != nil { + return err + } + + if err := p.Run(ctx, c.Action...); err != nil { + if errList := cueerrors.Errors(err); len(errList) > 0 { + buf := bytes.NewBuffer(nil) + for i := range errList { + cueerrors.Print(buf, errList[i], nil) + } + return errors.New(buf.String()) + } + return err + } + + return nil +} diff --git a/pkg/engine/project.go b/pkg/engine/project.go new file mode 100644 index 0000000..47b3040 --- /dev/null +++ b/pkg/engine/project.go @@ -0,0 +1,101 @@ +package engine + +import ( + "os" + "path" + "strings" + + "cuelang.org/go/cue/build" + "cuelang.org/go/cue/cuecontext" + cueload "cuelang.org/go/cue/load" + "github.com/octohelm/cuemod/pkg/cuemod" + "github.com/pkg/errors" + "golang.org/x/net/context" + + "github.com/octohelm/piper/cuepkg" + "github.com/octohelm/piper/pkg/cueflow" + "github.com/octohelm/piper/pkg/engine/task" + + _ "github.com/octohelm/piper/pkg/engine/task/archive" + _ "github.com/octohelm/piper/pkg/engine/task/client" + _ "github.com/octohelm/piper/pkg/engine/task/exec" + _ "github.com/octohelm/piper/pkg/engine/task/file" + _ "github.com/octohelm/piper/pkg/engine/task/http" + _ "github.com/octohelm/piper/pkg/engine/task/wd" +) + +func init() { + if err := cuepkg.RegistryCueStdlibs(); err != nil { + panic(err) + } +} + +type Project interface { + Run(ctx context.Context, action ...string) error +} + +type option struct { + entryFile string +} + +type OptFunc = func(o *option) + +func WithPlan(root string) OptFunc { + return func(o *option) { + o.entryFile = root + } +} + +func New(ctx context.Context, opts ...OptFunc) (Project, error) { + c := &project{} + for i := range opts { + opts[i](&c.opt) + } + + cwd, _ := os.Getwd() + sourceRoot := path.Join(cwd, c.opt.entryFile) + + if strings.HasSuffix(sourceRoot, ".cue") { + sourceRoot = path.Dir(sourceRoot) + } + + c.sourceRoot = sourceRoot + + buildConfig := cuemod.ContextFor(cwd).BuildConfig(ctx) + + instances := cueload.Instances([]string{c.opt.entryFile}, buildConfig) + if len(instances) != 1 { + return nil, errors.New("only one package is supported at a time") + } + c.instance = instances[0] + + if err := c.instance.Err; err != nil { + return nil, err + } + + return c, nil +} + +type project struct { + opt option + sourceRoot string + instance *build.Instance +} + +func (p *project) Root() string { + return p.sourceRoot +} + +func (p *project) Run(ctx context.Context, action ...string) error { + val := cuecontext.New().BuildInstance(p.instance) + if err := val.Err(); err != nil { + return err + } + + runner := cueflow.NewRunner(cueflow.WrapValue(val)) + + ctx = cueflow.TaskRunnerFactoryContext.Inject(ctx, task.Factory) + ctx = task.ClientContext.Inject(ctx, p) + + return runner.Run(ctx, action) +} diff --git a/pkg/engine/task/archive/tar.go b/pkg/engine/task/archive/tar.go new file mode 100644 index 0000000..d05eb25 --- /dev/null +++ b/pkg/engine/task/archive/tar.go @@ -0,0 +1,101 @@ +package archive + +import ( + "archive/tar" + "compress/gzip" + "context" + "fmt" + "github.com/go-courier/logr" + "io" + "os" + "path/filepath" + "strings" + + "github.com/octohelm/piper/pkg/engine/task" + "github.com/octohelm/piper/pkg/engine/task/file" + taskwd "github.com/octohelm/piper/pkg/engine/task/wd" + "github.com/octohelm/piper/pkg/wd" + "github.com/octohelm/unifs/pkg/filesystem" +) + +func init() { + task.Factory.Register(&Tar{}) +} + +type Tar struct { + task.Task + + taskwd.CurrentWorkDir + + Filename string `json:"filename"` + + Dir taskwd.WorkDir `json:"dir"` + + Output file.File `json:"-" output:"output"` +} + +func (e *Tar) Do(ctx context.Context) error { + return e.Cwd.Do(ctx, func(ctx context.Context, cwd wd.WorkDir) (err error) { + if err := filesystem.MkdirAll(ctx, cwd, filepath.Dir(e.Filename)); err != nil { + return err + } + + tarFile, err := cwd.OpenFile(ctx, e.Filename, os.O_TRUNC|os.O_RDWR|os.O_CREATE, os.ModePerm) + if err != nil { + return err + } + defer tarFile.Close() + defer func() { + if err == nil { + e.Output = file.File{ + Cwd: e.Cwd, + Filename: e.Filename, + } + + logr.FromContext(ctx).Info(fmt.Sprintf("%s created.", e.Output.Filename)) + } + }() + + return e.Dir.Do(ctx, func(ctx context.Context, contents wd.WorkDir) error { + var w io.WriteCloser = tarFile + + if strings.HasSuffix(e.Filename, ".gz") { + w = gzip.NewWriter(w) + defer func() { + _ = w.Close() + }() + } + + tw := tar.NewWriter(w) + defer func() { + _ = tw.Close() + }() + + err := wd.ListFile(contents, ".", func(filename string) error { + s, err := contents.Stat(ctx, filename) + if err != nil { + return err + } + if err := tw.WriteHeader(&tar.Header{ + Name: filename, + Size: s.Size(), + Mode: int64(s.Mode()), + ModTime: s.ModTime(), + }); err != nil { + return err + } + + f, err := contents.OpenFile(ctx, filename, os.O_RDONLY, os.ModePerm) + if err != nil { + return err + } + _, err = io.Copy(tw, f) + return err + }) + if err != nil { + return err + } + return nil + }) + }) +} diff --git a/pkg/engine/task/client.go b/pkg/engine/task/client.go new file mode 100644 index 0000000..f874be3 --- /dev/null +++ b/pkg/engine/task/client.go @@ -0,0 +1,9 @@ +package task + +import contextx "github.com/octohelm/x/context" + +var ClientContext = contextx.New[Client]() + +type Client interface { + Root() string +} diff --git a/pkg/engine/task/client/env.go b/pkg/engine/task/client/env.go new file mode 100644 index 0000000..48c3b94 --- /dev/null +++ b/pkg/engine/task/client/env.go @@ -0,0 +1,97 @@ +package client + +import ( + "context" + "encoding/json" + "os" + "strings" + "sync" + + "github.com/pkg/errors" + + "github.com/octohelm/piper/pkg/cueflow" + "github.com/octohelm/piper/pkg/engine/task" +) + +func init() { + task.Factory.Register(&Env{}) +} + +type Env struct { + cueflow.TaskImpl + + Env map[string]SecretOrString `json:",inline" output:"env"` +} + +func (ce *Env) UnmarshalJSON(data []byte) error { + if err := json.Unmarshal(data, &ce.Env); err != nil { + return err + } + + for k := range ce.Env { + if strings.HasPrefix(k, "$$") { + delete(ce.Env, k) + } + } + + return nil +} + +func (ce *Env) MarshalJSON() ([]byte, error) { + + return json.Marshal(ce.Env) +} + +func (ce *Env) Do(ctx context.Context) error { + secretStore := task.SecretContext.From(ctx) + + clientEnvs := getClientEnvs() + + env := map[string]SecretOrString{} + + for key := range ce.Env { + e := ce.Env[key] + + if envVar, ok := clientEnvs[key]; ok { + if secret := e.Secret; secret != nil { + id := secretStore.Set(task.Secret{ + Key: key, + Value: envVar, + }) + + env[key] = SecretOrString{ + Secret: SecretOfID(id), + } + } else { + env[key] = SecretOrString{ + Value: envVar, + } + } + } else { + if secret := e.Secret; secret != nil { + return errors.Errorf("EnvVar %s is not defined.", key) + } + + env[key] = e + } + } + + ce.Env = env + + return nil +} + +var getClientEnvs = sync.OnceValue(func() map[string]string { + clientEnvs := map[string]string{} + + for _, i := range os.Environ() { + parts := strings.SplitN(i, "=", 2) + if len(parts) == 2 { + clientEnvs[parts[0]] = parts[1] + } else { + clientEnvs[parts[0]] = "" + } + } + + return clientEnvs +}) diff --git a/pkg/engine/task/client/read_secret.go b/pkg/engine/task/client/read_secret.go new file mode 100644 index 0000000..7b063b5 --- /dev/null +++ b/pkg/engine/task/client/read_secret.go @@ -0,0 +1,30 @@ +package client + +import ( + "context" + + "github.com/octohelm/piper/pkg/cueflow" + "github.com/octohelm/piper/pkg/engine/task" + "github.com/pkg/errors" +) + +func init() { + task.Factory.Register(&ReadSecret{}) +} + +type ReadSecret struct { + cueflow.TaskImpl + + Secret Secret `json:"secret"` + + Value string `json:"-" output:"value"` +} + +func (e *ReadSecret) Do(ctx context.Context) error { + s, ok := e.Secret.Value(ctx) + if ok { + e.Value = s.Value + return nil + } + return errors.New("secret not found") +} diff --git a/pkg/engine/task/client/rev_info.go b/pkg/engine/task/client/rev_info.go new file mode 100644 index 0000000..18ae3a8 --- /dev/null +++ b/pkg/engine/task/client/rev_info.go @@ -0,0 +1,28 @@ +package client + +import ( + "context" + + "github.com/octohelm/cuemod/pkg/modutil" + "github.com/octohelm/piper/pkg/engine/task" +) + +func init() { + task.Factory.Register(&RevInfo{}) +} + +type RevInfo struct { + task.SetupTask + + Version string `json:"-" output:"version"` +} + +func (e *RevInfo) Do(ctx context.Context) error { + planRoot := task.ClientContext.From(ctx).Root() + r, err := modutil.RevInfoFromDir(context.Background(), planRoot) + if err != nil { + return err + } + e.Version = r.Version + return nil +} diff --git a/pkg/engine/task/client/secret.go b/pkg/engine/task/client/secret.go new file mode 100644 index 0000000..faf0272 --- /dev/null +++ b/pkg/engine/task/client/secret.go @@ -0,0 +1,26 @@ +package client + +import ( + "context" + "github.com/octohelm/piper/pkg/engine/task" +) + +func init() { + task.Factory.Register(&Secret{}) +} + +func SecretOfID(id string) *Secret { + s := &Secret{} + s.Ref.ID = id + return s +} + +type Secret struct { + Ref struct { + ID string `json:"id,omitempty"` + } `json:"$$secret"` +} + +func (s *Secret) Value(ctx context.Context) (task.Secret, bool) { + return task.SecretContext.From(ctx).Get(s.Ref.ID) +} diff --git a/pkg/engine/task/client/secret_or_string.go b/pkg/engine/task/client/secret_or_string.go new file mode 100644 index 0000000..6824782 --- /dev/null +++ b/pkg/engine/task/client/secret_or_string.go @@ -0,0 +1,34 @@ +package client + +import "encoding/json" + +type SecretOrString struct { + Secret *Secret + Value string +} + +func (SecretOrString) OneOf() []any { + return []any{ + "", + &Secret{}, + } +} + +func (s *SecretOrString) UnmarshalJSON(data []byte) error { + if len(data) > 0 && data[0] == '{' { + se := &Secret{} + if err := json.Unmarshal(data, se); err != nil { + return err + } + s.Secret = se + return nil + } + return json.Unmarshal(data, &s.Value) +} + +func (s SecretOrString) MarshalJSON() ([]byte, error) { + if s.Secret != nil { + return json.Marshal(s.Secret) + } + return json.Marshal(s.Value) +} diff --git a/pkg/engine/task/client/string_or_slice.go b/pkg/engine/task/client/string_or_slice.go new file mode 100644 index 0000000..3580cc3 --- /dev/null +++ b/pkg/engine/task/client/string_or_slice.go @@ -0,0 +1,37 @@ +package client + +import "encoding/json" + +type StringOrSlice []string + +func (StringOrSlice) OneOf() []any { + return []any{ + "", + []string{}, + } +} + +func (s *StringOrSlice) UnmarshalJSON(data []byte) error { + if len(data) > 0 && data[0] == '"' { + b := "" + if err := json.Unmarshal(data, &b); err != nil { + return err + } + *s = []string{b} + return nil + } + + var list []string + + if err := json.Unmarshal(data, &list); err != nil { + return err + } + + *s = list + + return nil +} + +func (s StringOrSlice) MarshalJSON() ([]byte, error) { + return json.Marshal([]string(s)) +} diff --git a/pkg/engine/task/exec/run.go b/pkg/engine/task/exec/run.go new file mode 100644 index 0000000..d0237cf --- /dev/null +++ b/pkg/engine/task/exec/run.go @@ -0,0 +1,60 @@ +package exec + +import ( + "context" + "github.com/pkg/errors" + "strings" + + "github.com/octohelm/piper/pkg/engine/task" + "github.com/octohelm/piper/pkg/engine/task/client" + "github.com/octohelm/piper/pkg/engine/task/flow" + taskwd "github.com/octohelm/piper/pkg/engine/task/wd" + "github.com/octohelm/piper/pkg/wd" +) + +func init() { + task.Factory.Register(&Run{}) +} + +type Run struct { + task.Task + + taskwd.CurrentWorkDir + + Command client.StringOrSlice `json:"cmd"` + Env map[string]client.SecretOrString `json:"env,omitempty"` + User string `json:"user,omitempty"` + + Result flow.ResultInterface `json:"-" output:"result"` +} + +func (e *Run) Do(ctx context.Context) error { + return e.Cwd.Do(ctx, func(ctx context.Context, cwd wd.WorkDir) error { + cmd := strings.Join(e.Command, " ") + + env := map[string]string{} + + for k, v := range e.Env { + if s := v.Secret; s != nil { + if secret, ok := s.Value(ctx); ok { + env[k] = secret.Value + } else { + return errors.Errorf("not found secret for %s", k) + } + } else { + env[k] = v.Value + } + } + + if err := cwd.Exec(ctx, cmd, + wd.WithEnv(env), + wd.WithUser(e.User), + ); err != nil { + return err + } + + e.Result.SetSuccess(true) + + return nil + }) +} diff --git a/pkg/engine/task/factory.go b/pkg/engine/task/factory.go new file mode 100644 index 0000000..7ebf25b --- /dev/null +++ b/pkg/engine/task/factory.go @@ -0,0 +1,5 @@ +package task + +import "github.com/octohelm/piper/pkg/cueflow" + +var Factory = cueflow.NewTaskFactory("piper.octohelm.tech") diff --git a/pkg/engine/task/file/file.go b/pkg/engine/task/file/file.go new file mode 100644 index 0000000..4af42d6 --- /dev/null +++ b/pkg/engine/task/file/file.go @@ -0,0 +1,83 @@ +package file + +import ( + "bytes" + "context" + "encoding/json" + "github.com/octohelm/piper/pkg/engine/task/wd" + "io" + "os" +) + +type File struct { + Cwd wd.WorkDir `json:"cwd"` + Filename string `json:"filename"` +} + +type StringOrFile struct { + File *File + String string +} + +func (StringOrFile) OneOf() []any { + return []any{ + "", + &File{}, + } +} + +func (s *StringOrFile) UnmarshalJSON(data []byte) error { + if len(data) > 0 && data[0] == '"' { + b := "" + if err := json.Unmarshal(data, &b); err != nil { + return err + } + *s = StringOrFile{ + String: b, + } + return nil + } + + *s = StringOrFile{ + File: &File{}, + } + return json.Unmarshal(data, s.File) +} + +func (s StringOrFile) MarshalJSON() ([]byte, error) { + if s.File != nil { + return json.Marshal(s.File) + } + return json.Marshal(s.String) +} + +func (s *StringOrFile) Size(ctx context.Context) (int64, error) { + if s.File != nil { + cwd, err := s.File.Cwd.Get(ctx) + if err != nil { + return -1, err + } + info, err := cwd.Stat(ctx, s.File.Filename) + if err != nil { + return -1, err + } + return info.Size(), nil + } + return int64(len(s.String)), nil +} + +func (s *StringOrFile) Open(ctx context.Context) (io.ReadCloser, error) { + if s.File != nil { + cwd, err := s.File.Cwd.Get(ctx) + if err != nil { + return nil, err + } + return cwd.OpenFile(ctx, s.File.Filename, os.O_RDONLY, os.ModePerm) + } + + if len(s.String) == 0 { + return nil, nil + } + + return io.NopCloser(bytes.NewBufferString(s.String)), nil +} diff --git a/pkg/engine/task/file/write.go b/pkg/engine/task/file/write.go new file mode 100644 index 0000000..e88af01 --- /dev/null +++ b/pkg/engine/task/file/write.go @@ -0,0 +1,48 @@ +package file + +import ( + "context" + "github.com/octohelm/piper/pkg/engine/task" + taskwd "github.com/octohelm/piper/pkg/engine/task/wd" + "github.com/octohelm/piper/pkg/wd" + "os" + "path/filepath" +) + +func init() { + task.Factory.Register(&Write{}) +} + +type Write struct { + task.Task + + taskwd.CurrentWorkDir + + Filename string `json:"filename"` + Contents string `json:"contents"` + + Output File `json:"-" output:"output"` +} + +func (t *Write) Do(ctx context.Context) error { + return t.Cwd.Do(ctx, func(ctx context.Context, cwd wd.WorkDir) error { + if err := cwd.Mkdir(ctx, filepath.Dir(t.Filename), os.ModeDir); err != nil { + return err + } + + f, err := cwd.OpenFile(ctx, t.Filename, os.O_RDWR|os.O_CREATE, os.ModePerm) + if err != nil { + return err + } + defer f.Close() + + if _, err = f.Write([]byte(t.Contents)); err != nil { + return err + } + + t.Output.Cwd = t.Cwd + t.Output.Filename = t.Filename + + return nil + }) +} diff --git a/pkg/engine/task/flow/all.go b/pkg/engine/task/flow/all.go new file mode 100644 index 0000000..459f200 --- /dev/null +++ b/pkg/engine/task/flow/all.go @@ -0,0 +1,66 @@ +package flow + +import ( + "context" + "strings" + + "cuelang.org/go/cue" + "github.com/pkg/errors" + + "github.com/octohelm/piper/pkg/cueflow" + "github.com/octohelm/piper/pkg/engine/task" +) + +func init() { + task.Factory.Register(&All{}) +} + +type All struct { + task.Group + + Tasks []TaskInterface `json:"tasks"` + + Result ResultInterface `json:"-" output:"result"` +} + +func (e *All) Do(ctx context.Context) error { + p := e.Parent() + + scope := cueflow.CueValue(p.Value().LookupPath(cue.ParsePath("tasks"))) + + list, err := scope.List() + if err != nil { + return err + } + + for idx := 0; list.Next(); idx++ { + itemValue := list.Value() + + if err := cueflow.RunTasks(ctx, p.Scope(), + cueflow.WithShouldRunFunc(func(value cue.Value) bool { + return value.LookupPath(cueflow.TaskPath).Exists() && strings.HasPrefix(value.Path().String(), itemValue.Path().String()) + }), + cueflow.WithPrefix(e.Parent().Path()), + ); err != nil { + return errors.Wrapf(err, "tasks[%d]", idx) + } + + resultValue := p.Scope().Value().LookupPath(itemValue.Path()) + + ti := &TaskInterface{} + + if err := resultValue.Decode(ti); err != nil { + + return err + } + + if !ti.Result.Success() { + e.Result = ti.Result + return nil + } + } + + e.Result.SetSuccess(true) + + return nil +} diff --git a/pkg/engine/task/flow/first.go b/pkg/engine/task/flow/first.go new file mode 100644 index 0000000..5dfd929 --- /dev/null +++ b/pkg/engine/task/flow/first.go @@ -0,0 +1,62 @@ +package flow + +import ( + "context" + "strings" + + "cuelang.org/go/cue" + "github.com/pkg/errors" + + "github.com/octohelm/piper/pkg/cueflow" + "github.com/octohelm/piper/pkg/engine/task" +) + +func init() { + task.Factory.Register(&First{}) +} + +type First struct { + task.Group + + Tasks []TaskInterface `json:"tasks"` + + Result ResultInterface `json:"-" output:"result"` +} + +func (e *First) Do(ctx context.Context) error { + p := e.Parent() + + scope := cueflow.CueValue(p.Value().LookupPath(cue.ParsePath("tasks"))) + + list, err := scope.List() + if err != nil { + return err + } + + for idx := 0; list.Next(); idx++ { + valuePath := list.Value().Path() + + if err := cueflow.RunTasks(ctx, p.Scope(), + cueflow.WithShouldRunFunc(func(value cue.Value) bool { + return value.LookupPath(cueflow.TaskPath).Exists() && strings.HasPrefix(value.Path().String(), valuePath.String()) + }), + cueflow.WithPrefix(e.Parent().Path()), + ); err != nil { + return errors.Wrapf(err, "tasks[%d]", idx) + } + + resultValue := p.Scope().Value().LookupPath(valuePath) + + ti := &TaskInterface{} + if err := resultValue.Decode(ti); err != nil { + return err + } + + if ti.Result.Success() { + e.Result = ti.Result + return nil + } + } + + return nil +} diff --git a/pkg/engine/task/flow/task.go b/pkg/engine/task/flow/task.go new file mode 100644 index 0000000..2dfb58b --- /dev/null +++ b/pkg/engine/task/flow/task.go @@ -0,0 +1,54 @@ +package flow + +import ( + "encoding/json" + "github.com/octohelm/piper/pkg/cueflow" +) + +type TaskInterface struct { + Result ResultInterface `json:"result"` +} + +type Result = cueflow.Result + +var _ Result = ResultInterface{} + +type ResultInterface struct { + Ok bool `json:"ok"` + + values map[string]any +} + +func (r *ResultInterface) SetSuccess(ok bool) { + r.Ok = ok +} + +func (r ResultInterface) Success() bool { + return r.Ok +} + +func (r ResultInterface) MarshalJSON() ([]byte, error) { + values := map[string]any{} + + for k, v := range r.values { + values[k] = v + } + + values["ok"] = r.Ok + + return json.Marshal(values) +} + +func (r *ResultInterface) UnmarshalJSON(bytes []byte) error { + rr := &ResultInterface{ + values: map[string]any{}, + } + + if err := json.Unmarshal(bytes, &rr.values); err != nil { + return err + } + + rr.Ok = rr.values["ok"].(bool) + *r = *rr + return nil +} diff --git a/pkg/engine/task/http/do.go b/pkg/engine/task/http/do.go new file mode 100644 index 0000000..bd91646 --- /dev/null +++ b/pkg/engine/task/http/do.go @@ -0,0 +1,180 @@ +package http + +import ( + "context" + "encoding/json" + "fmt" + "github.com/octohelm/piper/pkg/engine/task/client" + "io" + "net/http" + "strings" + "time" + + "github.com/go-courier/logr" + + "github.com/octohelm/piper/pkg/engine/task" + "github.com/octohelm/piper/pkg/engine/task/file" + "github.com/octohelm/piper/pkg/engine/task/flow" +) + +func init() { + task.Factory.Register(&Do{}) +} + +type Do struct { + task.Task + + Method string `json:"method"` + Url string `json:"url"` + Header map[string]client.StringOrSlice `json:"header,omitempty"` + Query map[string]client.StringOrSlice `json:"query,omitempty"` + RequestBody file.StringOrFile `json:"body,omitempty"` + + Result ResponseResult `json:"-" output:"result"` +} + +var _ flow.Result = ResponseResult{} + +type ResponseResult struct { + Ok bool `json:"ok"` + Status int `json:"status,omitempty"` + Header map[string]client.StringOrSlice `json:"header,omitempty"` + Data any `json:"data,omitempty"` +} + +func (r ResponseResult) Success() bool { + return r.Ok +} + +type processingRecoder struct { + written int64 + size int64 +} + +func (r *processingRecoder) Write(p []byte) (n int, err error) { + n = len(p) + r.written += int64(n) + return +} + +func (r *processingRecoder) percent() float64 { + return float64(r.written) / float64(r.size) * float64(100) +} + +func (r *processingRecoder) Processing(ctx context.Context) <-chan float64 { + percentCh := make(chan float64) + t := time.NewTicker(1 * time.Second) + + go func() { + defer func() { + t.Stop() + close(percentCh) + }() + + if r.size == 0 { + return + } + + for range t.C { + select { + case <-ctx.Done(): + return + default: + percent := r.percent() + percentCh <- percent + if percent >= 100 { + return + } + } + } + }() + + return percentCh +} + +func (r *Do) Do(ctx context.Context) error { + size, err := r.RequestBody.Size(ctx) + if err != nil { + return err + } + + var reader io.Reader + + if size > 0 { + pr := &processingRecoder{ + size: size, + } + + f, err := r.RequestBody.Open(ctx) + if err != nil { + return err + } + defer f.Close() + + go func() { + for p := range pr.Processing(ctx) { + logr.FromContext(ctx).Info(fmt.Sprintf("uploading %00.1f", p) + "%") + } + }() + + reader = io.TeeReader(f, pr) + } + + req, err := http.NewRequestWithContext(ctx, r.Method, r.Url, reader) + if err != nil { + return err + } + + if len(r.Query) > 0 { + q := req.URL.Query() + for k, vv := range r.Query { + q[k] = vv + } + req.URL.RawQuery = q.Encode() + } + + for k, vv := range r.Header { + req.Header[k] = vv + } + + if size > 0 { + req.ContentLength = size + } + + logr.FromContext(ctx).Info(fmt.Sprintf("%s %s", req.Method, req.URL.String())) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + r.Result.Ok = resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices + r.Result.Status = resp.StatusCode + + if contentType := resp.Header.Get("Content-Type"); strings.Contains(contentType, "json") { + if err := json.NewDecoder(resp.Body).Decode(&r.Result.Data); err != nil { + return err + } + } else if strings.HasPrefix(contentType, "text/") { + data, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + r.Result.Data = string(data) + } else { + data, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + r.Result.Data = data + } + + r.Result.Header = map[string]client.StringOrSlice{} + + for k, vv := range resp.Header { + r.Result.Header[k] = vv + } + + return nil +} diff --git a/pkg/engine/task/log.go b/pkg/engine/task/log.go new file mode 100644 index 0000000..fb95f27 --- /dev/null +++ b/pkg/engine/task/log.go @@ -0,0 +1,25 @@ +package task + +import "github.com/k0sproject/rig" + +func init() { + rig.SetLogger(&discord{}) +} + +type discord struct { +} + +func (discord) Tracef(s string, i ...interface{}) { +} + +func (discord) Debugf(s string, i ...interface{}) { +} + +func (discord) Infof(s string, i ...interface{}) { +} + +func (discord) Warnf(s string, i ...interface{}) { +} + +func (discord) Errorf(s string, i ...interface{}) { +} diff --git a/pkg/engine/task/secret.go b/pkg/engine/task/secret.go new file mode 100644 index 0000000..1dad566 --- /dev/null +++ b/pkg/engine/task/secret.go @@ -0,0 +1,22 @@ +package task + +import ( + contextx "github.com/octohelm/x/context" +) + +var SecretStore = &Store[string, Secret]{} + +var SecretContext = contextx.New[*Store[string, Secret]]( + contextx.WithDefaultsFunc[*Store[string, Secret]](func() *Store[string, Secret] { + return SecretStore + }), +) + +type Secret struct { + Key string + Value string +} + +func (s Secret) String() string { + return s.Key +} diff --git a/pkg/engine/task/store.go b/pkg/engine/task/store.go new file mode 100644 index 0000000..4480e74 --- /dev/null +++ b/pkg/engine/task/store.go @@ -0,0 +1,24 @@ +package task + +import ( + "fmt" + "github.com/opencontainers/go-digest" + "sync" +) + +type Store[K comparable, V fmt.Stringer] struct { + m sync.Map +} + +func (s *Store[K, V]) Get(k K) (V, bool) { + if v, ok := s.m.Load(k); ok { + return v.(V), true + } + return *(new(V)), false +} + +func (s *Store[K, V]) Set(v V) string { + dget := digest.FromString(v.String()).String() + s.m.Store(dget, v) + return dget +} diff --git a/pkg/engine/task/task.go b/pkg/engine/task/task.go new file mode 100644 index 0000000..698e02c --- /dev/null +++ b/pkg/engine/task/task.go @@ -0,0 +1,29 @@ +package task + +import "github.com/octohelm/piper/pkg/cueflow" + +type Task = cueflow.TaskImpl + +type SetupTask struct { + Task +} + +func (v *SetupTask) Setup() bool { + return true +} + +var _ cueflow.SubTask = &Group{} + +type Group struct { + Task + + parent cueflow.Task +} + +func (v *Group) SetParent(t cueflow.Task) { + v.parent = t +} + +func (v *Group) Parent() cueflow.Task { + return v.parent +} diff --git a/pkg/engine/task/wd.go b/pkg/engine/task/wd.go new file mode 100644 index 0000000..ce2fa46 --- /dev/null +++ b/pkg/engine/task/wd.go @@ -0,0 +1,15 @@ +package task + +import ( + contextx "github.com/octohelm/x/context" + + "github.com/octohelm/piper/pkg/wd" +) + +var WorkDirStore = &Store[string, wd.WorkDir]{} + +var WorkDirContext = contextx.New[*Store[string, wd.WorkDir]]( + contextx.WithDefaultsFunc[*Store[string, wd.WorkDir]](func() *Store[string, wd.WorkDir] { + return WorkDirStore + }), +) diff --git a/pkg/engine/task/wd/local.go b/pkg/engine/task/wd/local.go new file mode 100644 index 0000000..a7ec440 --- /dev/null +++ b/pkg/engine/task/wd/local.go @@ -0,0 +1,41 @@ +package wd + +import ( + "context" + "github.com/k0sproject/rig" + "github.com/octohelm/piper/pkg/engine/task" + "github.com/octohelm/piper/pkg/wd" +) + +func init() { + task.Factory.Register(&Local{}) +} + +type Local struct { + task.SetupTask + + Dir string `json:"dir" default:"."` + + WorkDir WorkDir `json:"-" output:"wd"` +} + +func (c *Local) Do(ctx context.Context) error { + planRoot := wd.Dir(task.ClientContext.From(ctx).Root()) + + cwd, err := wd.Wrap( + &rig.Connection{ + Localhost: &rig.Localhost{ + Enabled: true, + }, + }, + wd.WithDir(planRoot.String()), + ) + if err != nil { + return err + } + + id := task.WorkDirContext.From(ctx).Set(cwd) + c.WorkDir.Ref.ID = id + + return nil +} diff --git a/pkg/engine/task/wd/ssh.go b/pkg/engine/task/wd/ssh.go new file mode 100644 index 0000000..4b0a1df --- /dev/null +++ b/pkg/engine/task/wd/ssh.go @@ -0,0 +1,44 @@ +package wd + +import ( + "context" + + "github.com/k0sproject/rig" + "github.com/octohelm/piper/pkg/engine/task" + "github.com/octohelm/piper/pkg/sshutil" + "github.com/octohelm/piper/pkg/wd" +) + +func init() { + task.Factory.Register(&SSH{}) +} + +type SSH struct { + task.SetupTask + + Config string `json:"config" default:"~/.ssh/config"` + Alias string `json:"alias"` + + WorkDir WorkDir `json:"-" output:"wd"` +} + +func (c *SSH) Do(ctx context.Context) error { + ssh, err := sshutil.Load(c.Config, c.Alias) + if err != nil { + return err + } + + cwd, err := wd.Wrap( + &rig.Connection{ + SSH: ssh, + }, + wd.WithUser(ssh.User), + ) + if err != nil { + return err + } + + c.WorkDir.Ref.ID = task.WorkDirContext.From(ctx).Set(cwd) + + return nil +} diff --git a/pkg/engine/task/wd/su.go b/pkg/engine/task/wd/su.go new file mode 100644 index 0000000..c6d2157 --- /dev/null +++ b/pkg/engine/task/wd/su.go @@ -0,0 +1,29 @@ +package wd + +import ( + "context" + + "github.com/octohelm/piper/pkg/engine/task" + "github.com/octohelm/piper/pkg/wd" +) + +func init() { + task.Factory.Register(&Su{}) +} + +type Su struct { + task.Task + + CurrentWorkDir + + User string `json:"user"` + + WorkDir WorkDir `json:"-" output:"wd"` +} + +func (e *Su) Do(ctx context.Context) error { + return e.Cwd.Do(ctx, func(ctx context.Context, cwd wd.WorkDir) error { + e.WorkDir.SetBy(ctx, cwd) + return nil + }, wd.WithUser(e.User)) +} diff --git a/pkg/engine/task/wd/sub.go b/pkg/engine/task/wd/sub.go new file mode 100644 index 0000000..7bbb784 --- /dev/null +++ b/pkg/engine/task/wd/sub.go @@ -0,0 +1,30 @@ +package wd + +import ( + "context" + + "github.com/octohelm/piper/pkg/engine/task" + + "github.com/octohelm/piper/pkg/wd" +) + +func init() { + task.Factory.Register(&Sub{}) +} + +type Sub struct { + task.Task + + CurrentWorkDir + + Dir string `json:"dir"` + + WorkDir WorkDir `json:"-" output:"wd"` +} + +func (e *Sub) Do(ctx context.Context) error { + return e.Cwd.Do(ctx, func(ctx context.Context, cwd wd.WorkDir) error { + e.WorkDir.SetBy(ctx, cwd) + return nil + }, wd.WithDir(e.Dir)) +} diff --git a/pkg/engine/task/wd/sys_info.go b/pkg/engine/task/wd/sys_info.go new file mode 100644 index 0000000..0543738 --- /dev/null +++ b/pkg/engine/task/wd/sys_info.go @@ -0,0 +1,52 @@ +package wd + +import ( + "context" + "github.com/octohelm/piper/pkg/engine/task" + "github.com/octohelm/piper/pkg/wd" +) + +func init() { + task.Factory.Register(&SysInfo{}) +} + +type SysInfo struct { + task.Task + + CurrentWorkDir + + Release Release `json:"-" output:"release"` + Platform Platform `json:"-" output:"platform"` +} + +type Platform struct { + OS string `json:"os"` + Architecture string `json:"architecture"` +} + +type Release struct { + Name string `json:"name"` + ID string `json:"id"` + IDLike string `json:"id_like,omitempty"` + Version string `json:"version,omitempty"` +} + +func (e *SysInfo) Do(ctx context.Context) error { + return e.Cwd.Do(ctx, func(ctx context.Context, cwd wd.WorkDir) error { + if can, ok := cwd.(wd.CanOSInfo); ok { + info, err := can.OSInfo(ctx) + if err != nil { + return err + } + + e.Release.Name = info.Name + e.Release.ID = info.ID + e.Release.IDLike = info.IDLike + e.Release.Version = info.Version + + e.Platform.OS = info.Platform.OS + e.Platform.Architecture = info.Platform.Architecture + } + return nil + }) +} diff --git a/pkg/engine/task/wd/work_dir.go b/pkg/engine/task/wd/work_dir.go new file mode 100644 index 0000000..ffc5d22 --- /dev/null +++ b/pkg/engine/task/wd/work_dir.go @@ -0,0 +1,53 @@ +package wd + +import ( + "context" + "github.com/go-courier/logr" + + "github.com/octohelm/piper/pkg/engine/task" + "github.com/octohelm/piper/pkg/wd" + "github.com/pkg/errors" +) + +func init() { + task.Factory.Register(&WorkDir{}) +} + +type WorkDir struct { + Ref struct { + ID string `json:"id"` + } `json:"$$wd"` +} + +func (w *WorkDir) Get(ctx context.Context, optFns ...wd.OptionFunc) (wd.WorkDir, error) { + if found, ok := task.WorkDirContext.From(ctx).Get(w.Ref.ID); ok { + return wd.With(found, optFns...) + } + return nil, errors.Errorf("workdir %s is not found", w.Ref.ID) +} + +func (w *WorkDir) Do(ctx context.Context, action func(ctx context.Context, wd wd.WorkDir) error, optFns ...wd.OptionFunc) error { + cwd, err := w.Get(ctx, optFns...) + if err != nil { + return err + } + l := logr.FromContext(ctx).WithValues("name", cwd.String()) + ctx = logr.WithLogger(ctx, l) + return action(ctx, cwd) +} + +func (w *WorkDir) SetBy(ctx context.Context, workdir wd.WorkDir) { + w.Ref.ID = task.WorkDirContext.From(ctx).Set(workdir) +} + +type CurrentWorkDir struct { + Cwd WorkDir `json:"cwd"` +} + +func (s CurrentWorkDir) ScopeName(ctx context.Context) string { + cwd, err := s.Cwd.Get(ctx) + if err != nil { + panic(err) + } + return cwd.String() +} diff --git a/pkg/logutil/color.go b/pkg/logutil/color.go new file mode 100644 index 0000000..cd46008 --- /dev/null +++ b/pkg/logutil/color.go @@ -0,0 +1,135 @@ +package logutil + +import ( + "bytes" + "io" + "os" + "strconv" + "strings" +) + +var NoColor = noColorExists() + +func noColorExists() bool { + _, exists := os.LookupEnv("NO_COLOR") + return exists +} + +type WrapWriter = func(w io.Writer) io.Writer + +func WithColor(attrs ...Attribute) func(w io.Writer) io.Writer { + return func(w io.Writer) io.Writer { + if NoColor { + return w + } + return &colorWriter{w: w, attrs: attrs} + } +} + +type colorWriter struct { + w io.Writer + attrs []Attribute +} + +// write SGR sequence "\x1b[...m" +func writeSequenceTo(w io.Writer, attrs ...Attribute) { + if len(attrs) > 0 { + _, _ = io.WriteString(w, escape) + _, _ = io.WriteString(w, "[") + for i, attr := range attrs { + if i > 0 { + _, _ = io.WriteString(w, ";") + } + _, _ = io.WriteString(w, strconv.Itoa(int(attr))) + } + _, _ = io.WriteString(w, "m") + } +} + +func (c *colorWriter) sequence() string { + format := make([]string, len(c.attrs)) + for i, v := range c.attrs { + format[i] = strconv.Itoa(int(v)) + } + + return strings.Join(format, ";") +} + +func (c *colorWriter) Write(p []byte) (n int, err error) { + b := bytes.NewBuffer(nil) + + writeSequenceTo(b, c.attrs...) + + _, _ = b.Write(p) + + writeSequenceTo(b, Reset) + + i, err := io.Copy(c.w, b) + return int(i), err +} + +type Attribute int + +const escape = "\x1b" + +// Base attributes +const ( + Reset Attribute = iota + Bold + Faint + Italic + Underline + BlinkSlow + BlinkRapid + ReverseVideo + Concealed + CrossedOut +) + +// Foreground text colors +const ( + FgBlack Attribute = iota + 30 + FgRed + FgGreen + FgYellow + FgBlue + FgMagenta + FgCyan + FgWhite +) + +// Foreground Hi-Intensity text colors +const ( + FgHiBlack Attribute = iota + 90 + FgHiRed + FgHiGreen + FgHiYellow + FgHiBlue + FgHiMagenta + FgHiCyan + FgHiWhite +) + +// Background text colors +const ( + BgBlack Attribute = iota + 40 + BgRed + BgGreen + BgYellow + BgBlue + BgMagenta + BgCyan + BgWhite +) + +// Background Hi-Intensity text colors +const ( + BgHiBlack Attribute = iota + 100 + BgHiRed + BgHiGreen + BgHiYellow + BgHiBlue + BgHiMagenta + BgHiCyan + BgHiWhite +) diff --git a/pkg/logutil/logger.go b/pkg/logutil/logger.go new file mode 100644 index 0000000..46190f7 --- /dev/null +++ b/pkg/logutil/logger.go @@ -0,0 +1,47 @@ +package logutil + +import ( + "github.com/go-courier/logr" + "golang.org/x/net/context" + "log/slog" + + "github.com/innoai-tech/infra/pkg/configuration" +) + +// +gengo:enum +type LogLevel string + +const ( + ErrorLevel LogLevel = "error" + WarnLevel LogLevel = "warn" + InfoLevel LogLevel = "info" + DebugLevel LogLevel = "debug" +) + +type Logger struct { + // Log level + LogLevel LogLevel `flag:",omitempty" alias:"l"` + logger logr.Logger +} + +func (o *Logger) SetDefaults() { + if o.LogLevel == "" { + o.LogLevel = InfoLevel + } +} + +func (o *Logger) Init(ctx context.Context) error { + if o.logger == nil { + lvl, _ := logr.ParseLevel(string(o.LogLevel)) + o.logger = &logger{ + slogr: slog.New(&slogHandler{lvl: fromLogrLevel(lvl)})} + } + return nil +} + +func (o *Logger) InjectContext(ctx context.Context) context.Context { + return configuration.InjectContext( + ctx, + configuration.InjectContextFunc(logr.WithLogger, o.logger), + ) +} diff --git a/pkg/logutil/logr.go b/pkg/logutil/logr.go new file mode 100644 index 0000000..8f5b5a4 --- /dev/null +++ b/pkg/logutil/logr.go @@ -0,0 +1,77 @@ +package logutil + +import ( + "context" + "fmt" + "log/slog" + "strings" + + "github.com/go-courier/logr" +) + +type logger struct { + slogr *slog.Logger + spans []string +} + +func (d *logger) SetLevel(lvl logr.Level) { + d.slogr.Handler().(*slogHandler).lvl = fromLogrLevel(lvl) +} + +func (d *logger) WithValues(keyAndValues ...any) logr.Logger { + return &logger{ + spans: d.spans, + slogr: d.slogr.With(keyAndValues...), + } +} + +func (d *logger) Start(ctx context.Context, name string, keyAndValues ...any) (context.Context, logr.Logger) { + spans := append(d.spans, name) + + return ctx, &logger{ + spans: spans, + slogr: d.slogr.WithGroup(strings.Join(spans, "/")).With(keyAndValues...), + } +} + +func (d *logger) End() { + if len(d.spans) != 0 { + d.spans = d.spans[0 : len(d.spans)-1] + } +} + +func (d *logger) Debug(format string, args ...any) { + if len(args) > 0 { + d.slogr.Debug(fmt.Sprintf(format, args...)) + } else { + d.slogr.Debug(format) + } +} + +func (d *logger) Info(format string, args ...any) { + if len(args) > 0 { + d.slogr.Info(fmt.Sprintf(format, args...)) + } else { + d.slogr.Info(format) + } +} + +func (d *logger) Warn(err error) { + d.slogr.Warn(err.Error()) +} + +func (d *logger) Error(err error) { + d.slogr.Error(err.Error(), err) +} + +func (d *logger) Trace(format string, args ...any) { + d.Debug(format, args...) +} + +func (d *logger) Fatal(err error) { + d.Error(err) +} + +func (d *logger) Panic(err error) { + d.Error(err) +} diff --git a/pkg/logutil/slog.go b/pkg/logutil/slog.go new file mode 100644 index 0000000..2431bd3 --- /dev/null +++ b/pkg/logutil/slog.go @@ -0,0 +1,159 @@ +package logutil + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strings" + "sync" + "sync/atomic" + + "github.com/mattn/go-colorable" + + "golang.org/x/net/context" + + "github.com/go-courier/logr" + "log/slog" +) + +func fromLogrLevel(l logr.Level) slog.Level { + switch l { + case logr.ErrorLevel: + return slog.LevelError + case logr.WarnLevel: + return slog.LevelWarn + case logr.InfoLevel: + return slog.LevelInfo + case logr.DebugLevel: + return slog.LevelDebug + } + return slog.LevelDebug +} + +type slogHandler struct { + lvl slog.Level + group string + attrs []slog.Attr +} + +func (s *slogHandler) Enabled(ctx context.Context, level slog.Level) bool { + return level >= s.lvl +} + +type Printer interface { + Fprintf(w io.Writer, format string, a ...any) (n int, err error) +} + +func withLevelColor(level slog.Level) func(io.Writer) io.Writer { + switch level { + case slog.LevelError: + return WithColor(FgRed) + case slog.LevelWarn: + return WithColor(FgYellow) + case slog.LevelInfo: + return WithColor(FgGreen) + } + return WithColor(FgWhite) +} + +func (s *slogHandler) Handle(ctx context.Context, r slog.Record) error { + prefix := bytes.NewBuffer(nil) + + for _, attr := range s.attrs { + if attr.Key == "name" { + _, _ = fmt.Fprintf(WithNameColor(attr.Value.String())(prefix), "%s |", attr.Value.String()) + break + } + } + + _, _ = fmt.Fprintf(withLevelColor(r.Level)(prefix), " %s ", strings.ToUpper(r.Level.String())[0:4]) + _, _ = fmt.Fprintf(WithColor(FgWhite)(prefix), "%s", r.Time.Format("15:04:05.000")) + + w := bytes.NewBuffer(nil) + + scanner := bufio.NewScanner(bytes.NewBufferString(r.Message)) + idx := 0 + + for scanner.Scan() { + if line := strings.TrimSpace(scanner.Text()); len(line) > 0 { + if idx > 0 { + _, _ = fmt.Fprintln(w) + } + _, _ = fmt.Fprintf(w, "%s %s", prefix.String(), line) + idx++ + } + } + + for _, attr := range s.attrs { + if attr.Key != "name" { + switch attr.Value.Kind() { + case slog.KindString: + _, _ = fmt.Fprintf(WithColor(FgWhite)(w), " %s=%q", attr.Key, attr.Value) + default: + logValue := attr.Value.Any() + if valuer, ok := logValue.(slog.LogValuer); ok { + logValue = valuer.LogValue().Any() + } + + switch x := logValue.(type) { + case []byte: + _, _ = fmt.Fprintf(WithColor(FgWhite)(w), " %s=%v", attr.Key, string(x)) + default: + _, _ = fmt.Fprintf(WithColor(FgWhite)(w), " %s=%v", attr.Key, x) + } + } + } + } + + _, _ = fmt.Fprintln(w) + + r.Attrs(func(attr slog.Attr) bool { + if attr.Key == "err" { + if err := attr.Value.Any().(error); err != nil { + _, _ = fmt.Fprintf(w, "%+v", err) + } + } + return true + }) + + _, _ = io.Copy(colorable.NewColorableStdout(), w) + + return nil +} + +func (s slogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &slogHandler{ + lvl: s.lvl, + group: s.group, + attrs: append(s.attrs, attrs...), + } +} + +func (s slogHandler) WithGroup(group string) slog.Handler { + return &slogHandler{ + lvl: s.lvl, + attrs: s.attrs, + group: group, + } +} + +var colorIndexes = sync.Map{} +var colorIdx uint32 = 0 +var colorFns = []WrapWriter{ + WithColor(FgBlue), + WithColor(FgMagenta), + WithColor(FgCyan), + WithColor(FgYellow), +} + +func WithNameColor(name string) WrapWriter { + idx, ok := colorIndexes.Load(name) + if !ok { + i := atomic.LoadUint32(&colorIdx) + colorIndexes.Store(name, i) + atomic.AddUint32(&colorIdx, 1) + idx = i + } + return colorFns[int(idx.(uint32))%len(colorFns)] +} diff --git a/pkg/sshutil/config.go b/pkg/sshutil/config.go new file mode 100644 index 0000000..370f91b --- /dev/null +++ b/pkg/sshutil/config.go @@ -0,0 +1,71 @@ +package sshutil + +import ( + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/k0sproject/rig" + "github.com/kevinburke/ssh_config" + "github.com/pkg/errors" +) + +func Load(configPath string, alias string) (*rig.SSH, error) { + if configPath == "" { + configPath = filepath.Join(os.Getenv("HOME"), ".ssh", "config") + } + + if strings.HasPrefix(configPath, "~/") { + configPath = filepath.Join(os.Getenv("HOME"), configPath[2:]) + } + + f, err := os.Open(configPath) + if err != nil { + return nil, err + } + + cfg, err := ssh_config.Decode(f) + if err != nil { + return nil, err + } + + for _, host := range cfg.Hosts { + if len(host.Patterns) > 0 && host.Patterns[0].String() != "*" { + if host.Matches(alias) { + ssh := &rig.SSH{} + + for _, node := range host.Nodes { + switch x := node.(type) { + case *ssh_config.KV: + switch x.Key { + case "Hostname": + ssh.Address = x.Value + case "Port": + ssh.Port, _ = strconv.Atoi(x.Value) + case "User": + ssh.User = x.Value + case "IdentityFile": + v, _ := strconv.Unquote(x.Value) + ssh.KeyPath = &v + } + } + } + return ssh, nil + } + } + } + + for _, host := range cfg.Hosts { + if host.Matches("*") { + for _, node := range host.Nodes { + switch x := node.(type) { + case *ssh_config.Include: + return Load(x.String()[len("Include "):], alias) + } + } + } + } + + return nil, errors.Errorf("not found %s", alias) +} diff --git a/pkg/wd/fs.go b/pkg/wd/fs.go new file mode 100644 index 0000000..79ad7d9 --- /dev/null +++ b/pkg/wd/fs.go @@ -0,0 +1,171 @@ +package wd + +import ( + "context" + "io" + "io/fs" + "os" + "path/filepath" + + "github.com/k0sproject/rig/pkg/rigfs" + "github.com/octohelm/unifs/pkg/filesystem" + "golang.org/x/net/webdav" +) + +func WrapRigFS(fsys rigfs.Fsys) filesystem.FileSystem { + return &rigfsWrapper{fsys: fsys} +} + +type rigfsWrapper struct { + fsys rigfs.Fsys +} + +func (r *rigfsWrapper) Mkdir(ctx context.Context, name string, perm os.FileMode) error { + return r.fsys.MkDirAll(name, perm) +} + +func (r *rigfsWrapper) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) { + if perm.IsDir() { + info, err := r.fsys.Stat(name) + if err != nil { + return nil, err + } + + return &wrapper{ + fsys: r.fsys, + path: name, + info: info, + }, err + } + + f, err := r.fsys.OpenFile(name, flag, perm) + if err != nil { + return nil, err + } + + return &wrapper{ + fsys: r.fsys, + path: name, + file: f, + }, err +} + +var _ fs.ReadDirFile = &wrapper{} + +type wrapper struct { + fsys rigfs.Fsys + path string + info fs.FileInfo + file fs.File +} + +func (f *wrapper) Close() error { + if f.file != nil { + return f.file.Close() + } + return nil +} + +func (f *wrapper) Stat() (fs.FileInfo, error) { + if f.info != nil { + return f.info, nil + } + return f.file.Stat() +} + +func (f *wrapper) Read(bytes []byte) (int, error) { + if seeker, ok := f.file.(io.Reader); ok { + return seeker.Read(bytes) + } + return -1, &fs.PathError{ + Op: "read", + Path: f.path, + Err: fs.ErrInvalid, + } +} + +func (f *wrapper) Seek(offset int64, whence int) (int64, error) { + if seeker, ok := f.file.(io.Seeker); ok { + return seeker.Seek(offset, whence) + } + return -1, &fs.PathError{ + Op: "seek", + Path: f.path, + Err: fs.ErrInvalid, + } +} + +func (f *wrapper) Write(p []byte) (n int, err error) { + if writer, ok := f.file.(io.Writer); ok { + return writer.Write(p) + } + return -1, &fs.PathError{ + Op: "write", + Path: f.path, + Err: fs.ErrPermission, + } +} + +func (f *wrapper) ReadDir(n int) ([]fs.DirEntry, error) { + if ff, ok := f.file.(fs.ReadDirFile); ok { + if n < 0 { + n = 0 + } + return ff.ReadDir(n) + } + return fs.ReadDir(f.fsys, f.path) +} + +func (f *wrapper) Readdir(count int) ([]fs.FileInfo, error) { + list, err := f.ReadDir(count) + if err != nil { + return nil, err + } + + infos := make([]fs.FileInfo, len(list)) + for i := range list { + info, err := list[i].Info() + if err != nil { + return nil, err + } + infos[i] = info + } + + return infos, nil +} + +func (r *rigfsWrapper) RemoveAll(ctx context.Context, name string) error { + return r.fsys.RemoveAll(name) +} + +func (r *rigfsWrapper) Stat(ctx context.Context, name string) (os.FileInfo, error) { + return r.fsys.Stat(name) +} + +func (r *rigfsWrapper) Rename(ctx context.Context, oldName, newName string) (err error) { + oldFile, err := r.OpenFile(ctx, oldName, os.O_RDONLY, os.ModePerm) + if err != nil { + return err + } + defer func() { + _ = oldFile.Close() + if err != nil { + _ = r.RemoveAll(ctx, oldName) + } + }() + + if err := r.Mkdir(ctx, filepath.Dir(newName), os.ModeDir); err != nil { + return err + } + + newFile, err := r.OpenFile(ctx, oldName, os.O_CREATE, os.ModePerm) + if err != nil { + return err + } + + defer newFile.Close() + if _, err := io.Copy(newFile, oldFile); err != nil { + return err + } + return nil +} diff --git a/pkg/wd/platform.go b/pkg/wd/platform.go new file mode 100644 index 0000000..ce8e2ac --- /dev/null +++ b/pkg/wd/platform.go @@ -0,0 +1,38 @@ +package wd + +func GoOS(id string) (string, bool) { + if id == "darwin" || id == "windows" { + return id, true + } + return "linux", true +} + +func GoArch(os string, unameM string) (string, bool) { + switch os { + case "windows": + v, ok := windowsArches[unameM] + return v, ok + case "linux": + v, ok := linuxArches[unameM] + return v, ok + case "darwin": + v, ok := darwinArches[unameM] + return v, ok + } + return "", false +} + +var windowsArches = map[string]string{ + "x86_64": "amd64", + "aarch64": "arm64", +} + +var linuxArches = map[string]string{ + "x86_64": "amd64", + "aarch64": "arm64", +} + +var darwinArches = map[string]string{ + "x86_64": "amd64", + "arm64": "arm64", +} diff --git a/pkg/wd/util.go b/pkg/wd/util.go new file mode 100644 index 0000000..633bdab --- /dev/null +++ b/pkg/wd/util.go @@ -0,0 +1,47 @@ +package wd + +import ( + "context" + "io/fs" + "path/filepath" + "strings" + + "github.com/octohelm/unifs/pkg/filesystem" +) + +func ListFile(f filesystem.FileSystem, root string, each func(filename string) error) error { + return filesystem.WalkDir(context.Background(), f, root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + rel := path + if root != "" && root != "." { + rel, _ = filepath.Rel(root, path) + } + return each(rel) + }) +} + +type Dir string + +func (d Dir) String() string { + return string(d) +} + +func (d Dir) With(dir string) Dir { + if dir != "" { + if d == "" || strings.HasPrefix(dir, "/") { + return Dir(dir) + } else { + prefix := string(d) + if prefix == "" { + prefix = "/" + } + return Dir(filepath.Join(prefix, dir)) + } + } + return d +} diff --git a/pkg/wd/wd.go b/pkg/wd/wd.go new file mode 100644 index 0000000..8d5784d --- /dev/null +++ b/pkg/wd/wd.go @@ -0,0 +1,177 @@ +package wd + +import ( + "context" + "fmt" + "github.com/octohelm/unifs/pkg/filesystem/local" + "strings" + + "github.com/go-courier/logr" + "github.com/k0sproject/rig" + "github.com/k0sproject/rig/exec" + "github.com/octohelm/unifs/pkg/filesystem" +) + +type WorkDir interface { + fmt.Stringer + + filesystem.FileSystem + + Options() Options + Exec(ctx context.Context, cmd string, optFns ...OptionFunc) error + ExecOutput(ctx context.Context, cmd string, optFns ...OptionFunc) (string, error) +} + +type WorkDirConnection interface { + Connection() *rig.Connection +} + +type OptionFunc func(opt *Options) + +type Options struct { + BasePath Dir + Env map[string]string + User string +} + +func (o *Options) Build(optionFuncs ...OptionFunc) { + for _, fn := range optionFuncs { + fn(o) + } +} + +func WithDir(dir string) OptionFunc { + return func(opt *Options) { + opt.BasePath = opt.BasePath.With(dir) + } +} + +func WithUser(user string) OptionFunc { + return func(opt *Options) { + if user != "" { + opt.User = user + } + } +} + +func WithEnv(env map[string]string) OptionFunc { + return func(opt *Options) { + opt.Env = env + } +} + +func Wrap(c *rig.Connection, optionFuncs ...OptionFunc) (WorkDir, error) { + w := &wd{} + w.connection = c + w.opt.User = "root" + w.opt.BasePath = "/" + + w.opt.Build(optionFuncs...) + + if err := w.init(); err != nil { + return nil, err + } + + return w, nil +} + +func With(source WorkDir, optionFuncs ...OptionFunc) (WorkDir, error) { + if len(optionFuncs) == 0 { + return source, nil + } + + w := &wd{} + w.connection = source.(WorkDirConnection).Connection() + w.opt = source.Options() + + w.opt.Build(optionFuncs...) + + if err := w.init(); err != nil { + return nil, err + } + + return w, nil +} + +type wd struct { + opt Options + filesystem.FileSystem + connection *rig.Connection +} + +func (w *wd) Connection() *rig.Connection { + return w.connection +} + +func (w *wd) init() error { + if !w.connection.IsConnected() { + if err := w.connection.Connect(); err != nil { + return err + } + } + + if w.connection.Localhost != nil { + w.FileSystem = local.NewLocalFS(w.opt.BasePath.String()) + } else { + if w.opt.User == "root" { + w.FileSystem = filesystem.Sub(WrapRigFS(w.connection.SudoFsys()), w.opt.BasePath.String()) + } else { + w.FileSystem = filesystem.Sub(WrapRigFS(w.connection.Fsys()), w.opt.BasePath.String()) + } + } + return nil +} + +func (w *wd) Options() Options { + return w.opt +} + +func (w *wd) String() string { + switch w.connection.Protocol() { + case "Local": + return fmt.Sprintf("%s (%s)", w.opt.BasePath, w.opt.User) + default: + return fmt.Sprintf("%s (%s,%s@%s)", w.opt.BasePath, strings.ToLower(w.connection.Protocol()), w.opt.User, w.connection.Address()) + } +} + +func (w *wd) Exec(ctx context.Context, cmd string, optFns ...OptionFunc) error { + logr.FromContext(ctx).Info(cmd) + b, opts := w.normalizeExecArgs(cmd, optFns...) + return w.connection.Exec(b.String(), opts...) +} + +func (w *wd) ExecOutput(ctx context.Context, cmd string, optFns ...OptionFunc) (output string, err error) { + logr.FromContext(ctx).Info(cmd) + b, opts := w.normalizeExecArgs(cmd, optFns...) + return w.connection.ExecOutput(b.String(), opts...) +} + +func (w *wd) normalizeExecArgs(cmd string, optFns ...OptionFunc) (b *strings.Builder, execOptions []exec.Option) { + b = &strings.Builder{} + + opt := &Options{ + BasePath: w.opt.BasePath, + User: w.opt.User, + } + + for _, optFn := range optFns { + optFn(opt) + } + + if w.connection.Localhost == nil && opt.User == "root" { + execOptions = append(execOptions, exec.Sudo(w.connection)) + } + + if opt.BasePath != "" && opt.BasePath != "/" { + _, _ = fmt.Fprintf(b, "cd %s; ", opt.BasePath) + } + + for k, v := range opt.Env { + _, _ = fmt.Fprintf(b, "%s=%s ", k, v) + } + + b.WriteString(cmd) + + return +} diff --git a/pkg/wd/wd_os.go b/pkg/wd/wd_os.go new file mode 100644 index 0000000..dbe0b17 --- /dev/null +++ b/pkg/wd/wd_os.go @@ -0,0 +1,36 @@ +package wd + +import ( + "context" + "github.com/k0sproject/rig" + v1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +type CanOSInfo interface { + OSInfo(ctx context.Context) (*OSInfo, error) +} + +type OSInfo struct { + rig.OSVersion + + Platform v1.Platform +} + +var _ CanOSInfo = &wd{} + +func (w *wd) OSInfo(ctx context.Context) (*OSInfo, error) { + gnuarch, err := w.connection.ExecOutput("uname -m") + if err != nil { + return nil, err + } + os, _ := GoOS(w.connection.OSVersion.ID) + arch, _ := GoArch(os, gnuarch) + + return &OSInfo{ + Platform: v1.Platform{ + OS: os, + Architecture: arch, + }, + OSVersion: *w.connection.OSVersion, + }, nil +} diff --git a/pkg/wd/wd_test.go b/pkg/wd/wd_test.go new file mode 100644 index 0000000..185e90c --- /dev/null +++ b/pkg/wd/wd_test.go @@ -0,0 +1,46 @@ +package wd + +import ( + "github.com/k0sproject/rig" + testingx "github.com/octohelm/x/testing" + "golang.org/x/net/context" + "os" + "testing" +) + +func TestFS(t *testing.T) { + dir := t.TempDir() + t.Cleanup(func() { + _ = os.RemoveAll(dir) + }) + + local, err := Wrap( + &rig.Connection{ + Localhost: &rig.Localhost{ + Enabled: true, + }, + }, + WithDir(dir), + WithUser("root"), + ) + testingx.Expect(t, err, testingx.Be[error](nil)) + + t.Run("touch file", func(t *testing.T) { + f, err := local.OpenFile(context.Background(), "1.txt", os.O_RDWR|os.O_CREATE, os.ModePerm) + testingx.Expect(t, err, testingx.Be[error](nil)) + f.Close() + + t.Run("ls", func(t *testing.T) { + files := make([]string, 0) + + err = ListFile(local, ".", func(filename string) error { + files = append(files, filename) + return nil + }) + testingx.Expect(t, err, testingx.Be[error](nil)) + testingx.Expect(t, files, testingx.Equal([]string{ + "1.txt", + })) + }) + }) +}