diff --git a/go.mod b/go.mod index 16c51a0d4..e0bb07f63 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,20 @@ module github.com/defenseunicorns/uds-cli go 1.21.6 +// todo: remove once Zarf + Maru have been updated +replace github.com/defenseunicorns/zarf v0.32.5 => github.com/defenseunicorns/zarf v0.32.6-0.20240318172435-c7edb53ddf28 + +replace github.com/defenseunicorns/maru-runner v0.0.2 => github.com/defenseunicorns/maru-runner v0.0.3-0.20240318172058-b7493c2ffa00 + require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b + 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.0.2 github.com/defenseunicorns/zarf v0.32.5 + 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 +28,7 @@ require ( golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 golang.org/x/mod v0.16.0 golang.org/x/sync v0.6.0 + golang.org/x/term v0.17.0 helm.sh/helm/v3 v3.14.3 oras.land/oras-go/v2 v2.4.0 ) @@ -27,12 +37,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 +148,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 @@ -191,7 +198,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 +256,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 +451,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 +464,19 @@ require ( go4.org v0.0.0-20200411211856-f5505b9728dd // indirect golang.org/x/crypto v0.19.0 // indirect golang.org/x/net v0.21.0 // indirect - golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/oauth2 v0.17.0 // indirect golang.org/x/sys v0.17.0 // indirect - golang.org/x/term v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.18.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 0dfdcc1fa..63914fa34 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.0.3-0.20240318172058-b7493c2ffa00 h1:hGweEhcsSKNTfHF/UKOAkikd5y7tw/rtykH9YuQ8gZA= +github.com/defenseunicorns/maru-runner v0.0.3-0.20240318172058-b7493c2ffa00/go.mod h1:D4aOUUpBwxE3gXEfQysfpzR388MmXJLq1R71jwnE8RY= +github.com/defenseunicorns/zarf v0.32.6-0.20240318172435-c7edb53ddf28 h1:OU7i2/5gvdJCiPmgey3Mi00vWir7ITXEsgFUPEIbA9s= +github.com/defenseunicorns/zarf v0.32.6-0.20240318172435-c7edb53ddf28/go.mod h1:OdLoiJaWsX0u/6l/IZO1VBSVcJw7d+ZwDUI4pjc5aAc= 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= @@ -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= @@ -1926,8 +1926,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= @@ -2207,8 +2207,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= @@ -2323,12 +2323,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= @@ -2365,8 +2365,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= @@ -2383,8 +2383,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 0f9809293..5ccd2e293 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -39,7 +39,11 @@ var rootCmd = &cobra.Command{ if cmd.Parent() == nil { config.SkipLogFile = true } - cliSetup() + + // don't load log configs for the logs command + if cmd.Use != "logs" { + cliSetup() + } }, 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,6 +89,7 @@ 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() { @@ -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() + if err != nil { + message.Fatalf(err, "Error configuring logs") + } } } diff --git a/src/cmd/uds.go b/src/cmd/uds.go index ce266a7ec..172fe0065 100644 --- a/src/cmd/uds.go +++ b/src/cmd/uds.go @@ -5,20 +5,26 @@ package cmd import ( + "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" + "github.com/defenseunicorns/uds-cli/src/pkg/utils" zarfConfig "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/pkg/message" zarfUtils "github.com/defenseunicorns/zarf/src/pkg/utils" 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{ @@ -70,6 +76,13 @@ var deployCmd = &cobra.Command{ Short: lang.CmdBundleDeployShort, Args: cobra.MaximumNArgs(1), Run: func(_ *cobra.Command, args []string) { + // reconfigure logs for the deploy command so we can use BubbleTea + config.TeaEnabled = true + err := utils.ConfigureLogs() + if err != nil { + message.Fatalf(err, "Error configuring logs") + } + bundleCfg.DeployOpts.Source = chooseBundle(args) configureZarf() @@ -80,12 +93,47 @@ 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()) + // pre-deploy validation + bundleYAML := "" + bundleYAML, err = bndlClient.PreDeployValidation() + if err != nil { + return + } + + // don't use bubbletea if --no-tea flag is set + if config.CommonOptions.NoTea { + // 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()) + } + return + } + + // start up bubbletea + m := deploy.InitModel(bndlClient, bundleYAML) + + // detect tty so CI/containers don't break + if term.IsTerminal(int(os.Stdout.Fd())) { + deploy.Program = tea.NewProgram(&m, tea.WithMouseCellMotion()) + } 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()) } }, } @@ -175,6 +223,31 @@ var pullCmd = &cobra.Command{ }, } +var logsCmd = &cobra.Command{ + Use: "logs", + Aliases: []string{"l"}, + Short: "Display log file contents", // Replace with your actual short description + Run: func(cmd *cobra.Command, args []string) { + logFilePath := filepath.Join(config.CommonOptions.CachePath, config.CachedLogs) + + // Open the log file + file, err := os.Open(logFilePath) + if err != nil { + // Handle the error if the file can't be opened + fmt.Fprintf(os.Stderr, "Error opening log file: %v\n", err) + return + } + defer file.Close() // Ensure the file is closed when the function returns + + // Copy the contents of the log file to standard output + if _, err := io.Copy(os.Stdout, file); err != nil { + // Handle the error if the contents can't be read or written to stdout + fmt.Fprintf(os.Stderr, "Error reading or printing log file: %v\n", err) + return + } + }, +} + // loadViperConfig reads the config file and unmarshals the relevant config into DeployOpts.Variables func loadViperConfig() error { // get config file from Viper @@ -246,6 +319,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/viper.go b/src/cmd/viper.go index 07ad89c98..d242a78c0 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 c66582180..ad2f563ab 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,14 +78,14 @@ 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 + + // TeaEnabled is a flag to enable BubbleTea for select bundle ops as we refactor the TUI + TeaEnabled bool ) // GetArch returns the arch based on a priority list with options for overriding. diff --git a/src/config/lang/lang.go b/src/config/lang/lang.go index 84a0ba3f5..ef06a452c 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 54c342846..7226f5655 100644 --- a/src/pkg/bundle/common.go +++ b/src/pkg/bundle/common.go @@ -15,12 +15,13 @@ import ( "github.com/defenseunicorns/uds-cli/src/config" "github.com/defenseunicorns/uds-cli/src/pkg/bundler/fetcher" + "github.com/defenseunicorns/uds-cli/src/pkg/utils" "github.com/defenseunicorns/uds-cli/src/types" "github.com/defenseunicorns/zarf/src/config/lang" "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 +52,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,8 +73,9 @@ 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() { + utils.CacheLogFile.Close() _ = os.RemoveAll(b.tmp) } @@ -244,7 +246,7 @@ func (b *Bundle) CalculateBuildInfo() error { // ValidateBundleSignature validates the bundle signature func ValidateBundleSignature(bundleYAMLPath, signaturePath, publicKeyPath string) error { - if utils.InvalidPath(bundleYAMLPath) { + if zarfUtils.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 +254,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 zarfUtils.InvalidPath(signaturePath) && !zarfUtils.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 !zarfUtils.InvalidPath(signaturePath) && zarfUtils.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 96a8f4fec..2385d8c1f 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,59 @@ func (b *Bundle) loadChartOverrides(pkg types.Package, pkgVars map[string]string return processed, nil } +func (b *Bundle) PreDeployValidation() (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 + return string(bundleYAML), 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/tui/common.go b/src/pkg/bundle/tui/common.go new file mode 100644 index 000000000..7168bd510 --- /dev/null +++ b/src/pkg/bundle/tui/common.go @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023-Present The UDS Authors + +package tui + +import ( + "time" + + tea "github.com/charmbracelet/bubbletea" +) + +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 000000000..8b76644f9 --- /dev/null +++ b/src/pkg/bundle/tui/deploy/handlers.go @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023-Present The UDS Authors + +package deploy + +import ( + "fmt" + "os" + + "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" + 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 spinner + m.pkgIdx = currentPkgIdx + s := spinner.New() + s.Spinner = spinner.Dot + s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) + newPkg.spinner = s + m.packages = append(m.packages, newPkg) + return m.packages[m.pkgIdx].spinner.Tick +} + +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 { + var cmds []tea.Cmd + m.done = true // remove the current view + cmds = append(cmds, genSuccessOrFailCmds(m)...) + if err != nil { + errMsg := lipgloss.NewStyle().Padding(0, 3).Render(fmt.Sprintf("\nāŒ %s\n", err.Error())) + cmds = []tea.Cmd{tea.Println(errMsg)} + } + cmds = append(cmds, 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 progress bar, 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) == 1 { + 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)) + if !m.isScrolling { + m.logViewport.GotoBottom() + } + + return m, tickCmd() +} + +// genSuccessOrFailCmds generates the success or failure messages for each package +func genSuccessOrFailCmds(m *Model) []tea.Cmd { + cmds := []tea.Cmd{tea.Println(fmt.Sprintf("%s\n", logMsg))} + for i := 0; i < len(m.packages); i++ { + if m.packages[i].complete { + successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#32A852")).Padding(0, 3) + successMsg := fmt.Sprintf("āœ… Package %s deployed\n", m.packages[i].name) + cmds = append(cmds, tea.Println(successStyle.Render(successMsg))) + } else { + failStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000")).Padding(0, 3) + failMsg := fmt.Sprintf("āŒ Package %s failed to deploy\n", m.packages[i].name) + cmds = append(cmds, tea.Println(failStyle.Render(failMsg))) + } + } + return cmds +} diff --git a/src/pkg/bundle/tui/deploy/model.go b/src/pkg/bundle/tui/deploy/model.go new file mode 100644 index 000000000..d43850baa --- /dev/null +++ b/src/pkg/bundle/tui/deploy/model.go @@ -0,0 +1,228 @@ +// 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/zarf/src/pkg/cluster" + "golang.org/x/term" +) + +type deployTickMsg time.Time +type deployOp string +type packageOp string + +const ( + doDeploy deployOp = "deploy" + newPackage packageOp = "newPackage" + totalComponents packageOp = "totalComponents" + totalPackages packageOp = "totalPackages" + complete packageOp = "complete" +) + +var ( + Program *tea.Program + c *cluster.Cluster + logVpWidthScale = 0.9 + logVpHeightScale = 0.4 + lineWidthScale = 0.75 +) + +// private interface to decouple tui pkg from bundle pkg +type bndlClientShim interface { + Deploy() error + ClearPaths() +} + +// pkgState contains the state of the pkg as its deploying +type pkgState struct { + name string + numComponents int + componentStatuses []bool + spinner spinner.Model + complete bool + resetProgress 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 + isScrolling bool + errChan chan error + yamlViewport viewport.Model +} + +func InitModel(client bndlClientShim, bundleYAML string) Model { + var confirmed bool + var inProgress bool + if config.CommonOptions.Confirm { + confirmed = true + inProgress = true + } + + // 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) + line = lipgloss.NewStyle().Padding(0, 3).Render(strings.Repeat("ā”€", int(float64(termWidth)*lineWidthScale))) + + // set up logViewport for logs, adjust width and height of logViewport + logViewport := viewport.New(int(float64(termWidth)*logVpWidthScale), int(float64(termHeight)*logVpHeightScale)) + logViewport.MouseWheelEnabled = true + logViewport.MouseWheelDelta = 1 + + // set up yamlViewport to ensure the preDeploy YAML is scrollable + numYamlLines := 10 + yamlViewport := viewport.New(termWidth, numYamlLines) + yamlViewport.MouseWheelEnabled = true + yamlViewport.MouseWheelDelta = 1 + + return Model{ + bndlClient: client, + doneChan: make(chan int), + errChan: make(chan error), + confirmed: confirmed, + bundleYAML: bundleYAML, + inProgress: inProgress, + logViewport: logViewport, + yamlViewport: yamlViewport, + } +} + +func (m *Model) Init() tea.Cmd { + return func() tea.Msg { + return doDeploy + } +} + +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 + line = lipgloss.NewStyle().Padding(0, 3).Render(strings.Repeat("ā”€", int(float64(termWidth)*lineWidthScale))) + m.logViewport.Width = int(float64(termWidth) * logVpWidthScale) + m.logViewport.Height = int(float64(termHeight) * logVpHeightScale) + + // handle mouse events + case tea.MouseMsg: + m.isScrolling = true + m.logViewport, _ = m.logViewport.Update(msg) + m.yamlViewport, _ = m.yamlViewport.Update(msg) + + // handle spinner + case spinner.TickMsg: + var cmd tea.Cmd + m.packages[m.pkgIdx].spinner, cmd = m.packages[m.pkgIdx].spinner.Update(msg) + return m, cmd + + // 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("\nšŸ‘‹ Deployment cancelled") + return m, tea.Sequence(quitMsg, tea.Println(), tea.Quit) + } + case "ctrl+c", "q": + return m, tea.Quit + + case "l", "L": + if m.inProgress && !m.viewLogs { + m.viewLogs = true + m.isScrolling = false + } else if m.inProgress { + m.viewLogs = false + } + } + + // handle deploy + case deployOp: + cmd := m.handleDeploy() + return m, 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 totalComponents, err := strconv.Atoi(strings.Split(msg, ":")[1]); err == nil { + m.packages[m.pkgIdx].numComponents = totalComponents + m.packages[m.pkgIdx].componentStatuses = make([]bool, totalComponents) + } + case totalPackages: + if totalPkgs, err := strconv.Atoi(strings.Split(msg, ":")[1]); err == nil { + m.totalPkgs = totalPkgs + } + 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.viewLogs { + return fmt.Sprintf("%s\n\n%s\n", logMsg, m.logView()) + } else if m.confirmed { + return fmt.Sprintf("%s\n%s\n", 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 000000000..895c138a8 --- /dev/null +++ b/src/pkg/bundle/tui/deploy/views.go @@ -0,0 +1,155 @@ +// 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/fatih/color" + "github.com/goccy/go-yaml/lexer" + "github.com/goccy/go-yaml/printer" +) + +const ( + LIGHTBLUE = lipgloss.Color("#4BFDEB") + LIGHTGRAY = lipgloss.Color("#7A7A78") +) + +var ( + line string + termWidth int + termHeight int + lightBlueText = lipgloss.NewStyle().Foreground(LIGHTBLUE) + lightGrayText = lipgloss.NewStyle().Foreground(LIGHTGRAY) + logMsg = lipgloss.NewStyle().Padding(0, 3).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 { + return lipgloss.NewStyle().Padding(0, 3).Render( + fmt.Sprintf("%s\n%s\n%s\n\n", m.logHeaderView(), m.logViewport.View(), m.logFooterView()), + ) +} + +func (m *Model) logHeaderView() string { + title := titleStyle.Render(fmt.Sprintf("Package %s deploy logs", m.packages[m.pkgIdx].name)) + headerLine := strings.Repeat("ā”€", max(0, m.logViewport.Width-lipgloss.Width(title))) + return lipgloss.JoinHorizontal(lipgloss.Center, title, headerLine) +} + +func (m *Model) logFooterView() string { + footerLine := strings.Repeat("ā”€", max(0, m.logViewport.Width)) + 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++ + } + } + } + + text := lipgloss.NewStyle(). + Align(lipgloss.Left). + Padding(0, 3). + Render(fmt.Sprintf("%s Package %s deploying (%d / %d components)", p.spinner.View(), p.name, min(numComponentsSuccess+1, p.numComponents), p.numComponents)) + + if p.complete { + text = lipgloss.NewStyle(). + Align(lipgloss.Left). + Padding(0, 3). + Render(fmt.Sprintf("āœ… Package %s deployed", p.name)) + } + + view = lipgloss.JoinVertical(lipgloss.Left, view, text+"\n") + } + + return view +} + +func (m *Model) preDeployView() string { + paddingStyle := lipgloss.NewStyle().Padding(0, 3) + header := paddingStyle.Render("šŸŽ BUNDLE DEFINITION") + prompt := paddingStyle.Render("ā“ Deploy this bundle? (y/n)") + prettyYAML := paddingStyle.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\n%s", header, line, m.yamlViewport.View(), 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) +} diff --git a/src/pkg/sources/remote.go b/src/pkg/sources/remote.go index 5efdc76d9..3d14c173f 100644 --- a/src/pkg/sources/remote.go +++ b/src/pkg/sources/remote.go @@ -12,6 +12,7 @@ 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" @@ -49,6 +50,10 @@ func (r *RemoteBundle) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) return 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) diff --git a/src/pkg/sources/tarball.go b/src/pkg/sources/tarball.go index 93afb5734..0d0e1bdb4 100644 --- a/src/pkg/sources/tarball.go +++ b/src/pkg/sources/tarball.go @@ -12,8 +12,7 @@ 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" @@ -23,6 +22,9 @@ import ( 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 @@ -51,6 +53,10 @@ func (t *TarballBundle) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) } 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 } diff --git a/src/pkg/utils/utils.go b/src/pkg/utils/utils.go index 344a65c37..a7b85de1c 100644 --- a/src/pkg/utils/utils.go +++ b/src/pkg/utils/utils.go @@ -10,30 +10,34 @@ 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) +var ( + CacheLogFile *os.File +) - // 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 @@ -52,33 +56,47 @@ 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() error { + writer, err := message.UseLogFile("") + logFile := writer + if err != nil { + return err + + } + location := message.LogFileLocation() + config.LogFileName = location + + // empty cache logs file + os.Remove(filepath.Join(config.CommonOptions.CachePath, config.CachedLogs)) + + // 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) } + CacheLogFile, err = os.OpenFile(filepath.Join(config.CommonOptions.CachePath, config.CachedLogs), os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + logWriter := io.MultiWriter(logFile, CacheLogFile) + + // use Zarf pterm output if no-tea flag is set + if !config.TeaEnabled || config.CommonOptions.NoTea { + message.Notef("Saving log file to %s", location) + logWriter = io.MultiWriter(os.Stderr, CacheLogFile, logFile) + pterm.SetDefaultOutput(logWriter) + return nil + } + + // set pterm output to only go to this logfile + 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", location)) + return nil } // ExtractJSON extracts and unmarshals a tarballed JSON file into a type diff --git a/src/test/e2e/bundle_test.go b/src/test/e2e/bundle_test.go index 3d5e4d1ba..997bee961 100644 --- a/src/test/e2e/bundle_test.go +++ b/src/test/e2e/bundle_test.go @@ -180,12 +180,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 3bc6aab53..3bbeba3f0 100644 --- a/src/test/e2e/commands_test.go +++ b/src/test/e2e/commands_test.go @@ -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) @@ -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/packages/gitrepo/zarf.yaml b/src/test/packages/gitrepo/zarf.yaml index 90b39b3b6..7fb5964bf 100644 --- a/src/test/packages/gitrepo/zarf.yaml +++ b/src/test/packages/gitrepo/zarf.yaml @@ -8,3 +8,48 @@ components: required: true repos: - https://github.com/defenseunicorns/uds-package-dubbd + - name: nginx-remote + required: true + manifests: + - name: simple-nginx-deployment + namespace: nginx + files: + # remote manifests are specified with a URL and you can verify integrity of a manifest + # by adding a sha256sum to the end of the URL, separated by an @: + - https://k8s.io/examples/application/deployment.yaml@c57f73449b26eae02ca2a549c388807d49ef6d3f2dc040a9bbb1290128d97157 + # this sha256 can be discovered using: + # zarf prepare sha256sum https://k8s.io/examples/application/deployment.yaml + actions: + onDeploy: + # the following checks were computed by viewing the success state of the package deployment + # and creating `wait` actions that match + after: + - wait: + cluster: + kind: deployment + name: nginx-deployment + namespace: nginx + condition: available + # image discovery is supported in all manifests and charts using: + # zarf prepare find-images + images: + - docker.io/library/nginx:1.14.2 + - name: podinfo + required: true + charts: + - name: podinfo + version: 6.4.0 + namespace: podinfo + url: https://github.com/stefanprodan/podinfo.git + gitPath: charts/podinfo + images: + - ghcr.io/stefanprodan/podinfo:6.4.0 + actions: + onDeploy: + after: + - wait: + cluster: + kind: deployment + name: podinfo + namespace: podinfo + condition: available 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 f1fb53e0e..eabe41763 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=Use BubbleTea TUI"` } // PathMap is a map of either absolute paths to relative paths or relative paths to absolute paths