From 3cbfbbb1ab9afff58b62858d3b5161958007b4e0 Mon Sep 17 00:00:00 2001 From: UncleGedd <42304551+UncleGedd@users.noreply.github.com> Date: Mon, 25 Mar 2024 13:48:36 -0500 Subject: [PATCH] feat: beautiful TUI round 3 (#509) --- README.md | 8 + adr/0004-tui.md | 21 ++ go.mod | 52 ++-- go.sum | 84 +++---- src/cmd/root.go | 25 +- src/cmd/uds.go | 82 ++++++- src/cmd/version.go | 2 +- src/cmd/viper.go | 1 + src/config/config.go | 13 +- src/config/lang/lang.go | 4 + src/pkg/bundle/common.go | 14 +- src/pkg/bundle/deploy.go | 137 +++++------ src/pkg/bundle/pull.go | 3 +- src/pkg/bundle/remote.go | 8 +- src/pkg/bundle/tarball.go | 28 ++- src/pkg/bundle/tui/common.go | 22 ++ src/pkg/bundle/tui/deploy/handlers.go | 172 ++++++++++++++ src/pkg/bundle/tui/deploy/model.go | 290 +++++++++++++++++++++++ src/pkg/bundle/tui/deploy/views.go | 260 ++++++++++++++++++++ src/pkg/bundler/fetcher/remote.go | 6 +- src/pkg/bundler/pusher/remote.go | 2 +- src/pkg/sources/remote.go | 71 ++++-- src/pkg/sources/tarball.go | 63 +++-- src/pkg/utils/oci.go | 17 +- src/pkg/utils/utils.go | 96 +++++--- src/test/bundles/04-init/uds-bundle.yaml | 4 +- src/test/e2e/bundle_test.go | 11 +- src/test/e2e/commands_test.go | 18 +- src/test/e2e/main_test.go | 2 +- src/test/e2e/variable_test.go | 8 +- src/test/packages/gitrepo/zarf.yaml | 18 ++ src/types/{bundler.go => options.go} | 6 +- tasks.schema.json | 106 ++++----- zarf.schema.json | 158 ++++++------ 34 files changed, 1395 insertions(+), 417 deletions(-) create mode 100644 adr/0004-tui.md create mode 100644 src/pkg/bundle/tui/common.go create mode 100644 src/pkg/bundle/tui/deploy/handlers.go create mode 100644 src/pkg/bundle/tui/deploy/model.go create mode 100644 src/pkg/bundle/tui/deploy/views.go rename src/types/{bundler.go => options.go} (89%) diff --git a/README.md b/README.md index 7c8fab54..aa9ee465 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ - [Inspect](#bundle-inspect) - [Publish](#bundle-publish) - [Remove](#bundle-remove) + - [Logs](#logs) 1. [Bundle Architecture and Multi-Arch Support](#bundle-architecture-and-multi-arch-support) 1. [Configuration](#configuration) 1. [Sharing Variables](#sharing-variables) @@ -127,6 +128,13 @@ By default all the packages in the bundle are removed, but you can also remove o As an example: `uds remove uds-bundle-.tar.zst --packages init,nginx` +### Logs + +> [!NOTE] +> Only works with `uds deploy` for now, may work for other operations but isn't guaranteed. + +The `uds logs` command can be used to view the most recent logs of a bundle operation. Note that depending on your OS temporary directory and file settings, recent logs are purged after a certain amount of time, so this command may return an error if the logs are no longer available. + ## Bundle Architecture and Multi-Arch Support There are several ways to specify the architecture of a bundle: 1. Setting `--architecture` or `-a` flag during `uds ...` operations: `uds create --architecture arm64` diff --git a/adr/0004-tui.md b/adr/0004-tui.md new file mode 100644 index 00000000..cdf3502b --- /dev/null +++ b/adr/0004-tui.md @@ -0,0 +1,21 @@ +# 3. Terminal User Interface (TUI) + +Date: 22 March 2024 + +## Status +Accepted + +## Context +- Today, UDS CLI leverages the `pterm` library and `messaging` pkg in Zarf to provide a terminal user interface (TUI) for users. The TUI is used to display progress bars, tables, etc to the user. The current implementation of the TUI is tightly coupled with Zarf, making it difficult to customize and address the unique needs of UDS CLI. + + +- The UDS CLI team has formed the following hypothesis about UDS CLI users: UDS CLI users (particularly those doing `uds deploy`) are more interested in the status of their deployment than the details of the deployment process. + + +- In an effort to address the unique needs of UDS CLI users, the UDS CLI team would like to decouple UDS CLI from the Zarf TUI in an effort to make a custom user experience for UDS CLI users. + +## Decision +The UDS CLI team has decided to refactor the existing TUI implementation with [BubbleTea](https://github.com/charmbracelet/bubbletea) and other tools from the [Charm](https://charm.sh/) stack. We believe the Charm stack offers the most advanced and modern tooling for building TUIs in Go. + +## Consequences +This refactor is large and will be implemented one `uds` operation at time, starting with operations most likely to be performed during and after bundle installation. diff --git a/go.mod b/go.mod index 0b19b502..3f518a26 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,12 @@ go 1.21.6 require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b - github.com/defenseunicorns/maru-runner v0.0.2 - github.com/defenseunicorns/zarf v0.32.5 + github.com/charmbracelet/bubbles v0.16.1 + github.com/charmbracelet/bubbletea v0.25.0 + github.com/charmbracelet/lipgloss v0.9.1 + github.com/defenseunicorns/maru-runner v0.1.0 + github.com/defenseunicorns/zarf v0.32.6 + github.com/fatih/color v1.16.0 github.com/fsnotify/fsnotify v1.7.0 github.com/goccy/go-yaml v1.11.3 github.com/mholt/archiver/v3 v3.5.1 @@ -19,6 +23,7 @@ require ( golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 golang.org/x/mod v0.16.0 golang.org/x/sync v0.6.0 + golang.org/x/term v0.18.0 helm.sh/helm/v3 v3.14.3 oras.land/oras-go/v2 v2.4.0 ) @@ -27,12 +32,12 @@ require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect - cloud.google.com/go v0.111.0 // indirect - cloud.google.com/go/compute v1.23.3 // indirect + cloud.google.com/go v0.112.1 // indirect + cloud.google.com/go/compute v1.24.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v1.1.5 // indirect - cloud.google.com/go/kms v1.15.5 // indirect - cloud.google.com/go/storage v1.35.1 // indirect + cloud.google.com/go/iam v1.1.6 // indirect + cloud.google.com/go/kms v1.15.7 // indirect + cloud.google.com/go/storage v1.38.0 // indirect cuelabs.dev/go/oci/ociregistry v0.0.0-20231103182354-93e78c079a13 // indirect cuelang.org/go v0.7.0 // indirect dario.cat/mergo v1.0.0 // indirect @@ -138,10 +143,7 @@ require ( github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect - github.com/charmbracelet/bubbles v0.16.1 // indirect - github.com/charmbracelet/bubbletea v0.25.0 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/charmbracelet/lipgloss v0.9.1 // indirect github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect @@ -174,7 +176,7 @@ require ( github.com/distribution/reference v0.5.0 // indirect github.com/docker/cli v24.0.9+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker v24.0.7+incompatible // indirect + github.com/docker/docker v24.0.9+incompatible // indirect github.com/docker/docker-credential-helpers v0.8.0 // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect @@ -191,7 +193,6 @@ require ( github.com/facebookincubator/nvdtools v0.1.5 // indirect github.com/fairwindsops/pluto/v5 v5.18.4 // indirect github.com/fatih/camelcase v1.0.0 // indirect - github.com/fatih/color v1.16.0 // indirect github.com/felixge/fgprof v0.9.3 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fluxcd/helm-controller/api v0.37.4 // indirect @@ -250,7 +251,7 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/googleapis/gax-go/v2 v2.12.1 // indirect github.com/gookit/color v1.5.4 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect @@ -445,12 +446,12 @@ require ( go.mongodb.org/mongo-driver v1.13.1 // indirect go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect - go.opentelemetry.io/otel v1.22.0 // indirect - go.opentelemetry.io/otel/metric v1.22.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.48.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 // indirect + go.opentelemetry.io/otel v1.23.0 // indirect + go.opentelemetry.io/otel/metric v1.23.0 // indirect go.opentelemetry.io/otel/sdk v1.22.0 // indirect - go.opentelemetry.io/otel/trace v1.22.0 // indirect + go.opentelemetry.io/otel/trace v1.23.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect go.step.sm/crypto v0.42.1 // indirect go.uber.org/multierr v1.11.0 // indirect @@ -458,20 +459,19 @@ require ( go4.org v0.0.0-20200411211856-f5505b9728dd // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/net v0.22.0 // indirect - golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/oauth2 v0.17.0 // indirect golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.19.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect - google.golang.org/api v0.159.0 // indirect + google.golang.org/api v0.166.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect - google.golang.org/grpc v1.61.0 // indirect - google.golang.org/protobuf v1.32.0 // indirect + google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect + google.golang.org/grpc v1.61.1 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/evanphx/json-patch.v5 v5.6.0 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 8228d5c0..a2471c50 100644 --- a/go.sum +++ b/go.sum @@ -39,8 +39,8 @@ cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w9 cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= -cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM= -cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU= +cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= +cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= @@ -77,8 +77,8 @@ cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= +cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= @@ -119,10 +119,10 @@ cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y97 cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= -cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= -cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= -cloud.google.com/go/kms v1.15.5 h1:pj1sRfut2eRbD9pFRjNnPNg/CzJPuQAzUujMIM1vVeM= -cloud.google.com/go/kms v1.15.5/go.mod h1:cU2H5jnp6G2TDpUGZyqTCoy1n16fbubHZjmVXSMtwDI= +cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc= +cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= +cloud.google.com/go/kms v1.15.7 h1:7caV9K3yIxvlQPAcaFffhlT7d1qpxjB1wHBtjWa13SM= +cloud.google.com/go/kms v1.15.7/go.mod h1:ub54lbsa6tDkUwnu4W7Yt1aAIFLnspgh0kPGToDukeI= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= @@ -183,8 +183,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= -cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w= -cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= +cloud.google.com/go/storage v1.38.0 h1:Az68ZRGlnNTpIBbLjSMIV2BDcwwXYlRlQzis0llkpJg= +cloud.google.com/go/storage v1.38.0/go.mod h1:tlUADB0mAb9BgYls9lq+8MGkfzOXuLrnHXlpHmvFJoY= cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= @@ -598,10 +598,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/daviddengcn/go-colortext v1.0.0 h1:ANqDyC0ys6qCSvuEK7l3g5RaehL/Xck9EX8ATG8oKsE= github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c= -github.com/defenseunicorns/maru-runner v0.0.2 h1:aZ/MCryY3Dp+u14nhGrvOXVnZI3DYY3ytjFdfHnyqY4= -github.com/defenseunicorns/maru-runner v0.0.2/go.mod h1:3K+JeLpud+rb8vC+nPFaTNjhqW40++6qFKKVTBEEzQM= -github.com/defenseunicorns/zarf v0.32.5 h1:g7hQPCUEd8PlK4s6f+e7naCS9q85ghWLjegEbcgUvPs= -github.com/defenseunicorns/zarf v0.32.5/go.mod h1:dDIYZdh4XGkI/A6EpZHEBzQaQjw0hSbxJs4MQFOSqLc= +github.com/defenseunicorns/maru-runner v0.1.0 h1:LCrMzA+TaQMV2LdvMVrd319jDAIL9CuwhIes0vKf7Kg= +github.com/defenseunicorns/maru-runner v0.1.0/go.mod h1:/0Qu2O8WyZuYB+6LkUsgUwuHiQS8AgXU/Tw7Re0GaTk= +github.com/defenseunicorns/zarf v0.32.6 h1:zWfKmAo898P7OHosKQaiV4E6YjDrqpsryz0BqrQqgg4= +github.com/defenseunicorns/zarf v0.32.6/go.mod h1:EISjg8T4jZUgc1QAMkX/RNQ0PS4WegKcJDZwztvQ2Bw= github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da h1:ZOjWpVsFZ06eIhnh4mkaceTiVoktdU67+M7KDHJ268M= github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4imdLi4Asdha1Sc6feLMTfPLXh9IUYmysk= github.com/depcheck-test/depcheck-test v0.0.0-20220607135614-199033aaa936 h1:foGzavPWwtoyBvjWyKJYDYsyzy+23iBV7NKTwdk+LRY= @@ -638,8 +638,8 @@ github.com/docker/cli v24.0.9+incompatible h1:OxbimnP/z+qVjDLpq9wbeFU3Nc30XhSe+L github.com/docker/cli v24.0.9+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= -github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= +github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8= github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= @@ -986,8 +986,8 @@ github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99 github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= -github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/gax-go/v2 v2.12.1 h1:9F8GV9r9ztXyAi00gsMQHNoF51xPZm8uj1dpYt2ZETM= +github.com/googleapis/gax-go/v2 v2.12.1/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= @@ -1731,22 +1731,22 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= -go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= -go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.48.0 h1:P+/g8GpuJGYbOp2tAdKrIPUX9JO02q8Q0YNlHolpibA= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.48.0/go.mod h1:tIKj3DbO8N9Y2xo52og3irLsPI4GW02DSMtrVgNMgxg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 h1:doUP+ExOpH3spVTLS0FcWGLnQrPct/hD/bCPbDRUEAU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA= +go.opentelemetry.io/otel v1.23.0 h1:Df0pqjqExIywbMCMTxkAwzjLZtRf+bBKLbUcpxO2C9E= +go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0 h1:H2JFgRcGiyHg7H7bwcwaQJYrNFqCqrbTQ8K4p1OvDu8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0/go.mod h1:WfCWp1bGoYK8MeULtI15MmQVczfR+bFkk0DF3h06QmQ= -go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= -go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= +go.opentelemetry.io/otel/metric v1.23.0 h1:pazkx7ss4LFVVYSxYew7L5I6qvLXHA0Ap2pwV+9Cnpo= +go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo= go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= -go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= -go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +go.opentelemetry.io/otel/trace v1.23.0 h1:37Ik5Ib7xfYVb4V1UtnT97T1jI+AoIYkJyPkuL4iJgI= +go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= @@ -1927,8 +1927,8 @@ golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= +golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -2210,8 +2210,8 @@ google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.159.0 h1:fVTj+7HHiUYz4JEZCHHoRIeQX7h5FMzrA2RF/DzDdbs= -google.golang.org/api v0.159.0/go.mod h1:0mu0TpK33qnydLvWqbImq2b1eQ5FHRSDCBzAxX9ZHyw= +google.golang.org/api v0.166.0 h1:6m4NUwrZYhAaVIHZWxaKjw1L1vNAjtMwORmKRyEEo24= +google.golang.org/api v0.166.0/go.mod h1:4FcBc686KFi7QI/U51/2GKKevfZMpM17sCdibqe/bSA= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -2326,12 +2326,12 @@ google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqw google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 h1:nz5NESFLZbJGPFxDT/HCn+V1mZ8JGNoY4nUpmW/Y2eg= -google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917/go.mod h1:pZqR+glSb11aJ+JQcczCvgf47+duRuzNSKqE8YAQnV0= -google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM= -google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= +google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c h1:9g7erC9qu44ks7UK4gDNlnk4kOxZG707xKm4jVniy6o= +google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -2368,8 +2368,8 @@ google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACu google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= -google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY= +google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -2386,8 +2386,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 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= diff --git a/src/cmd/root.go b/src/cmd/root.go index 0f980929..754b44d6 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -13,8 +13,8 @@ import ( "github.com/defenseunicorns/uds-cli/src/pkg/utils" "github.com/defenseunicorns/uds-cli/src/types" "github.com/defenseunicorns/zarf/src/cmd/common" + zarfCommon "github.com/defenseunicorns/zarf/src/cmd/common" "github.com/defenseunicorns/zarf/src/pkg/message" - "github.com/defenseunicorns/zarf/src/pkg/utils/exec" "github.com/spf13/cobra" ) @@ -33,13 +33,17 @@ var rootCmd = &cobra.Command{ return } - exec.ExitOnInterrupt() + zarfCommon.ExitOnInterrupt() // Don't add the logo to the help command if cmd.Parent() == nil { config.SkipLogFile = true } - cliSetup() + + // don't load log configs for the logs command + if cmd.Use != "logs" { + cliSetup(cmd.Use) + } }, Short: lang.RootCmdShort, Run: func(cmd *cobra.Command, _ []string) { @@ -72,6 +76,7 @@ func init() { v.SetDefault(V_INSECURE, false) v.SetDefault(V_TMP_DIR, "") v.SetDefault(V_BNDL_OCI_CONCURRENCY, 3) + v.SetDefault(V_NO_TEA, false) // by default use the BubbleTea TUI homeDir, _ := os.UserHomeDir() v.SetDefault(V_UDS_CACHE, filepath.Join(homeDir, config.UDSCache)) @@ -84,9 +89,10 @@ func init() { rootCmd.PersistentFlags().StringVar(&config.CommonOptions.TempDirectory, "tmpdir", v.GetString(V_TMP_DIR), lang.RootCmdFlagTempDir) rootCmd.PersistentFlags().BoolVar(&config.CommonOptions.Insecure, "insecure", v.GetBool(V_INSECURE), lang.RootCmdFlagInsecure) rootCmd.PersistentFlags().IntVar(&config.CommonOptions.OCIConcurrency, "oci-concurrency", v.GetInt(V_BNDL_OCI_CONCURRENCY), lang.CmdBundleFlagConcurrency) + rootCmd.PersistentFlags().BoolVar(&config.CommonOptions.NoTea, "no-tea", v.GetBool(V_NO_TEA), lang.RootCmdNoTea) } -func cliSetup() { +func cliSetup(op string) { match := map[string]message.LogLevel{ "warn": message.WarnLevel, "info": message.InfoLevel, @@ -106,13 +112,10 @@ func cliSetup() { } } - // Disable progress bars for CI envs - if os.Getenv("CI") == "true" { - message.Debug("CI environment detected, disabling progress bars") - message.NoProgress = true - } - if !config.SkipLogFile && !config.ListTasks { - utils.UseLogFile() + err := utils.ConfigureLogs(op) + if err != nil { + message.Fatalf(err, "Error configuring logs") + } } } diff --git a/src/cmd/uds.go b/src/cmd/uds.go index ce266a7e..964861ef 100644 --- a/src/cmd/uds.go +++ b/src/cmd/uds.go @@ -5,20 +5,26 @@ package cmd import ( + "errors" + "fmt" + "io" "os" "path/filepath" "strings" "github.com/AlecAivazis/survey/v2" + tea "github.com/charmbracelet/bubbletea" "github.com/defenseunicorns/uds-cli/src/config" "github.com/defenseunicorns/uds-cli/src/config/lang" "github.com/defenseunicorns/uds-cli/src/pkg/bundle" + "github.com/defenseunicorns/uds-cli/src/pkg/bundle/tui/deploy" zarfConfig "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/pkg/message" - zarfUtils "github.com/defenseunicorns/zarf/src/pkg/utils" + "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" zarfTypes "github.com/defenseunicorns/zarf/src/types" goyaml "github.com/goccy/go-yaml" "github.com/spf13/cobra" + "golang.org/x/term" ) var createCmd = &cobra.Command{ @@ -29,7 +35,7 @@ var createCmd = &cobra.Command{ PreRun: func(_ *cobra.Command, args []string) { pathToBundleFile := "" if len(args) > 0 { - if !zarfUtils.IsDir(args[0]) { + if !helpers.IsDir(args[0]) { message.Fatalf(nil, "(%q) is not a valid path to a directory", args[0]) } pathToBundleFile = filepath.Join(args[0]) @@ -80,16 +86,52 @@ var deployCmd = &cobra.Command{ return } } + // create new bundle client bndlClient := bundle.NewOrDie(&bundleCfg) defer bndlClient.ClearPaths() - if err := bndlClient.Deploy(); err != nil { - bndlClient.ClearPaths() - message.Fatalf(err, "Failed to deploy bundle: %s", err.Error()) + // don't use bubbletea if --no-tea flag is set + if config.CommonOptions.NoTea { + deployWithoutTea(bndlClient) + return + } + + // start up bubbletea + m := deploy.InitModel(bndlClient) + + // detect tty so CI/containers don't break + if term.IsTerminal(int(os.Stdout.Fd())) { + deploy.Program = tea.NewProgram(&m) + } else { + deploy.Program = tea.NewProgram(&m, tea.WithInput(nil)) + } + + if _, err := deploy.Program.Run(); err != nil { + message.Fatalf(err, "TUI program error: %s", err.Error()) } }, } +func deployWithoutTea(bndlClient *bundle.Bundle) { + _, _, _, err := bndlClient.PreDeployValidation() + if err != nil { + message.Fatalf(err, "Failed to validate bundle: %s", err.Error()) + } + // confirm deployment + if ok := bndlClient.ConfirmBundleDeploy(); !ok { + message.Fatal(nil, "bundle deployment cancelled") + } + // create an empty program and kill it, this makes Program.Send a no-op + deploy.Program = tea.NewProgram(nil) + deploy.Program.Kill() + + // deploy the bundle + if err := bndlClient.Deploy(); err != nil { + bndlClient.ClearPaths() + message.Fatalf(err, "Failed to deploy bundle: %s", err.Error()) + } +} + var inspectCmd = &cobra.Command{ Use: "inspect [BUNDLE_TARBALL|OCI_REF]", Aliases: []string{"i"}, @@ -175,6 +217,33 @@ var pullCmd = &cobra.Command{ }, } +var logsCmd = &cobra.Command{ + Use: "logs", + Aliases: []string{"l"}, + Short: "Display log file contents", + Run: func(cmd *cobra.Command, args []string) { + logFilePath := filepath.Join(config.CommonOptions.CachePath, config.CachedLogs) + + // Open the cached log file + logfile, err := os.Open(logFilePath) + if err != nil { + var pathError *os.PathError + if errors.As(err, &pathError) { + msg := fmt.Sprintf("No cached logs found at %s", logFilePath) + message.Fatalf(nil, msg) + } + message.Fatalf("Error opening log file: %s\n", err.Error()) + } + defer logfile.Close() + + // Copy the contents of the log file to stdout + if _, err := io.Copy(os.Stdout, logfile); err != nil { + // Handle the error if the contents can't be read or written to stdout + message.Fatalf(err, "Error reading or printing log file: %v\n", err.Error()) + } + }, +} + // loadViperConfig reads the config file and unmarshals the relevant config into DeployOpts.Variables func loadViperConfig() error { // get config file from Viper @@ -246,6 +315,9 @@ func init() { rootCmd.AddCommand(pullCmd) pullCmd.Flags().StringVarP(&bundleCfg.PullOpts.OutputDirectory, "output", "o", v.GetString(V_BNDL_PULL_OUTPUT), lang.CmdBundlePullFlagOutput) pullCmd.Flags().StringVarP(&bundleCfg.PullOpts.PublicKeyPath, "key", "k", v.GetString(V_BNDL_PULL_KEY), lang.CmdBundlePullFlagKey) + + // logs cmd + rootCmd.AddCommand(logsCmd) } // configureZarf copies configs from UDS-CLI to Zarf diff --git a/src/cmd/version.go b/src/cmd/version.go index 495dab60..926c2a3d 100644 --- a/src/cmd/version.go +++ b/src/cmd/version.go @@ -17,7 +17,7 @@ var versionCmd = &cobra.Command{ Aliases: []string{"v"}, PersistentPreRun: func(_ *cobra.Command, _ []string) { config.SkipLogFile = true - cliSetup() + cliSetup("") }, Short: lang.CmdVersionShort, Long: lang.CmdVersionLong, diff --git a/src/cmd/viper.go b/src/cmd/viper.go index 07ad89c9..d242a78c 100644 --- a/src/cmd/viper.go +++ b/src/cmd/viper.go @@ -25,6 +25,7 @@ const ( V_TMP_DIR = "options.tmp_dir" V_INSECURE = "options.insecure" V_BNDL_OCI_CONCURRENCY = "options.oci_concurrency" + V_NO_TEA = "options.no_tea" // Bundle create config keys V_BNDL_CREATE_OUTPUT = "create.output" diff --git a/src/config/config.go b/src/config/config.go index c6658218..f0772e6e 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -49,9 +49,6 @@ const ( // UDSCacheLayers is the directory in the cache containing cached bundle layers UDSCacheLayers = "layers" - // TasksYAML is the default name of the uds run cmd file - TasksYAML = "tasks.yaml" - // EnvVarPrefix is the prefix for environment variables to override bundle helm variables EnvVarPrefix = "UDS_" @@ -60,6 +57,9 @@ const ( // UDSPackageNameAnnotation is the annotation key for the value that specifies the name given to a zarf package in the uds-bundle.yaml UDSPackageNameAnnotation = "uds.package.name" + + // CachedLogs is a file containing cached logs + CachedLogs = "recent-logs" ) var ( @@ -78,11 +78,8 @@ var ( // ListTasks is a flag to print available tasks in a TaskFileLocation ListTasks bool - // TaskFileLocation is the location of the tasks file to run - TaskFileLocation string - - // SetRunnerVariables is a map of the run time variables defined using --set - SetRunnerVariables map[string]string + // LogFileName is the name of the UDS log file + LogFileName string // HelmTimeout is the default timeout for helm deploys HelmTimeout = 15 * time.Minute diff --git a/src/config/lang/lang.go b/src/config/lang/lang.go index 84a0ba3f..ef06a452 100644 --- a/src/config/lang/lang.go +++ b/src/config/lang/lang.go @@ -15,6 +15,10 @@ const ( RootCmdFlagLogLevel = "Log level when running UDS-CLI. Valid options are: warn, info, debug, trace" RootCmdErrInvalidLogLevel = "Invalid log level. Valid options are: warn, info, debug, trace." RootCmdFlagArch = "Architecture for UDS bundles and Zarf packages" + RootCmdNoTea = "Don't use the BubbleTea TUI" + + // logs + CmdBundleLogsShort = "View most recent UDS CLI logs" // bundle CmdBundleShort = "Commands for creating, deploying, removing, pulling, and inspecting bundles" diff --git a/src/pkg/bundle/common.go b/src/pkg/bundle/common.go index 54c34284..5130f356 100644 --- a/src/pkg/bundle/common.go +++ b/src/pkg/bundle/common.go @@ -20,7 +20,7 @@ import ( "github.com/defenseunicorns/zarf/src/pkg/cluster" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/oci" - "github.com/defenseunicorns/zarf/src/pkg/utils" + zarfUtils "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/pkg/zoci" zarfTypes "github.com/defenseunicorns/zarf/src/types" @@ -51,7 +51,7 @@ func New(cfg *types.BundleConfig) (*Bundle, error) { } ) - tmp, err := utils.MakeTempDir(config.CommonOptions.TempDirectory) + tmp, err := zarfUtils.MakeTempDir(config.CommonOptions.TempDirectory) if err != nil { return nil, fmt.Errorf("bundler unable to create temp directory: %w", err) } @@ -72,7 +72,7 @@ func NewOrDie(cfg *types.BundleConfig) *Bundle { return bundle } -// ClearPaths clears out the paths used by Bundle +// ClearPaths closes any files and clears out the paths used by Bundle func (b *Bundle) ClearPaths() { _ = os.RemoveAll(b.tmp) } @@ -244,7 +244,7 @@ func (b *Bundle) CalculateBuildInfo() error { // ValidateBundleSignature validates the bundle signature func ValidateBundleSignature(bundleYAMLPath, signaturePath, publicKeyPath string) error { - if utils.InvalidPath(bundleYAMLPath) { + if helpers.InvalidPath(bundleYAMLPath) { return fmt.Errorf("path for %s at %s does not exist", config.BundleYAML, bundleYAMLPath) } // The package is not signed, and no public key was provided @@ -252,16 +252,16 @@ func ValidateBundleSignature(bundleYAMLPath, signaturePath, publicKeyPath string return nil } // The package is not signed, but a public key was provided - if utils.InvalidPath(signaturePath) && !utils.InvalidPath(publicKeyPath) { + if helpers.InvalidPath(signaturePath) && !helpers.InvalidPath(publicKeyPath) { return fmt.Errorf("package is not signed, but a public key was provided") } // The package is signed, but no public key was provided - if !utils.InvalidPath(signaturePath) && utils.InvalidPath(publicKeyPath) { + if !helpers.InvalidPath(signaturePath) && helpers.InvalidPath(publicKeyPath) { return fmt.Errorf("package is signed, but no public key was provided") } // The package is signed, and a public key was provided - return utils.CosignVerifyBlob(bundleYAMLPath, signaturePath, publicKeyPath) + return zarfUtils.CosignVerifyBlob(bundleYAMLPath, signaturePath, publicKeyPath) } // GetDeployedPackages returns packages that have been deployed diff --git a/src/pkg/bundle/deploy.go b/src/pkg/bundle/deploy.go index 067130ec..c2b83841 100644 --- a/src/pkg/bundle/deploy.go +++ b/src/pkg/bundle/deploy.go @@ -14,6 +14,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/defenseunicorns/uds-cli/src/config" + "github.com/defenseunicorns/uds-cli/src/pkg/bundle/tui/deploy" "github.com/defenseunicorns/uds-cli/src/pkg/sources" "github.com/defenseunicorns/uds-cli/src/types" zarfConfig "github.com/defenseunicorns/zarf/src/config" @@ -22,7 +23,7 @@ import ( "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" zarfTypes "github.com/defenseunicorns/zarf/src/types" - "github.com/pterm/pterm" + goyaml "github.com/goccy/go-yaml" "golang.org/x/exp/slices" "helm.sh/helm/v3/pkg/cli/values" "helm.sh/helm/v3/pkg/getter" @@ -36,64 +37,8 @@ var templatedVarRegex = regexp.MustCompile(`\${([^}]+)}`) // Deploy deploys a bundle func (b *Bundle) Deploy() error { - - pterm.Println() - metadataSpinner := message.NewProgressSpinner("Loading bundle metadata") - - defer metadataSpinner.Stop() - - // Check that provided oci source path is valid, and update it if it's missing the full path - source, err := CheckOCISourcePath(b.cfg.DeployOpts.Source) - if err != nil { - return err - } - b.cfg.DeployOpts.Source = source - - // validate config's arch against cluster - err = ValidateArch(config.GetArch()) - if err != nil { - return err - } - - // create a new provider - provider, err := NewBundleProvider(b.cfg.DeployOpts.Source, b.tmp) - if err != nil { - return err - } - - // pull the bundle's metadata + sig - loaded, err := provider.LoadBundleMetadata() - if err != nil { - return err - } - - // validate the sig (if present) - if err := ValidateBundleSignature(loaded[config.BundleYAML], loaded[config.BundleYAMLSignature], b.cfg.DeployOpts.PublicKeyPath); err != nil { - return err - } - - // read the bundle's metadata into memory - // todo: we also read the SHAs from the uds-bundle.yaml here, should we refactor so that we use the bundle's root manifest? - if err := utils.ReadYaml(loaded[config.BundleYAML], &b.bundle); err != nil { - return err - } - - metadataSpinner.Successf("Loaded bundle metadata") - - // confirm deploy - if ok := b.confirmBundleDeploy(); !ok { - return fmt.Errorf("bundle deployment cancelled") - } - - // Check if --resume is set resume := b.cfg.DeployOpts.Resume - // Maps name given to zarf package in the bundle to the actual name of the zarf package - zarfPackageNameMap, err := provider.ZarfPackageNameMap() - if err != nil { - return err - } - // Check if --packages flag is set and zarf packages have been specified var packagesToDeploy []types.Package if len(b.cfg.DeployOpts.Packages) != 0 { @@ -109,13 +54,13 @@ func (b *Bundle) Deploy() error { if len(userSpecifiedPackages) != len(packagesToDeploy) { return fmt.Errorf("invalid zarf packages specified by --packages") } - return deployPackages(packagesToDeploy, resume, b, zarfPackageNameMap) + return deployPackages(packagesToDeploy, resume, b) } - return deployPackages(b.bundle.Packages, resume, b, zarfPackageNameMap) + return deployPackages(b.bundle.Packages, resume, b) } -func deployPackages(packages []types.Package, resume bool, b *Bundle, zarfPackageNameMap map[string]string) error { +func deployPackages(packages []types.Package, resume bool, b *Bundle) error { // map of Zarf pkgs and their vars bundleExportedVars := make(map[string]map[string]string) @@ -132,8 +77,11 @@ func deployPackages(packages []types.Package, resume bool, b *Bundle, zarfPackag packagesToDeploy = packages } + // let TUI know how many packages are being deployed + deploy.Program.Send(fmt.Sprintf("totalPackages:%d", len(packagesToDeploy))) + // deploy each package - for _, pkg := range packagesToDeploy { + for i, pkg := range packagesToDeploy { sha := strings.Split(pkg.Ref, "@sha256:")[1] // using appended SHA from create! pkgTmp, err := utils.MakeTempDir(config.CommonOptions.TempDirectory) if err != nil { @@ -180,7 +128,7 @@ func deployPackages(packages []types.Package, resume bool, b *Bundle, zarfPackag // Automatically confirm the package deployment zarfConfig.CommonOptions.Confirm = true - source, err := sources.New(b.cfg.DeployOpts.Source, zarfPackageNameMap[pkg.Name], opts, sha) + source, err := sources.New(b.cfg.DeployOpts.Source, b.cfg.DeployOpts.ZarfPackageNameMap[pkg.Name], opts, sha) if err != nil { return err } @@ -189,10 +137,15 @@ func deployPackages(packages []types.Package, resume bool, b *Bundle, zarfPackag if err != nil { return err } + + deploy.Program.Send(fmt.Sprintf("newPackage:%s:%d", pkg.Name, i)) + if err := pkgClient.Deploy(); err != nil { return err } + deploy.Program.Send(fmt.Sprintf("complete:%d", i)) + // save exported vars pkgExportedVars := make(map[string]string) for _, exp := range pkg.Exports { @@ -254,8 +207,8 @@ func (b *Bundle) loadVariables(pkg types.Package, bundleExportedVars map[string] return pkgVars } -// confirmBundleDeploy prompts the user to confirm bundle creation -func (b *Bundle) confirmBundleDeploy() (confirm bool) { +// ConfirmBundleDeploy uses Zarf's pterm logging to prompt the user to confirm bundle creation +func (b *Bundle) ConfirmBundleDeploy() (confirm bool) { message.HeaderInfof("šŸŽ BUNDLE DEFINITION") utils.ColorPrintYAML(b.bundle, nil, false) @@ -271,8 +224,6 @@ func (b *Bundle) confirmBundleDeploy() (confirm bool) { Message: "Deploy this bundle?", } - pterm.Println() - if err := survey.AskOne(prompt, &confirm); err != nil || !confirm { return false } @@ -330,6 +281,60 @@ func (b *Bundle) loadChartOverrides(pkg types.Package, pkgVars map[string]string return processed, nil } +func (b *Bundle) PreDeployValidation() (string, string, string, error) { + + // Check that provided oci source path is valid, and update it if it's missing the full path + source, err := CheckOCISourcePath(b.cfg.DeployOpts.Source) + if err != nil { + return "", "", "", err + } + b.cfg.DeployOpts.Source = source + + // validate config's arch against cluster + err = ValidateArch(config.GetArch()) + if err != nil { + return "", "", "", err + } + + // create a new provider + provider, err := NewBundleProvider(b.cfg.DeployOpts.Source, b.tmp) + if err != nil { + return "", "", "", err + } + + // pull the bundle's metadata + sig + loaded, err := provider.LoadBundleMetadata() + if err != nil { + return "", "", "", err + } + + // validate the sig (if present) + if err := ValidateBundleSignature(loaded[config.BundleYAML], loaded[config.BundleYAMLSignature], b.cfg.DeployOpts.PublicKeyPath); err != nil { + return "", "", "", err + } + + // read in file at config.BundleYAML + message.Debugf("Reading YAML at %s", loaded[config.BundleYAML]) + bundleYAML, err := os.ReadFile(loaded[config.BundleYAML]) + if err != nil { + return "", "", "", err + } + + // todo: we also read the SHAs from the uds-bundle.yaml here, should we refactor so that we use the bundle's root manifest? + if err := goyaml.Unmarshal(bundleYAML, &b.bundle); err != nil { + return "", "", "", err + } + + // Maps name given to zarf package in the bundle to the actual name of the zarf package + zarfPackageNameMap, err := provider.ZarfPackageNameMap() + if err != nil { + return "", "", "", err + } + b.cfg.DeployOpts.ZarfPackageNameMap = zarfPackageNameMap + bundleName := b.bundle.Metadata.Name + return bundleName, string(bundleYAML), source, err +} + // processOverrideValues processes a bundles values overrides and adds them to the override map func (b *Bundle) processOverrideValues(overrideMap *map[string]map[string]*values.Options, values *[]types.BundleChartValue, componentName string, chartName string, pkgVars map[string]string) error { for _, v := range *values { diff --git a/src/pkg/bundle/pull.go b/src/pkg/bundle/pull.go index 41815746..54e16072 100644 --- a/src/pkg/bundle/pull.go +++ b/src/pkg/bundle/pull.go @@ -16,7 +16,6 @@ import ( zarfConfig "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/oci" - "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/pkg/zoci" "github.com/mholt/archiver/v4" @@ -29,7 +28,7 @@ func (b *Bundle) Pull() error { // use uds-cache/packages as the dst dir for the pull to get auto caching // we use an ORAS ocistore to make that dir look like an OCI artifact cacheDir := filepath.Join(zarfConfig.GetAbsCachePath(), "packages") - if err := utils.CreateDirectory(cacheDir, 0o755); err != nil { + if err := helpers.CreateDirectory(cacheDir, 0o755); err != nil { return err } diff --git a/src/pkg/bundle/remote.go b/src/pkg/bundle/remote.go index b3977923..545ddd95 100644 --- a/src/pkg/bundle/remote.go +++ b/src/pkg/bundle/remote.go @@ -8,6 +8,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -21,6 +22,7 @@ import ( "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/oci" zarfUtils "github.com/defenseunicorns/zarf/src/pkg/utils" + "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/pkg/zoci" goyaml "github.com/goccy/go-yaml" "github.com/mholt/archiver/v4" @@ -55,7 +57,7 @@ func (op *ociProvider) getBundleManifest() (*oci.Manifest, error) { // LoadBundleMetadata loads a remote bundle's metadata func (op *ociProvider) LoadBundleMetadata() (types.PathMap, error) { ctx := context.TODO() - if err := zarfUtils.CreateDirectory(filepath.Join(op.dst, config.BlobsDir), 0700); err != nil { + if err := helpers.CreateDirectory(filepath.Join(op.dst, config.BlobsDir), 0700); err != nil { return nil, err } @@ -289,7 +291,9 @@ func getOCIValidatedSource(source string) (string, error) { _, err = remote.ResolveRoot(ctx) } if err != nil { - message.Fatalf(nil, "%s: not found", originalSource) + errMsg := fmt.Sprintf("%s: not found", originalSource) + message.Debug(errMsg) + return "", errors.New(errMsg) } } } diff --git a/src/pkg/bundle/tarball.go b/src/pkg/bundle/tarball.go index 52f1116b..5fbbca57 100644 --- a/src/pkg/bundle/tarball.go +++ b/src/pkg/bundle/tarball.go @@ -18,6 +18,7 @@ import ( "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/oci" zarfUtils "github.com/defenseunicorns/zarf/src/pkg/utils" + "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/pkg/zoci" av3 "github.com/mholt/archiver/v3" av4 "github.com/mholt/archiver/v4" @@ -166,7 +167,7 @@ func (tp *tarballBundleProvider) loadBundleManifest() error { defer os.Remove(manifestPath) - if err := zarfUtils.SHAsMatch(manifestPath, bundleManifestDesc.Digest.Encoded()); err != nil { + if err := helpers.SHAsMatch(manifestPath, bundleManifestDesc.Digest.Encoded()); err != nil { return err } @@ -206,7 +207,7 @@ func (tp *tarballBundleProvider) LoadBundleMetadata() (types.PathMap, error) { pathInTarball := filepath.Join(config.BlobsDir, layer.Digest.Encoded()) abs := filepath.Join(tp.dst, pathInTarball) loaded[path] = abs - if !zarfUtils.InvalidPath(abs) && zarfUtils.SHAsMatch(abs, layer.Digest.Encoded()) == nil { + if !helpers.InvalidPath(abs) && helpers.SHAsMatch(abs, layer.Digest.Encoded()) == nil { continue } if err := av3.Extract(tp.src, pathInTarball, tp.dst); err != nil { @@ -295,9 +296,26 @@ func (tp *tarballBundleProvider) PublishBundle(bundle types.UDSBundle, remote *o return err } - _, err = oras.Copy(tp.ctx, store, ref, remote.Repo(), ref, copyOpts) - if err != nil { - return err + // copy bundle layers to remote with retries + maxRetries := 3 + retries := 0 + + // reset retries if a desc was successful + copyOpts.PostCopy = func(_ context.Context, desc ocispec.Descriptor) error { + retries = 0 + return nil + } + + for { + _, err = oras.Copy(tp.ctx, store, ref, remote.Repo(), ref, copyOpts) + if err != nil && retries < maxRetries { + retries++ + message.Debugf("Encountered err during publish: %s\nRetrying %d/%d", err, retries, maxRetries) + continue + } else if err != nil { + return err + } + break } // create or update, then push index.json diff --git a/src/pkg/bundle/tui/common.go b/src/pkg/bundle/tui/common.go new file mode 100644 index 00000000..3cb54886 --- /dev/null +++ b/src/pkg/bundle/tui/common.go @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023-Present The UDS Authors + +package tui + +import ( + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var ( + // IndentStyle is the style for indenting text + IndentStyle = lipgloss.NewStyle().Padding(0, 4) +) + +func Pause() tea.Cmd { + return tea.Tick(time.Millisecond*500, func(_ time.Time) tea.Msg { + return nil + }) +} diff --git a/src/pkg/bundle/tui/deploy/handlers.go b/src/pkg/bundle/tui/deploy/handlers.go new file mode 100644 index 00000000..5f6ebe76 --- /dev/null +++ b/src/pkg/bundle/tui/deploy/handlers.go @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023-Present The UDS Authors + +package deploy + +import ( + "fmt" + "os" + "strings" + + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/defenseunicorns/uds-cli/src/config" + "github.com/defenseunicorns/uds-cli/src/pkg/bundle/tui" + "github.com/defenseunicorns/uds-cli/src/pkg/utils" + "github.com/defenseunicorns/zarf/src/pkg/message" + zarfTypes "github.com/defenseunicorns/zarf/src/types" +) + +func (m *model) handleNewPackage(pkgName string, currentPkgIdx int) tea.Cmd { + // see if pkg has already been deployed + deployedPkg, _ := c.GetDeployedPackage(pkgName) + newPkg := pkgState{ + name: pkgName, + } + + // upgrade scenario, reset component progress + if deployedPkg != nil { + newPkg.resetProgress = true + } + + // finish creating newPkg and start the spinners + m.pkgIdx = currentPkgIdx + + // create spinner to track deployment progress + deploySpinner := spinner.New() + deploySpinner.Spinner = spinner.Dot + deploySpinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) + newPkg.deploySpinner = deploySpinner + + // for remote packages, create spinner to track verification and download progress + verifySpinner := spinner.New() + verifySpinner.Spinner = spinner.Dot + verifySpinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) + newPkg.verifySpinner = verifySpinner + downloadSpinner := spinner.New() + downloadSpinner.Spinner = spinner.Dot + downloadSpinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) + newPkg.downloadSpinner = downloadSpinner + + m.packages = append(m.packages, newPkg) + return tea.Batch(m.packages[m.pkgIdx].deploySpinner.Tick, + m.packages[m.pkgIdx].verifySpinner.Tick, + m.packages[m.pkgIdx].downloadSpinner.Tick, + ) +} + +func (m *model) handlePreDeploy() tea.Cmd { + cmd := func() tea.Msg { + name, bundleYAML, source, err := m.bndlClient.PreDeployValidation() + if err != nil { + m.errChan <- err + } + m.validatingBundle = false + m.bundleYAML = bundleYAML + m.bundleName = name + // check if the bundle is remote + if strings.HasPrefix(source, "oci://") { + m.isRemoteBundle = true + } + return doDeploy + } + + return cmd +} + +func (m *model) handleDeploy() tea.Cmd { + // ensure bundle deployment is confirmed and is only being deployed once + if m.confirmed && !m.deploying { + // run Deploy concurrently so we can update the TUI while it runs + deployCmd := func() tea.Msg { + // if something goes wrong in Deploy(), reset the terminal + defer utils.GracefulPanic() + + if err := m.bndlClient.Deploy(); err != nil { + m.bndlClient.ClearPaths() + m.errChan <- err + } + return nil + } + m.deploying = true + + // use a ticker to update the TUI during deployment + return tea.Batch(tickCmd(), deployCmd) + } + return nil +} + +func (m *model) handleDone(err error) tea.Cmd { + cmds := []tea.Cmd{tea.Println(), tea.Println(m.udsTitle()), tea.Println()} + m.done = true // remove the current view + cmds = append(cmds, genSuccessCmds(m)...) + if err != nil { + hint := lightBlueText.Render("uds logs") + message.Debug(err) // capture err in debug logs + errMsg := tui.IndentStyle.Render(fmt.Sprintf("\nāŒ Error deploying bundle: %s\n\nRun %s to view deployment logs", lightGrayText.Render(err.Error()), hint) + "\n") + cmds = []tea.Cmd{tea.Println(errMsg), tui.Pause(), tea.Quit} + return tea.Sequence(cmds...) + } + styledBundleName := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFF258")).Render(m.bundleName) + successMsg := tea.Println( + tui.IndentStyle. + Render(fmt.Sprintf("\nāœØ Bundle %s deployed successfully\n", styledBundleName))) + cmds = append(cmds, successMsg, tui.Pause(), tea.Quit) + return tea.Sequence(cmds...) +} + +func (m *model) handleDeployTick() (tea.Model, tea.Cmd) { + // check if all pkgs are complete + numComplete := 0 + if len(m.packages) == m.totalPkgs { + for _, p := range m.packages { + if !p.complete { + break + } + numComplete++ + } + } + + // check if last pkg is complete + if numComplete == m.totalPkgs { + return m, func() tea.Msg { + m.doneChan <- 1 + return nil + } + } + + // update component progress + for i, p := range m.packages { + if p.complete { + continue + } + deployedPkg, _ := c.GetDeployedPackage(p.name) + // if deployedPkg is nil, the package hasn't been deployed yet + if deployedPkg == nil { + break + } + // handle upgrade scenario by resetting the component progress, otherwise increment it + if p.resetProgress { + // if upgraded len(deployedPkg.DeployedComponents) will be equal to the number of components in the package + if deployedPkg != nil && len(deployedPkg.DeployedComponents) > 0 { + m.packages[i].resetProgress = false + } + break + } + // check component progress + for j := range deployedPkg.DeployedComponents { + // check numComponents bc there is a slight delay between rendering the TUI and updating this value + if p.numComponents > 0 && deployedPkg.DeployedComponents[j].Status == zarfTypes.ComponentStatusSucceeded { + m.packages[i].componentStatuses[j] = true + } + } + } + + // always update logViewport content with logs + file, _ := os.ReadFile(config.LogFileName) + m.logViewport.SetContent(string(file)) + m.logViewport.GotoBottom() + + return m, tickCmd() +} diff --git a/src/pkg/bundle/tui/deploy/model.go b/src/pkg/bundle/tui/deploy/model.go new file mode 100644 index 00000000..e9e98d24 --- /dev/null +++ b/src/pkg/bundle/tui/deploy/model.go @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023-Present The UDS Authors + +package deploy + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/defenseunicorns/uds-cli/src/config" + "github.com/defenseunicorns/uds-cli/src/pkg/bundle/tui" + "github.com/defenseunicorns/zarf/src/pkg/cluster" + "golang.org/x/term" +) + +type deployTickMsg time.Time +type deployOp string +type packageOp string + +const ( + doDeploy deployOp = "deploy" + doPreDeploy deployOp = "preDeploy" + newPackage packageOp = "newPackage" + totalComponents packageOp = "totalComponents" + totalPackages packageOp = "totalPackages" + complete packageOp = "complete" + verifying packageOp = "verifying" + downloading packageOp = "downloading" +) + +var ( + Program *tea.Program + c *cluster.Cluster + logVpWidthScale = 0.9 + logVpHeightScale = 0.4 +) + +// private interface to decouple tui pkg from bundle pkg +type bndlClientShim interface { + Deploy() error + PreDeployValidation() (string, string, string, error) + ClearPaths() +} + +// pkgState contains the state of the pkg as its deploying +type pkgState struct { + name string + numComponents int + percLayersVerified int + componentStatuses []bool + deploySpinner spinner.Model + downloadSpinner spinner.Model + verifySpinner spinner.Model + complete bool + resetProgress bool + percDownloaded int + downloaded bool + verified bool + isRemote bool +} + +type model struct { + bndlClient bndlClientShim + bundleYAML string + doneChan chan int + pkgIdx int + totalPkgs int + confirmed bool + done bool + packages []pkgState + deploying bool + inProgress bool + viewLogs bool + logViewport viewport.Model + errChan chan error + yamlViewport viewport.Model + isRemoteBundle bool + bundleName string + validatingBundle bool + validatingBundleSpinner spinner.Model +} + +func InitModel(client bndlClientShim) model { + var confirmed bool + var inProgress bool + var isRemoteBundle bool + if config.CommonOptions.Confirm { + confirmed = true + inProgress = true + } + + // create spinner to track bundle validation + validatingBundleSpinner := spinner.New() + validatingBundleSpinner.Spinner = spinner.Ellipsis + validatingBundleSpinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) + + // create cluster client for querying packages during deployment + c, _ = cluster.NewCluster() + + // set termWidth and line length based on window size + termWidth, termHeight, _ = term.GetSize(0) + + // make log viewport scale dynamic based on termHeight to prevent weird artifacts + if termHeight < 30 { + logVpHeightScale = 0.3 + } else { + logVpHeightScale = 0.4 + } + + // set up logViewport for logs, adjust width and height of logViewport + logViewport := viewport.New(int(float64(termWidth)*logVpWidthScale), int(float64(termHeight)*logVpHeightScale)) + + // set up yamlViewport to ensure the preDeploy YAML is scrollable + numYAMLLines := 10 + yamlViewport := viewport.New(termWidth, numYAMLLines) + + return model{ + bndlClient: client, + doneChan: make(chan int), + errChan: make(chan error), + confirmed: confirmed, + inProgress: inProgress, + logViewport: logViewport, + yamlViewport: yamlViewport, + isRemoteBundle: isRemoteBundle, + validatingBundleSpinner: validatingBundleSpinner, + validatingBundle: true, + } +} + +func (m *model) Init() tea.Cmd { + return func() tea.Msg { + return doPreDeploy + } +} + +func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + select { + case err := <-m.errChan: + cmd := m.handleDone(err) + return m, cmd + case <-m.doneChan: + cmd := m.handleDone(nil) + return m, cmd + + default: + switch msg := msg.(type) { + + // handle changes in window size + case tea.WindowSizeMsg: + termWidth = msg.Width + termHeight = msg.Height + + // make log viewport scale dynamic based on termHeight to prevent weird artifacts + if termHeight < 30 { + logVpHeightScale = 0.3 + } else { + logVpHeightScale = 0.4 + } + m.logViewport.Width = int(float64(termWidth) * logVpWidthScale) + m.logViewport.Height = int(float64(termHeight) * logVpHeightScale) + + // spin the spinners + case spinner.TickMsg: + var spinDeploy, spinVerify, spinDownload, spinValidateBundle tea.Cmd + if len(m.packages) > m.pkgIdx { + m.packages[m.pkgIdx].deploySpinner, spinDeploy = m.packages[m.pkgIdx].deploySpinner.Update(msg) + m.packages[m.pkgIdx].verifySpinner, spinVerify = m.packages[m.pkgIdx].verifySpinner.Update(msg) + m.packages[m.pkgIdx].downloadSpinner, spinDownload = m.packages[m.pkgIdx].downloadSpinner.Update(msg) + } else { + m.validatingBundleSpinner, spinValidateBundle = m.validatingBundleSpinner.Update(msg) + } + return m, tea.Batch(spinDeploy, spinVerify, spinDownload, spinValidateBundle) + + // handle ticks + case deployTickMsg: + return m.handleDeployTick() + + // handle key presses + case tea.KeyMsg: + switch msg.String() { + case "y", "Y": + if !m.confirmed { + m.confirmed = true + m.inProgress = true + } + return m, func() tea.Msg { + return doDeploy + } + + case "n", "N": + if !m.confirmed && !m.inProgress { + m.done = true + quitMsg := tea.Println(tui.IndentStyle.Render("\nšŸ‘‹ Deployment cancelled")) + return m, tea.Sequence(quitMsg, tea.Println(), tea.Quit) + } + case "ctrl+c", "q": + return m, tea.Sequence(tea.Quit) + + case "up": + if !m.confirmed { + m.yamlViewport.LineUp(1) + } + case "down": + if !m.confirmed { + m.yamlViewport.LineDown(1) + } + + case "l", "L": + if m.inProgress && !m.viewLogs { + m.viewLogs = true + } else if m.inProgress { + m.viewLogs = false + } + } + + // handle deploy + case deployOp: + switch msg { + case doDeploy: + cmd := m.handleDeploy() + return m, cmd + case doPreDeploy: + cmd := m.handlePreDeploy() + return m, tea.Sequence(m.validatingBundleSpinner.Tick, cmd) + } + + // handle package updates + case string: + if strings.Contains(msg, ":") { + switch packageOp(strings.Split(msg, ":")[0]) { + case newPackage: + pkgName := strings.Split(msg, ":")[1] + pkgIdx, _ := strconv.Atoi(strings.Split(msg, ":")[2]) + cmd := m.handleNewPackage(pkgName, pkgIdx) + return m, cmd + case totalComponents: + if tc, err := strconv.Atoi(strings.Split(msg, ":")[1]); err == nil { + m.packages[m.pkgIdx].numComponents = tc + m.packages[m.pkgIdx].componentStatuses = make([]bool, tc) + } + case totalPackages: + if totalPkgs, err := strconv.Atoi(strings.Split(msg, ":")[1]); err == nil { + m.totalPkgs = totalPkgs + } + case verifying: + if perc, err := strconv.Atoi(strings.Split(msg, ":")[1]); err == nil { + m.packages[m.pkgIdx].percLayersVerified = perc + if perc == 100 { + m.packages[m.pkgIdx].verified = true + } + } + case downloading: + if perc, err := strconv.Atoi(strings.Split(msg, ":")[1]); err == nil { + m.packages[m.pkgIdx].percDownloaded = perc + if perc == 100 { + m.packages[m.pkgIdx].downloaded = true + } + } + case complete: + m.packages[m.pkgIdx].complete = true + } + } + } + } + + return m, nil +} + +func (m *model) View() string { + if m.done { + // no errors, clear the controlled Program's output + return "" + } else if m.validatingBundle { + validatingBundleMsg := lightGrayText.Render("Validating bundle") + return tui.IndentStyle.Render(fmt.Sprintf("\n%s %s", validatingBundleMsg, m.validatingBundleSpinner.View())) + } else if m.viewLogs { + return fmt.Sprintf("\n%s\n\n%s\n%s\n\n%s\n", m.udsTitle(), m.bundleDeployProgress(), logMsg, m.logView()) + } else if m.confirmed { + return fmt.Sprintf("\n%s\n\n%s\n%s\n%s\n", m.udsTitle(), m.bundleDeployProgress(), logMsg, m.deployView()) + } else { + return fmt.Sprintf("%s\n", m.preDeployView()) + } +} diff --git a/src/pkg/bundle/tui/deploy/views.go b/src/pkg/bundle/tui/deploy/views.go new file mode 100644 index 00000000..bcdc2b3c --- /dev/null +++ b/src/pkg/bundle/tui/deploy/views.go @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023-Present The UDS Authors + +package deploy + +import ( + "fmt" + "strings" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/defenseunicorns/uds-cli/src/pkg/bundle/tui" + "github.com/fatih/color" + "github.com/goccy/go-yaml/lexer" + "github.com/goccy/go-yaml/printer" +) + +const ( + LIGHTBLUE = lipgloss.Color("#4BFDEB") + LIGHTGRAY = lipgloss.Color("#7A7A78") +) + +var ( + termWidth int + termHeight int + styledCheck = lipgloss.NewStyle().Foreground(lipgloss.Color("#00FF00")).Render("āœ”") + lightBlueText = lipgloss.NewStyle().Foreground(LIGHTBLUE) + lightGrayText = lipgloss.NewStyle().Foreground(LIGHTGRAY) + logMsg = tui.IndentStyle.Render(fmt.Sprintf("\n%s %s", + lightBlueText.Render(""), lightGrayText.Render("Toggle logs"))) +) + +var ( + titleStyle = func() lipgloss.Style { + b := lipgloss.RoundedBorder() + b.Right = "ā”œ" + return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) + }() +) + +func (m *model) logView() string { + headerMsg := fmt.Sprintf("%s %s", lightBlueText.Render(m.packages[m.pkgIdx].name), lightGrayText.Render("package logs")) + return tui.IndentStyle.Render( + fmt.Sprintf("%s\n%s\n%s\n\n", m.logHeaderView(headerMsg), m.logViewport.View(), m.logFooterView()), + ) +} + +func (m *model) yamlHeaderView() string { + upArrow := "ā–² " + styledUpArrow := lipgloss.NewStyle().Foreground(LIGHTGRAY).Render(upArrow) + if !m.yamlViewport.AtTop() { + styledUpArrow = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFF258")).Render(upArrow) + } + headerLine := strings.Repeat("ā”€", max(0, m.logViewport.Width-lipgloss.Width(styledUpArrow)-1)) + return lipgloss.JoinHorizontal(lipgloss.Center, styledUpArrow, headerLine) +} + +func (m *model) yamlFooterView() string { + downArrow := "ā–¼ " + styledDownArrow := lipgloss.NewStyle().Foreground(LIGHTGRAY).Render(downArrow) + if !m.yamlViewport.AtBottom() { + styledDownArrow = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFF258")).Render(downArrow) + + } + footerLine := strings.Repeat("ā”€", max(0, m.logViewport.Width-lipgloss.Width(styledDownArrow)-1)) + return lipgloss.JoinHorizontal(lipgloss.Center, styledDownArrow, footerLine) +} + +func (m *model) logHeaderView(msg string) string { + title := titleStyle.Render(msg) + if msg == "" { + title = "" + } + headerLine := strings.Repeat("ā”€", max(0, m.logViewport.Width-lipgloss.Width(title)-1)) + return lipgloss.JoinHorizontal(lipgloss.Center, title, headerLine) +} + +func (m *model) logFooterView() string { + footerLine := strings.Repeat("ā”€", max(0, m.logViewport.Width)-1) + return lipgloss.JoinHorizontal(lipgloss.Center, footerLine) +} + +func (m *model) deployView() string { + view := "" + for _, p := range m.packages { + // count number of successful components + numComponentsSuccess := 0 + if !p.resetProgress { + for _, status := range p.componentStatuses { + if status { + numComponentsSuccess++ + } + } + } + + var text string + if m.isRemoteBundle { + text = genRemotePkgText(p, numComponentsSuccess) + } else { + text = genLocalPkgText(p, numComponentsSuccess) + } + + if p.complete { + text = tui.IndentStyle. + Align(lipgloss.Left). + Render(fmt.Sprintf("%s Package %s deployed", styledCheck, lightBlueText.Render(p.name))) + } + + view = lipgloss.JoinVertical(lipgloss.Left, view, text+"\n") + } + + return view +} + +func genLocalPkgText(p pkgState, numComponentsSuccess int) string { + text := "" + styledName := lightBlueText.Render(p.name) + styledComponents := lightGrayText.Render(fmt.Sprintf("(%d / %d components)", min(numComponentsSuccess+1, p.numComponents), p.numComponents)) + if p.numComponents > 0 { + text = tui.IndentStyle. + Align(lipgloss.Left). + Render(fmt.Sprintf("%s Package %s deploying %s", p.deploySpinner.View(), styledName, styledComponents)) + } else { + text = tui.IndentStyle. + Align(lipgloss.Left). + Render(fmt.Sprintf("%s Package %s deploying", p.deploySpinner.View(), styledName)) + } + return text +} + +func genRemotePkgText(p pkgState, numComponentsSuccess int) string { + text := "" + styledName := lightBlueText.Render(p.name) + styledComponents := lightGrayText.Render(fmt.Sprintf("(%d / %d components)", min(numComponentsSuccess+1, p.numComponents), p.numComponents)) + if !p.verified { + perc := lightGrayText.Render(fmt.Sprintf("(%d%%)", p.percLayersVerified)) + text = tui.IndentStyle. + Align(lipgloss.Left). + Render(fmt.Sprintf("%sVerifying %s package %s", p.verifySpinner.View(), styledName, perc)) + } else if p.verified && !p.downloaded { + perc := lightGrayText.Render(fmt.Sprintf("(%d%%)", p.percDownloaded)) + text = tui.IndentStyle. + Align(lipgloss.Left). + Render(fmt.Sprintf("%sDownloading %s package %s", p.downloadSpinner.View(), styledName, perc)) + } else if p.downloaded && p.verified && p.numComponents > 0 { + text = tui.IndentStyle. + Align(lipgloss.Left). + Render(fmt.Sprintf("%sDeploying %s package %s", p.deploySpinner.View(), styledName, styledComponents)) + } else { + text = tui.IndentStyle. + Align(lipgloss.Left). + Render(fmt.Sprintf("%sDeploying %s package", p.deploySpinner.View(), styledName)) + } + + return text +} + +func (m *model) preDeployView() string { + header := tui.IndentStyle.Render("šŸ“¦ Bundle Definition (ā–² / ā–¼)") + prompt := tui.IndentStyle.Render("ā“ Deploy this bundle? (y/n)") + prettyYAML := tui.IndentStyle.Render(colorPrintYAML(m.bundleYAML)) + m.yamlViewport.SetContent(prettyYAML) + + // Concatenate header, highlighted YAML, and prompt + return fmt.Sprintf("\n%s\n\n%s\n\n%s\n%s\n%s\n\n%s", + m.udsTitle(), + header, + tui.IndentStyle.Render(m.yamlHeaderView()), + tui.IndentStyle.Render(m.yamlViewport.View()), + tui.IndentStyle.Render(m.yamlFooterView()), + prompt, + ) +} + +func tickCmd() tea.Cmd { + return tea.Tick(time.Millisecond*250, func(t time.Time) tea.Msg { + return deployTickMsg(t) + }) +} + +// colorPrintYAML makes a pretty-print YAML string with color +func colorPrintYAML(yaml string) string { + tokens := lexer.Tokenize(yaml) + + var p printer.Printer + p.Bool = func() *printer.Property { + return &printer.Property{ + Prefix: yamlFormat(color.FgHiWhite), + Suffix: yamlFormat(color.Reset), + } + } + p.Number = func() *printer.Property { + return &printer.Property{ + Prefix: yamlFormat(color.FgHiWhite), + Suffix: yamlFormat(color.Reset), + } + } + p.MapKey = func() *printer.Property { + return &printer.Property{ + Prefix: yamlFormat(color.FgHiCyan), + Suffix: yamlFormat(color.Reset), + } + } + p.Anchor = func() *printer.Property { + return &printer.Property{ + Prefix: yamlFormat(color.FgHiYellow), + Suffix: yamlFormat(color.Reset), + } + } + p.Alias = func() *printer.Property { + return &printer.Property{ + Prefix: yamlFormat(color.FgHiYellow), + Suffix: yamlFormat(color.Reset), + } + } + p.String = func() *printer.Property { + return &printer.Property{ + Prefix: yamlFormat(color.FgHiMagenta), + Suffix: yamlFormat(color.Reset), + } + } + + outputYAML := p.PrintTokens(tokens) + return outputYAML +} + +func yamlFormat(attr color.Attribute) string { + const yamlEscape = "\x1b" + return fmt.Sprintf("%s[%dm", yamlEscape, attr) +} + +// udsTitle returns the title header for the UDS bundle +func (m *model) udsTitle() string { + styledBundleName := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFF258")).Render(m.bundleName + " ") + title := " UDS Bundle: " + styledTitle := lipgloss.NewStyle().Margin(0, 3). + Padding(1, 0). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#6233f2")). + Render(fmt.Sprintf("%s%s", title, styledBundleName)) + return styledTitle +} + +// genSuccessCmds generates the success or failure messages for each package +func genSuccessCmds(m *model) []tea.Cmd { + var cmds []tea.Cmd + for i := 0; i < len(m.packages); i++ { + successMsg := fmt.Sprintf("%s Package %s deployed\n", styledCheck, lightBlueText.Render(m.packages[i].name)) + cmds = append(cmds, tea.Println(tui.IndentStyle.Render(successMsg))) + } + return cmds +} + +func (m *model) bundleDeployProgress() string { + styledText := lightGrayText.Render("šŸ“¦ Deploying bundle package") + styledPkgCounter := lightGrayText.Render(fmt.Sprintf("(%d / %d)", m.pkgIdx+1, m.totalPkgs)) + msg := fmt.Sprintf("%s %s", styledText, styledPkgCounter) + return tui.IndentStyle.Render(msg) +} diff --git a/src/pkg/bundler/fetcher/remote.go b/src/pkg/bundler/fetcher/remote.go index 56fffcaf..c9ad92c4 100644 --- a/src/pkg/bundler/fetcher/remote.go +++ b/src/pkg/bundler/fetcher/remote.go @@ -19,12 +19,14 @@ import ( "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/oci" zarfUtils "github.com/defenseunicorns/zarf/src/pkg/utils" + "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/pkg/zoci" zarfTypes "github.com/defenseunicorns/zarf/src/types" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" ) +// remoteFetcher fetches remote Zarf pkgs for local bundles type remoteFetcher struct { pkg types.Package cfg Config @@ -99,7 +101,7 @@ func (f *remoteFetcher) Fetch() ([]ocispec.Descriptor, error) { func (f *remoteFetcher) layersToLocalBundle(spinner *message.Spinner, currentPackageIter int, totalPackages int) ([]ocispec.Descriptor, error) { spinner.Updatef("Fetching %s package layer metadata (package %d of %d)", f.pkg.Name, currentPackageIter, totalPackages) // get only the layers that are required by the components - layersToCopy, err := utils.GetZarfLayers(*f.remote, f.pkg, f.pkgRootManifest) + layersToCopy, err := utils.GetZarfLayers(*f.remote, f.pkgRootManifest) if err != nil { return nil, err } @@ -150,7 +152,7 @@ func (f *remoteFetcher) remoteToLocal(layersToCopy []ocispec.Descriptor) ([]ocis // Grab tmpDirSize and add it to the estimatedBytes, otherwise the progress bar will be off // because as multiple packages are pulled into the tmpDir, RenderProgressBarForLocalDirWrite continues to // add their size which results in strange MB ratios - tmpDirSize, err := zarfUtils.GetDirSize(f.cfg.TmpDstDir) + tmpDirSize, err := helpers.GetDirSize(f.cfg.TmpDstDir) if err != nil { return nil, err } diff --git a/src/pkg/bundler/pusher/remote.go b/src/pkg/bundler/pusher/remote.go index 1012157b..fa846f26 100644 --- a/src/pkg/bundler/pusher/remote.go +++ b/src/pkg/bundler/pusher/remote.go @@ -88,7 +88,7 @@ func (p *RemotePusher) PushManifest() (ocispec.Descriptor, error) { func (p *RemotePusher) LayersToRemoteBundle(spinner *message.Spinner, currentPackageIter int, totalPackages int) ([]ocispec.Descriptor, error) { spinner.Updatef("Fetching %s package layer metadata (package %d of %d)", p.pkg.Name, currentPackageIter, totalPackages) // get only the layers that are required by the components - layersToCopy, err := utils.GetZarfLayers(p.cfg.RemoteSrc, p.pkg, p.cfg.PkgRootManifest) + layersToCopy, err := utils.GetZarfLayers(p.cfg.RemoteSrc, p.cfg.PkgRootManifest) if err != nil { return nil, err } diff --git a/src/pkg/sources/remote.go b/src/pkg/sources/remote.go index 5efdc76d..727903b7 100644 --- a/src/pkg/sources/remote.go +++ b/src/pkg/sources/remote.go @@ -12,11 +12,13 @@ import ( "strings" "github.com/defenseunicorns/uds-cli/src/config" + "github.com/defenseunicorns/uds-cli/src/pkg/bundle/tui/deploy" "github.com/defenseunicorns/uds-cli/src/pkg/cache" "github.com/defenseunicorns/uds-cli/src/pkg/utils" "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/oci" + "github.com/defenseunicorns/zarf/src/pkg/packager/filters" "github.com/defenseunicorns/zarf/src/pkg/packager/sources" zarfUtils "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/pkg/zoci" @@ -38,22 +40,32 @@ type RemoteBundle struct { } // LoadPackage loads a Zarf package from a remote bundle -func (r *RemoteBundle) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) error { +func (r *RemoteBundle) LoadPackage(dst *layout.PackagePaths, filter filters.ComponentFilterStrategy, unarchiveAll bool) (zarfTypes.ZarfPackage, []string, error) { + // todo: progress bar?? layers, err := r.downloadPkgFromRemoteBundle() if err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } var pkg zarfTypes.ZarfPackage if err = zarfUtils.ReadYaml(dst.ZarfYAML, &pkg); err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } + pkg.Components, err = filter.Apply(pkg) + if err != nil { + return pkg, nil, err + } + + // record number of components to be deployed for TUI + // todo: won't work for optional components...... + deploy.Program.Send(fmt.Sprintf("totalComponents:%d", len(pkg.Components))) + dst.SetFromLayers(layers) err = sources.ValidatePackageIntegrity(dst, pkg.Metadata.AggregateChecksum, r.isPartial) if err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } if unarchiveAll { @@ -62,39 +74,39 @@ func (r *RemoteBundle) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) if layout.IsNotLoaded(err) { _, err := dst.Components.Create(component) if err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } } else { - return err + return zarfTypes.ZarfPackage{}, nil, err } } } if dst.SBOMs.Path != "" { if err := dst.SBOMs.Unarchive(); err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } } } - return nil + return pkg, nil, err } // LoadPackageMetadata loads a Zarf package's metadata from a remote bundle -func (r *RemoteBundle) LoadPackageMetadata(dst *layout.PackagePaths, _ bool, _ bool) (err error) { +func (r *RemoteBundle) LoadPackageMetadata(dst *layout.PackagePaths, _ bool, _ bool) (zarfTypes.ZarfPackage, []string, error) { ctx := context.TODO() root, err := r.Remote.FetchRoot(ctx) if err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } pkgManifestDesc := root.Locate(r.PkgManifestSHA) if oci.IsEmptyDescriptor(pkgManifestDesc) { - return fmt.Errorf("zarf package %s with manifest sha %s not found", r.PkgName, r.PkgManifestSHA) + return zarfTypes.ZarfPackage{}, nil, fmt.Errorf("zarf package %s with manifest sha %s not found", r.PkgName, r.PkgManifestSHA) } // look at Zarf pkg manifest, grab zarf.yaml desc and download it pkgManifest, err := r.Remote.FetchManifest(ctx, pkgManifestDesc) if err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } var zarfYAMLDesc ocispec.Descriptor @@ -104,17 +116,17 @@ func (r *RemoteBundle) LoadPackageMetadata(dst *layout.PackagePaths, _ bool, _ b break } } - zarfYAMLBytes, err := r.Remote.FetchLayer(ctx, zarfYAMLDesc) + pkgBytes, err := r.Remote.FetchLayer(ctx, zarfYAMLDesc) if err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } - var zarfYAML zarfTypes.ZarfPackage - if err = goyaml.Unmarshal(zarfYAMLBytes, &zarfYAML); err != nil { - return err + var pkg zarfTypes.ZarfPackage + if err = goyaml.Unmarshal(pkgBytes, &pkg); err != nil { + return zarfTypes.ZarfPackage{}, nil, err } - err = zarfUtils.WriteYaml(filepath.Join(dst.Base, config.ZarfYAML), zarfYAML, 0600) + err = zarfUtils.WriteYaml(filepath.Join(dst.Base, config.ZarfYAML), pkg, 0600) if err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } // grab checksums.txt so we can validate pkg integrity @@ -123,11 +135,11 @@ func (r *RemoteBundle) LoadPackageMetadata(dst *layout.PackagePaths, _ bool, _ b if layer.Annotations[ocispec.AnnotationTitle] == config.ChecksumsTxt { checksumBytes, err := r.Remote.FetchLayer(ctx, layer) if err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } err = os.WriteFile(filepath.Join(dst.Base, config.ChecksumsTxt), checksumBytes, 0600) if err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } checksumLayer = layer break @@ -136,8 +148,8 @@ func (r *RemoteBundle) LoadPackageMetadata(dst *layout.PackagePaths, _ bool, _ b dst.SetFromLayers([]ocispec.Descriptor{pkgManifestDesc, checksumLayer}) - err = sources.ValidatePackageIntegrity(dst, zarfYAML.Metadata.AggregateChecksum, true) - return err + err = sources.ValidatePackageIntegrity(dst, pkg.Metadata.AggregateChecksum, true) + return pkg, nil, err } // Collect doesn't need to be implemented @@ -170,6 +182,8 @@ func (r *RemoteBundle) downloadPkgFromRemoteBundle() ([]ocispec.Descriptor, erro estimatedBytes := int64(0) layersToPull := []ocispec.Descriptor{pkgManifestDesc} layersInBundle := []ocispec.Descriptor{pkgManifestDesc} + numLayersVerified := 0.0 + downloadedBytes := int64(0) for _, layer := range pkgManifest.Layers { ok, err := r.Remote.Repo().Blobs().Exists(ctx, layer) @@ -177,7 +191,10 @@ func (r *RemoteBundle) downloadPkgFromRemoteBundle() ([]ocispec.Descriptor, erro return nil, err } progressBar.Add(1) + numLayersVerified++ if ok { + percVerified := numLayersVerified / float64(len(pkgManifest.Layers)) * 100 + deploy.Program.Send(fmt.Sprintf("verifying:%v", int64(percVerified))) estimatedBytes += layer.Size layersInBundle = append(layersInBundle, layer) digest := layer.Digest.Encoded() @@ -205,6 +222,14 @@ func (r *RemoteBundle) downloadPkgFromRemoteBundle() ([]ocispec.Descriptor, erro copyOpts := utils.CreateCopyOpts(layersToPull, config.CommonOptions.OCIConcurrency) doneSaving := make(chan error) go zarfUtils.RenderProgressBarForLocalDirWrite(r.TmpDir, estimatedBytes, doneSaving, fmt.Sprintf("Pulling bundled Zarf pkg: %s", r.PkgName), fmt.Sprintf("Successfully pulled package: %s", r.PkgName)) + + copyOpts.PostCopy = func(_ context.Context, desc ocispec.Descriptor) error { + downloadedBytes += desc.Size + downloadedPerc := float64(downloadedBytes) / float64(estimatedBytes) * 100 + deploy.Program.Send(fmt.Sprintf("downloading:%d", int64(downloadedPerc))) + return nil + } + _, err = oras.Copy(ctx, r.Remote.Repo(), r.Remote.Repo().Reference.String(), store, "", copyOpts) doneSaving <- err <-doneSaving diff --git a/src/pkg/sources/tarball.go b/src/pkg/sources/tarball.go index 93afb573..6a7ad0ef 100644 --- a/src/pkg/sources/tarball.go +++ b/src/pkg/sources/tarball.go @@ -12,17 +12,20 @@ import ( "path/filepath" "strings" - "github.com/defenseunicorns/uds-cli/src/config" - "github.com/defenseunicorns/uds-cli/src/pkg/utils" + "github.com/defenseunicorns/uds-cli/src/pkg/bundle/tui/deploy" "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/oci" + "github.com/defenseunicorns/zarf/src/pkg/packager/filters" "github.com/defenseunicorns/zarf/src/pkg/packager/sources" zarfUtils "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" zarfTypes "github.com/defenseunicorns/zarf/src/types" av4 "github.com/mholt/archiver/v4" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/defenseunicorns/uds-cli/src/config" + "github.com/defenseunicorns/uds-cli/src/pkg/utils" ) // TarballBundle is a package source for local tarball bundles that implements Zarf's packager.PackageSource @@ -36,23 +39,33 @@ type TarballBundle struct { } // LoadPackage loads a Zarf package from a local tarball bundle -func (t *TarballBundle) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) error { +func (t *TarballBundle) LoadPackage(dst *layout.PackagePaths, filter filters.ComponentFilterStrategy, unarchiveAll bool) (zarfTypes.ZarfPackage, []string, error) { packageSpinner := message.NewProgressSpinner("Loading bundled Zarf package: %s", t.PkgName) defer packageSpinner.Stop() files, err := t.extractPkgFromBundle() if err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } var pkg zarfTypes.ZarfPackage if err = zarfUtils.ReadYaml(dst.ZarfYAML, &pkg); err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err + } + + pkg.Components, err = filter.Apply(pkg) + if err != nil { + return pkg, nil, err } + dst.SetFromPaths(files) + // record number of components to be deployed for TUI + // todo: won't work for optional components...... + deploy.Program.Send(fmt.Sprintf("totalComponents:%d", len(pkg.Components))) + if err := sources.ValidatePackageIntegrity(dst, pkg.Metadata.AggregateChecksum, t.isPartial); err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } if unarchiveAll { @@ -61,26 +74,26 @@ func (t *TarballBundle) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) if layout.IsNotLoaded(err) { _, err := dst.Components.Create(component) if err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } } else { - return err + return zarfTypes.ZarfPackage{}, nil, err } } } if dst.SBOMs.Path != "" { if err := dst.SBOMs.Unarchive(); err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } } } packageSpinner.Successf("Loaded bundled Zarf package: %s", t.PkgName) - return nil + return pkg, nil, err } // LoadPackageMetadata loads a Zarf package's metadata from a local tarball bundle -func (t *TarballBundle) LoadPackageMetadata(dst *layout.PackagePaths, _ bool, _ bool) (err error) { +func (t *TarballBundle) LoadPackageMetadata(dst *layout.PackagePaths, _ bool, _ bool) (zarfTypes.ZarfPackage, []string, error) { ctx := context.TODO() format := av4.CompressedArchive{ Compression: av4.Zstd{}, @@ -89,12 +102,12 @@ func (t *TarballBundle) LoadPackageMetadata(dst *layout.PackagePaths, _ bool, _ sourceArchive, err := os.Open(t.BundleLocation) if err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } var imageManifest oci.Manifest if err := format.Extract(ctx, sourceArchive, []string{filepath.Join(config.BlobsDir, t.PkgManifestSHA)}, utils.ExtractJSON(&imageManifest)); err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } var zarfYamlSHA string @@ -106,7 +119,7 @@ func (t *TarballBundle) LoadPackageMetadata(dst *layout.PackagePaths, _ bool, _ } if zarfYamlSHA == "" { - return fmt.Errorf(fmt.Sprintf("zarf.yaml with SHA %s not found", zarfYamlSHA)) + return zarfTypes.ZarfPackage{}, nil, fmt.Errorf(fmt.Sprintf("zarf.yaml with SHA %s not found", zarfYamlSHA)) } // grab SHA of checksums.txt @@ -121,7 +134,7 @@ func (t *TarballBundle) LoadPackageMetadata(dst *layout.PackagePaths, _ bool, _ // reset file reader _, err = sourceArchive.Seek(0, 0) if err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } // grab zarf.yaml and checksums.txt @@ -151,25 +164,25 @@ func (t *TarballBundle) LoadPackageMetadata(dst *layout.PackagePaths, _ bool, _ }); err != nil { err = sourceArchive.Close() if err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } - return err + return zarfTypes.ZarfPackage{}, nil, err } // deserialize zarf.yaml to grab checksum for validating pkg integrity - var zarfYAML zarfTypes.ZarfPackage - err = zarfUtils.ReadYaml(dst.ZarfYAML, &zarfYAML) + var pkg zarfTypes.ZarfPackage + err = zarfUtils.ReadYaml(dst.ZarfYAML, &pkg) if err != nil { - return err + return zarfTypes.ZarfPackage{}, nil, err } dst.SetFromPaths(filePaths) - if err := sources.ValidatePackageIntegrity(dst, zarfYAML.Metadata.AggregateChecksum, true); err != nil { - return err + if err := sources.ValidatePackageIntegrity(dst, pkg.Metadata.AggregateChecksum, true); err != nil { + return zarfTypes.ZarfPackage{}, nil, err } err = sourceArchive.Close() - return err + return pkg, nil, err } // Collect doesn't need to be implemented @@ -223,7 +236,7 @@ func (t *TarballBundle) extractPkgFromBundle() ([]string, error) { } size := desc.Size layerDst := filepath.Join(t.TmpDir, cleanPath) - if err := zarfUtils.CreateDirectory(filepath.Dir(layerDst), 0700); err != nil { + if err := helpers.CreateDirectory(filepath.Dir(layerDst), 0700); err != nil { return err } @@ -245,7 +258,7 @@ func (t *TarballBundle) extractPkgFromBundle() ([]string, error) { return nil } - layersToExtract := []string{} + var layersToExtract []string for _, layer := range manifest.Layers { layersToExtract = append(layersToExtract, filepath.Join(config.BlobsDir, layer.Digest.Encoded())) diff --git a/src/pkg/utils/oci.go b/src/pkg/utils/oci.go index b8fff8bd..0a2b2118 100644 --- a/src/pkg/utils/oci.go +++ b/src/pkg/utils/oci.go @@ -18,6 +18,7 @@ import ( "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/oci" "github.com/defenseunicorns/zarf/src/pkg/zoci" + zarfTypes "github.com/defenseunicorns/zarf/src/types" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" @@ -260,9 +261,21 @@ func EnsureOCIPrefix(source string) string { } // GetZarfLayers grabs the necessary Zarf pkg layers from a remote OCI registry -func GetZarfLayers(remote zoci.Remote, pkg types.Package, pkgRootManifest *oci.Manifest) ([]ocispec.Descriptor, error) { +func GetZarfLayers(remote zoci.Remote, pkgRootManifest *oci.Manifest) ([]ocispec.Descriptor, error) { + // todo: ensure we are only pulling non-optional components ctx := context.TODO() - layersFromComponents, err := remote.LayersFromRequestedComponents(ctx, pkg.OptionalComponents) + var components []zarfTypes.ZarfComponent + for _, layer := range pkgRootManifest.Layers { + // infer component name from layer title annotation + titleAnnotation := layer.Annotations[ocispec.AnnotationTitle] + isComponent := strings.HasPrefix(titleAnnotation, "components/") && strings.HasSuffix(titleAnnotation, ".tar") + if isComponent { + afterComponents := strings.Split(titleAnnotation, "components/")[1] + componentName := strings.Split(afterComponents, ".tar")[0] + components = append(components, zarfTypes.ZarfComponent{Name: componentName}) + } + } + layersFromComponents, err := remote.LayersFromRequestedComponents(ctx, components) if err != nil { return nil, err } diff --git a/src/pkg/utils/utils.go b/src/pkg/utils/utils.go index 344a65c3..2f941205 100644 --- a/src/pkg/utils/utils.go +++ b/src/pkg/utils/utils.go @@ -10,35 +10,35 @@ import ( "fmt" "io" "os" + "os/exec" "path/filepath" "regexp" "strings" - "time" "github.com/defenseunicorns/uds-cli/src/config" "github.com/defenseunicorns/uds-cli/src/types" "github.com/defenseunicorns/zarf/src/pkg/message" - "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" av4 "github.com/mholt/archiver/v4" "github.com/pterm/pterm" ) -// MergeVariables merges the variables from the config file and the CLI -// -// TODO: move this to helpers.MergeAndTransformMap -func MergeVariables(left map[string]string, right map[string]string) map[string]string { - // Ensure uppercase keys from viper - leftUpper := helpers.TransformMapKeys(left, strings.ToUpper) - rightUpper := helpers.TransformMapKeys(right, strings.ToUpper) - - // Merge the viper config file variables and provided CLI flag variables (CLI takes precedence)) - return helpers.MergeMap(leftUpper, rightUpper) +// GracefulPanic in the event of a panic, attempt to reset the terminal using the 'reset' command. +func GracefulPanic() { + if r := recover(); r != nil { + fmt.Println("Recovering from panic to reset terminal before exiting") + // todo: this approach is heavy-handed, consider alternatives using the term lib (check out what BubbleTea does) + cmd := exec.Command("reset") + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + _ = cmd.Run() + panic(r) + } } // IsValidTarballPath returns true if the path is a valid tarball path to a bundle tarball func IsValidTarballPath(path string) bool { - if utils.InvalidPath(path) || utils.IsDir(path) { + if helpers.InvalidPath(path) || helpers.IsDir(path) { return false } name := filepath.Base(path) @@ -52,33 +52,51 @@ func IsValidTarballPath(path string) bool { return re.MatchString(name) } -// UseLogFile writes output to stderr and a logFile. -func UseLogFile() { - // LogWriter is the stream to write logs to. - var LogWriter io.Writer - - // Write logs to stderr and a buffer for logFile generation. - var logFile *os.File - - // Prepend the log filename with a timestamp. - ts := time.Now().Format("2006-01-02-15-04-05") - - var err error - if logFile != nil { - // Use the existing log file if logFile is set - LogWriter = io.MultiWriter(os.Stderr, logFile) - pterm.SetDefaultOutput(LogWriter) - } else { - // Try to create a temp log file if one hasn't been made already - if logFile, err = os.CreateTemp("", fmt.Sprintf("uds-%s-*.log", ts)); err != nil { - message.WarnErr(err, "Error saving a log file to a temporary directory") - } else { - LogWriter = io.MultiWriter(os.Stderr, logFile) - pterm.SetDefaultOutput(LogWriter) - msg := fmt.Sprintf("Saving log file to %s", logFile.Name()) - message.Note(msg) - } +// ConfigureLogs sets up the log file, log cache and output for the CLI +func ConfigureLogs(op string) error { + // don't configure UDS logs for vendored cmds + if strings.HasPrefix(op, "zarf") || strings.HasPrefix(op, "run") { + return nil + } + writer, err := message.UseLogFile("") + logFile := writer + if err != nil { + return err + } + tmpLogLocation := message.LogFileLocation() + config.LogFileName = tmpLogLocation + + // Set up cache dir and cache logs file + cacheDir := filepath.Join(config.CommonOptions.CachePath) + if err := os.MkdirAll(cacheDir, 0o0755); err != nil { // Ensure the directory exists + return fmt.Errorf("failed to create cache directory: %w", err) + } + + // remove old cache logs file, and set up symlink to the new log file + os.Remove(filepath.Join(config.CommonOptions.CachePath, config.CachedLogs)) + if err = os.Symlink(tmpLogLocation, filepath.Join(config.CommonOptions.CachePath, config.CachedLogs)); err != nil { + return err + } + + logWriter := io.MultiWriter(logFile) + + // use Zarf pterm output if no-tea flag is set + // todo: as more bundle ops use BubbleTea, need to also check them alongside 'deploy' + if !strings.Contains(op, "deploy") || config.CommonOptions.NoTea { + message.Notef("Saving log file to %s", tmpLogLocation) + logWriter = io.MultiWriter(os.Stderr, logFile) + pterm.SetDefaultOutput(logWriter) + return nil + } + + pterm.SetDefaultOutput(logWriter) + + // disable progress bars (otherwise they will still get printed to STDERR) + message.NoProgress = true + + message.Debugf(fmt.Sprintf("Saving log file to %s", tmpLogLocation)) + return nil } // ExtractJSON extracts and unmarshals a tarballed JSON file into a type diff --git a/src/test/bundles/04-init/uds-bundle.yaml b/src/test/bundles/04-init/uds-bundle.yaml index 7ecf2a1e..d6944dce 100644 --- a/src/test/bundles/04-init/uds-bundle.yaml +++ b/src/test/bundles/04-init/uds-bundle.yaml @@ -10,12 +10,12 @@ packages: - name: init path: "../../packages" # renovate: datasource=github-tags depName=defenseunicorns/zarf - ref: v0.32.5 + ref: v0.32.6 optionalComponents: - git-server - name: init repository: ghcr.io/defenseunicorns/packages/init # renovate: datasource=github-tags depName=defenseunicorns/zarf - ref: v0.32.5 + ref: v0.32.6 optionalComponents: - git-server diff --git a/src/test/e2e/bundle_test.go b/src/test/e2e/bundle_test.go index 3d5e4d1b..aa6bf7c4 100644 --- a/src/test/e2e/bundle_test.go +++ b/src/test/e2e/bundle_test.go @@ -31,6 +31,14 @@ func TestUDSCmd(t *testing.T) { require.NoError(t, err) } +func TestUDSLogs(t *testing.T) { + inspectRemote(t, "ghcr.io/defenseunicorns/packages/uds-cli/test/publish/ghcr-test:0.0.1") + stderr, _, err := e2e.UDS("logs") + require.NoError(t, err) + require.Contains(t, stderr, "DEBUG") + require.Contains(t, stderr, "UDSBundle") +} + func TestSimpleBundleWithZarfAction(t *testing.T) { zarfPkgPath := "src/test/packages/no-cluster/real-simple" e2e.CreateZarfPkg(t, zarfPkgPath, false) @@ -180,12 +188,11 @@ func TestPackagesFlag(t *testing.T) { t.Run("Test invalid package deploy", func(t *testing.T) { _, stderr := deployPackagesFlag(bundlePath, "podinfo,nginx,peanuts") require.Contains(t, stderr, "invalid zarf packages specified by --packages") - }) + t.Run("Test invalid package remove", func(t *testing.T) { _, stderr := removePackagesFlag(bundlePath, "podinfo,nginx,peanuts") require.Contains(t, stderr, "invalid zarf packages specified by --packages") - }) } diff --git a/src/test/e2e/commands_test.go b/src/test/e2e/commands_test.go index 3bc6aab5..72adfa44 100644 --- a/src/test/e2e/commands_test.go +++ b/src/test/e2e/commands_test.go @@ -16,7 +16,7 @@ import ( "testing" "github.com/defenseunicorns/uds-cli/src/config" - "github.com/defenseunicorns/zarf/src/pkg/utils" + "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -99,7 +99,7 @@ func inspectLocalAndSBOMExtract(t *testing.T, tarballPath string) { } func deploy(t *testing.T, tarballPath string) (stdout string, stderr string) { - cmd := strings.Split(fmt.Sprintf("deploy %s --confirm", tarballPath), " ") + cmd := strings.Split(fmt.Sprintf("deploy %s --confirm --no-tea", tarballPath), " ") stdout, stderr, err := e2e.UDS(cmd...) require.NoError(t, err) return stdout, stderr @@ -113,13 +113,13 @@ func runCmd(t *testing.T, input string) (stdout string, stderr string) { } func deployPackagesFlag(tarballPath string, packages string) (stdout string, stderr string) { - cmd := strings.Split(fmt.Sprintf("deploy %s --confirm -l=debug --packages %s", tarballPath, packages), " ") + cmd := strings.Split(fmt.Sprintf("deploy %s --confirm -l=debug --packages %s --no-tea", tarballPath, packages), " ") stdout, stderr, _ = e2e.UDS(cmd...) return stdout, stderr } func deployResumeFlag(t *testing.T, tarballPath string) { - cmd := strings.Split(fmt.Sprintf("deploy %s --confirm -l=debug --resume", tarballPath), " ") + cmd := strings.Split(fmt.Sprintf("deploy %s --confirm -l=debug --resume --no-tea", tarballPath), " ") _, _, err := e2e.UDS(cmd...) require.NoError(t, err) } @@ -137,7 +137,7 @@ func removePackagesFlag(tarballPath string, packages string) (stdout string, std } func deployAndRemoveRemoteInsecure(t *testing.T, ref string) { - cmd := strings.Split(fmt.Sprintf("deploy %s --insecure --oci-concurrency=10 --confirm", ref), " ") + cmd := strings.Split(fmt.Sprintf("deploy %s --insecure --confirm --no-tea", ref), " ") _, _, err := e2e.UDS(cmd...) require.NoError(t, err) } @@ -148,7 +148,7 @@ func deployAndRemoveLocalAndRemoteInsecure(t *testing.T, ref string, tarballPath t.Run( "deploy+remove bundle via OCI", func(t *testing.T) { - cmd = strings.Split(fmt.Sprintf("deploy %s --insecure --oci-concurrency=10 --confirm", ref), " ") + cmd = strings.Split(fmt.Sprintf("deploy %s --insecure --confirm --no-tea", ref), " ") _, _, err := e2e.UDS(cmd...) require.NoError(t, err) @@ -161,7 +161,7 @@ func deployAndRemoveLocalAndRemoteInsecure(t *testing.T, ref string, tarballPath t.Run( "deploy+remove bundle via local tarball", func(t *testing.T) { - cmd = strings.Split(fmt.Sprintf("deploy %s --confirm", tarballPath), " ") + cmd = strings.Split(fmt.Sprintf("deploy %s --confirm --no-tea", tarballPath), " ") _, _, err := e2e.UDS(cmd...) require.NoError(t, err) @@ -173,7 +173,7 @@ func deployAndRemoveLocalAndRemoteInsecure(t *testing.T, ref string, tarballPath } func shasMatch(t *testing.T, path string, expected string) { - actual, err := utils.GetSHA256OfFile(path) + actual, err := helpers.GetSHA256OfFile(path) require.NoError(t, err) require.Equal(t, expected, actual) } @@ -230,7 +230,7 @@ func publish(t *testing.T, bundlePath, ociPath string) { } func publishInsecure(t *testing.T, bundlePath, ociPath string) { - cmd := strings.Split(fmt.Sprintf("publish %s %s --insecure --oci-concurrency=10", bundlePath, ociPath), " ") + cmd := strings.Split(fmt.Sprintf("publish %s %s --insecure", bundlePath, ociPath), " ") _, _, err := e2e.UDS(cmd...) require.NoError(t, err) } diff --git a/src/test/e2e/main_test.go b/src/test/e2e/main_test.go index 2d6cfd77..b20e908e 100644 --- a/src/test/e2e/main_test.go +++ b/src/test/e2e/main_test.go @@ -116,7 +116,7 @@ func deployZarfInit(t *testing.T) { require.NoError(t, err) // Deploy - cmd = strings.Split(fmt.Sprintf("deploy %s --confirm -l=debug", bundlePath), " ") + cmd = strings.Split(fmt.Sprintf("deploy %s --confirm -l=debug --no-tea", bundlePath), " ") _, _, err = e2e.UDS(cmd...) require.NoError(t, err) } diff --git a/src/test/e2e/variable_test.go b/src/test/e2e/variable_test.go index 49d4d13b..6a82aa8d 100644 --- a/src/test/e2e/variable_test.go +++ b/src/test/e2e/variable_test.go @@ -52,19 +52,19 @@ func bundleVariablesTestChecks(t *testing.T, stderr string, bundleTarballPath st require.Contains(t, stderr, "shared var in output-var pkg: burning.boats") require.Contains(t, stderr, "shared var in receive-var pkg: burning.boats") - _, stderr = runCmd(t, "deploy "+bundleTarballPath+" --set ANIMAL=Longhorns --set COUNTRY=Texas --confirm -l=debug") + _, stderr = runCmd(t, "deploy "+bundleTarballPath+" --set ANIMAL=Longhorns --set COUNTRY=Texas --confirm -l=debug --no-tea") require.Contains(t, stderr, "This fun-fact was imported: Longhorns are the national animal of Texas") require.NotContains(t, stderr, "This fun-fact was imported: Unicorns are the national animal of Scotland") - _, stderr = runCmd(t, "deploy "+bundleTarballPath+" --set output-var.SPECIFIC_PKG_VAR=output-var-set --confirm -l=debug") + _, stderr = runCmd(t, "deploy "+bundleTarballPath+" --set output-var.SPECIFIC_PKG_VAR=output-var-set --confirm -l=debug --no-tea") require.Contains(t, stderr, "output-var SPECIFIC_PKG_VAR = output-var-set") require.Contains(t, stderr, "receive-var SPECIFIC_PKG_VAR = not-set") - _, stderr = runCmd(t, "deploy "+bundleTarballPath+" --set output-var.specific_pkg_var=output --set receive-var.SPECIFIC_PKG_VAR=receive --confirm -l=debug") + _, stderr = runCmd(t, "deploy "+bundleTarballPath+" --set output-var.specific_pkg_var=output --set receive-var.SPECIFIC_PKG_VAR=receive --confirm -l=debug --no-tea") require.Contains(t, stderr, "output-var SPECIFIC_PKG_VAR = output") require.Contains(t, stderr, "receive-var SPECIFIC_PKG_VAR = receive") - _, stderr = runCmd(t, "deploy "+bundleTarballPath+" --set SPECIFIC_PKG_VAR=errbody --confirm -l=debug") + _, stderr = runCmd(t, "deploy "+bundleTarballPath+" --set SPECIFIC_PKG_VAR=errbody --confirm -l=debug --no-tea") require.Contains(t, stderr, "output-var SPECIFIC_PKG_VAR = errbody") require.Contains(t, stderr, "receive-var SPECIFIC_PKG_VAR = errbody") } diff --git a/src/test/packages/gitrepo/zarf.yaml b/src/test/packages/gitrepo/zarf.yaml index 90b39b3b..8fb7aad1 100644 --- a/src/test/packages/gitrepo/zarf.yaml +++ b/src/test/packages/gitrepo/zarf.yaml @@ -8,3 +8,21 @@ components: required: true repos: - https://github.com/defenseunicorns/uds-package-dubbd + - name: nginx-remote + required: true + manifests: + - name: simple-nginx-deployment + namespace: nginx + files: + - https://k8s.io/examples/application/deployment.yaml@c57f73449b26eae02ca2a549c388807d49ef6d3f2dc040a9bbb1290128d97157 + actions: + onDeploy: + after: + - wait: + cluster: + kind: deployment + name: nginx-deployment + namespace: nginx + condition: available + images: + - docker.io/library/nginx:1.14.2 diff --git a/src/types/bundler.go b/src/types/options.go similarity index 89% rename from src/types/bundler.go rename to src/types/options.go index f1fb53e0..e8c6d256 100644 --- a/src/types/bundler.go +++ b/src/types/options.go @@ -31,8 +31,9 @@ type BundleDeployOptions struct { PublicKeyPath string SetVariables map[string]string `json:"setVariables" jsonschema:"description=Key-Value map of variable names and their corresponding values that will be used by Zarf packages in a bundle"` // Variables and SharedVariables are read in from uds-config.yaml - Variables map[string]map[string]interface{} `yaml:"variables,omitempty"` - SharedVariables map[string]interface{} `yaml:"shared,omitempty"` + Variables map[string]map[string]interface{} `yaml:"variables,omitempty"` + SharedVariables map[string]interface{} `yaml:"shared,omitempty"` + ZarfPackageNameMap map[string]string `yaml:"-" json:"-"` } // BundleInspectOptions is the options for the bundler.Inspect() function @@ -69,6 +70,7 @@ type BundleCommonOptions struct { CachePath string `json:"cachePath" jsonschema:"description=Path to use to cache images and git repos on package create"` TempDirectory string `json:"tempDirectory" jsonschema:"description=Location Zarf should use as a staging ground when managing files and images for package creation and deployment"` OCIConcurrency int `jsonschema:"description=Number of concurrent layer operations to perform when interacting with a remote package"` + NoTea bool `json:"useTea" jsonschema:"description=Don't use BubbleTea TUI"` } // PathMap is a map of either absolute paths to relative paths or relative paths to absolute paths diff --git a/tasks.schema.json b/tasks.schema.json index 56367f3b..7ff89d56 100644 --- a/tasks.schema.json +++ b/tasks.schema.json @@ -33,7 +33,7 @@ }, "shell": { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/ZarfComponentActionShell", + "$ref": "#/definitions/Shell", "description": "(cmd only) Indicates a preference for a shell for the provided cmd to be executed in on supported operating systems" }, "setVariable": { @@ -106,6 +106,49 @@ "^x-": {} } }, + "Shell": { + "properties": { + "windows": { + "type": "string", + "description": "(default 'powershell') Indicates a preference for the shell to use on Windows systems (note that choosing 'cmd' will turn off migrations like touch -> New-Item)", + "examples": [ + "powershell", + "cmd", + "pwsh", + "sh", + "bash", + "gsh" + ] + }, + "linux": { + "type": "string", + "description": "(default 'sh') Indicates a preference for the shell to use on Linux systems", + "examples": [ + "sh", + "bash", + "fish", + "zsh", + "pwsh" + ] + }, + "darwin": { + "type": "string", + "description": "(default 'sh') Indicates a preference for the shell to use on macOS systems", + "examples": [ + "sh", + "bash", + "fish", + "zsh", + "pwsh" + ] + } + }, + "additionalProperties": false, + "type": "object", + "patternProperties": { + "^x-": {} + } + }, "Task": { "required": [ "name" @@ -206,18 +249,6 @@ "type": "string", "description": "The name to be used for the variable" }, - "sensitive": { - "type": "boolean", - "description": "Whether to mark this variable as sensitive to not print it in the Zarf log" - }, - "autoIndent": { - "type": "boolean", - "description": "Whether to automatically indent the variable's value (if multiline) when templating. Based on the number of chars before the start of ###ZARF_VAR_." - }, - "pattern": { - "type": "string", - "description": "An optional regex pattern that a variable value must match before a package deployment can continue." - }, "type": { "enum": [ "raw", @@ -225,49 +256,18 @@ ], "type": "string", "description": "Changes the handling of a variable to load contents differently (i.e. from a file rather than as a raw variable - templated files should be kept below 1 MiB)" - } - }, - "additionalProperties": false, - "type": "object", - "patternProperties": { - "^x-": {} - } - }, - "ZarfComponentActionShell": { - "properties": { - "windows": { - "type": "string", - "description": "(default 'powershell') Indicates a preference for the shell to use on Windows systems (note that choosing 'cmd' will turn off migrations like touch -> New-Item)", - "examples": [ - "powershell", - "cmd", - "pwsh", - "sh", - "bash", - "gsh" - ] }, - "linux": { + "pattern": { "type": "string", - "description": "(default 'sh') Indicates a preference for the shell to use on Linux systems", - "examples": [ - "sh", - "bash", - "fish", - "zsh", - "pwsh" - ] + "description": "An optional regex pattern that a variable value must match before a package deployment can continue." }, - "darwin": { - "type": "string", - "description": "(default 'sh') Indicates a preference for the shell to use on macOS systems", - "examples": [ - "sh", - "bash", - "fish", - "zsh", - "pwsh" - ] + "sensitive": { + "type": "boolean", + "description": "Whether to mark this variable as sensitive to not print it in the Zarf log" + }, + "autoIndent": { + "type": "boolean", + "description": "Whether to automatically indent the variable's value (if multiline) when templating. Based on the number of chars before the start of ###ZARF_VAR_." } }, "additionalProperties": false, diff --git a/zarf.schema.json b/zarf.schema.json index f8e53e98..f61482b7 100644 --- a/zarf.schema.json +++ b/zarf.schema.json @@ -82,6 +82,49 @@ "^x-": {} } }, + "Shell": { + "properties": { + "windows": { + "type": "string", + "description": "(default 'powershell') Indicates a preference for the shell to use on Windows systems (note that choosing 'cmd' will turn off migrations like touch -> New-Item)", + "examples": [ + "powershell", + "cmd", + "pwsh", + "sh", + "bash", + "gsh" + ] + }, + "linux": { + "type": "string", + "description": "(default 'sh') Indicates a preference for the shell to use on Linux systems", + "examples": [ + "sh", + "bash", + "fish", + "zsh", + "pwsh" + ] + }, + "darwin": { + "type": "string", + "description": "(default 'sh') Indicates a preference for the shell to use on macOS systems", + "examples": [ + "sh", + "bash", + "fish", + "zsh", + "pwsh" + ] + } + }, + "additionalProperties": false, + "type": "object", + "patternProperties": { + "^x-": {} + } + }, "ZarfBuildData": { "required": [ "terminal", @@ -118,10 +161,6 @@ "type": "array", "description": "Any migrations that have been run on this package" }, - "differential": { - "type": "boolean", - "description": "Whether this package was created with differential components" - }, "registryOverrides": { "patternProperties": { ".*": { @@ -131,6 +170,14 @@ "type": "object", "description": "Any registry domains that were overridden on package create when pulling images" }, + "differential": { + "type": "boolean", + "description": "Whether this package was created with differential components" + }, + "differentialPackageVersion": { + "type": "string", + "description": "Version of a previously built package used as the basis for creating this differential package" + }, "differentialMissing": { "items": { "type": "string" @@ -257,18 +304,13 @@ "$ref": "#/definitions/ZarfComponentImport", "description": "Import a component from another Zarf package" }, - "scripts": { - "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/DeprecatedZarfComponentScripts", - "description": "[Deprecated] (replaced by actions) Custom commands to run before or after package deployment. This will be removed in Zarf v1.0.0." - }, - "files": { + "manifests": { "items": { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/ZarfFile" + "$ref": "#/definitions/ZarfManifest" }, "type": "array", - "description": "Files or folders to place on disk during package deployment" + "description": "Kubernetes manifests to be included in a generated Helm chart on package deploy" }, "charts": { "items": { @@ -278,13 +320,21 @@ "type": "array", "description": "Helm charts to install during package deploy" }, - "manifests": { + "dataInjections": { "items": { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/ZarfManifest" + "$ref": "#/definitions/ZarfDataInjection" }, "type": "array", - "description": "Kubernetes manifests to be included in a generated Helm chart on package deploy" + "description": "Datasets to inject into a container in the target cluster" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/ZarfFile" + }, + "type": "array", + "description": "Files or folders to place on disk during package deployment" }, "images": { "items": { @@ -300,19 +350,16 @@ "type": "array", "description": "List of git repos to include in the package" }, - "dataInjections": { - "items": { - "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/ZarfDataInjection" - }, - "type": "array", - "description": "Datasets to inject into a container in the target cluster" - }, "extensions": { "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/ZarfComponentExtensions", "description": "Extend component functionality with additional features" }, + "scripts": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/DeprecatedZarfComponentScripts", + "description": "[Deprecated] (replaced by actions) Custom commands to run before or after package deployment. This will be removed in Zarf v1.0.0." + }, "actions": { "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/ZarfComponentActions", @@ -355,7 +402,7 @@ "description": "The command to run. Must specify either cmd or wait for the action to do anything." }, "shell": { - "$ref": "#/definitions/ZarfComponentActionShell", + "$ref": "#/definitions/Shell", "description": "(cmd only) Indicates a preference for a shell for the provided cmd to be executed in on supported operating systems" }, "setVariable": { @@ -414,7 +461,7 @@ }, "shell": { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/ZarfComponentActionShell", + "$ref": "#/definitions/Shell", "description": "(cmd only) Indicates a preference for a shell for the provided cmd to be executed in on supported operating systems" } }, @@ -477,18 +524,6 @@ "type": "string", "description": "The name to be used for the variable" }, - "sensitive": { - "type": "boolean", - "description": "Whether to mark this variable as sensitive to not print it in the Zarf log" - }, - "autoIndent": { - "type": "boolean", - "description": "Whether to automatically indent the variable's value (if multiline) when templating. Based on the number of chars before the start of ###ZARF_VAR_." - }, - "pattern": { - "type": "string", - "description": "An optional regex pattern that a variable value must match before a package deployment can continue." - }, "type": { "enum": [ "raw", @@ -496,49 +531,18 @@ ], "type": "string", "description": "Changes the handling of a variable to load contents differently (i.e. from a file rather than as a raw variable - templated files should be kept below 1 MiB)" - } - }, - "additionalProperties": false, - "type": "object", - "patternProperties": { - "^x-": {} - } - }, - "ZarfComponentActionShell": { - "properties": { - "windows": { - "type": "string", - "description": "(default 'powershell') Indicates a preference for the shell to use on Windows systems (note that choosing 'cmd' will turn off migrations like touch -> New-Item)", - "examples": [ - "powershell", - "cmd", - "pwsh", - "sh", - "bash", - "gsh" - ] }, - "linux": { + "pattern": { "type": "string", - "description": "(default 'sh') Indicates a preference for the shell to use on Linux systems", - "examples": [ - "sh", - "bash", - "fish", - "zsh", - "pwsh" - ] + "description": "An optional regex pattern that a variable value must match before a package deployment can continue." }, - "darwin": { - "type": "string", - "description": "(default 'sh') Indicates a preference for the shell to use on macOS systems", - "examples": [ - "sh", - "bash", - "fish", - "zsh", - "pwsh" - ] + "sensitive": { + "type": "boolean", + "description": "Whether to mark this variable as sensitive to not print it in the Zarf log" + }, + "autoIndent": { + "type": "boolean", + "description": "Whether to automatically indent the variable's value (if multiline) when templating. Based on the number of chars before the start of ###ZARF_VAR_." } }, "additionalProperties": false,