diff --git a/src/cmd/root.go b/src/cmd/root.go index 5ccd2e293..2183d90d0 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -42,7 +42,7 @@ var rootCmd = &cobra.Command{ // don't load log configs for the logs command if cmd.Use != "logs" { - cliSetup() + cliSetup(cmd.Use) } }, Short: lang.RootCmdShort, @@ -92,7 +92,7 @@ func init() { 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, @@ -113,7 +113,7 @@ func cliSetup() { } if !config.SkipLogFile && !config.ListTasks { - err := utils.ConfigureLogs() + 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 c94afcf85..83bb90659 100644 --- a/src/cmd/uds.go +++ b/src/cmd/uds.go @@ -18,7 +18,6 @@ import ( "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" @@ -77,13 +76,6 @@ 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() @@ -100,7 +92,7 @@ var deployCmd = &cobra.Command{ // pre-deploy validation bundleYAML := "" - bundleYAML, err = bndlClient.PreDeployValidation() + bundleYAML, err := bndlClient.PreDeployValidation() if err != nil { return } diff --git a/src/cmd/version.go b/src/cmd/version.go index 495dab607..926c2a3de 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/pkg/bundle/tarball.go b/src/pkg/bundle/tarball.go index 52f1116b2..eb69a8495 100644 --- a/src/pkg/bundle/tarball.go +++ b/src/pkg/bundle/tarball.go @@ -295,9 +295,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/deploy/model.go b/src/pkg/bundle/tui/deploy/model.go index be3f4dffc..87a2ac054 100644 --- a/src/pkg/bundle/tui/deploy/model.go +++ b/src/pkg/bundle/tui/deploy/model.go @@ -28,7 +28,8 @@ const ( totalComponents packageOp = "totalComponents" totalPackages packageOp = "totalPackages" complete packageOp = "complete" - verified packageOp = "verified" + verifying packageOp = "verifying" + downloading packageOp = "downloading" ) var ( @@ -49,12 +50,16 @@ type bndlClientShim interface { type pkgState struct { name string numComponents int - percLayersVerified float64 + percLayersVerified int64 componentStatuses []bool deploySpinner spinner.Model complete bool resetProgress bool verifySpinner spinner.Model + percDownloaded int64 + downloaded bool + verified bool + isRemote bool } type Model struct { @@ -225,9 +230,19 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if totalPkgs, err := strconv.Atoi(strings.Split(msg, ":")[1]); err == nil { m.totalPkgs = totalPkgs } - case verified: - if perc, err := strconv.ParseFloat(strings.Split(msg, ":")[1], 64); err == nil { + case verifying: + if perc, err := strconv.ParseInt(strings.Split(msg, ":")[1], 10, 8); err == nil { m.packages[m.pkgIdx].percLayersVerified = perc + if perc == 100 { + m.packages[m.pkgIdx].verified = true + } + } + case downloading: + if perc, err := strconv.ParseInt(strings.Split(msg, ":")[1], 10, 8); 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 diff --git a/src/pkg/bundle/tui/deploy/views.go b/src/pkg/bundle/tui/deploy/views.go index 65003c7f6..10de21404 100644 --- a/src/pkg/bundle/tui/deploy/views.go +++ b/src/pkg/bundle/tui/deploy/views.go @@ -39,7 +39,7 @@ var ( ) func (m *Model) logView() string { - headerMsg := fmt.Sprintf("Package %s deploy logs", m.packages[m.pkgIdx].name) + headerMsg := fmt.Sprintf("%s %s", lightBlueText.Render(m.packages[m.pkgIdx].name), lightGrayText.Render("package logs")) return lipgloss.NewStyle().Padding(0, 3).Render( fmt.Sprintf("%s\n%s\n%s\n\n", m.logHeaderView(headerMsg), m.logViewport.View(), m.logFooterView()), ) @@ -70,26 +70,12 @@ func (m *Model) deployView() string { } var text string - if p.percLayersVerified > 0 { - perc := lightGrayText.Render(fmt.Sprintf("%d%%", int32(p.percLayersVerified))) - text = lipgloss.NewStyle(). - Align(lipgloss.Left). - Padding(0, 3). - Render(fmt.Sprintf("%s Verifying pkg %s (%s)", p.verifySpinner.View(), p.name, perc)) - } - if p.numComponents != 0 { - // todo: sometimes this says it's deploying 0/0 components, fix this - text = lipgloss.NewStyle(). - Align(lipgloss.Left). - Padding(0, 3). - Render(fmt.Sprintf("%s Package %s deploying (%d / %d components)", p.deploySpinner.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)) - } + // todo: check is pkg is remote.... + //if p.isRemote { + text = genRemotePkgText(p, numComponentsSuccess) + //} else { + // text = genLocalPkgText(p, numComponentsSuccess) + //} view = lipgloss.JoinVertical(lipgloss.Left, view, text+"\n") } @@ -97,6 +83,58 @@ func (m *Model) deployView() string { return view } +func genLocalPkgText(p pkgState, numComponentsSuccess int) string { + text := "" + if p.numComponents > 0 { + text = lipgloss.NewStyle(). + Align(lipgloss.Left). + Padding(0, 3). + Render(fmt.Sprintf("%s Package %s deploying (%d / %d components)", p.deploySpinner.View(), p.name, min(numComponentsSuccess+1, p.numComponents), p.numComponents)) + } else { + text = lipgloss.NewStyle(). + Align(lipgloss.Left). + Padding(0, 3). + Render(fmt.Sprintf("%s Package %s deploying", p.deploySpinner.View(), p.name)) + } + return text +} + +func genRemotePkgText(p pkgState, numComponentsSuccess int) string { + text := "" + styledName := lightBlueText.Render(p.name) + if !p.verified { + perc := lightGrayText.Render(fmt.Sprintf("%d%%", p.percLayersVerified)) + text = lipgloss.NewStyle(). + Align(lipgloss.Left). + Padding(0, 3). + Render(fmt.Sprintf("%s Verifying %s package (%s)", p.verifySpinner.View(), styledName, perc)) + } else if p.verified && !p.downloaded { + perc := lightGrayText.Render(fmt.Sprintf("%d%%", p.percDownloaded)) + text = lipgloss.NewStyle(). + Align(lipgloss.Left). + Padding(0, 3). + Render(fmt.Sprintf("%s Downloading %s package (%s)", p.deploySpinner.View(), styledName, perc)) + } else if p.downloaded && p.verified && p.numComponents > 0 { + text = lipgloss.NewStyle(). + Align(lipgloss.Left). + Padding(0, 3). + Render(fmt.Sprintf("%s Deploying %s package (%d / %d components)", p.deploySpinner.View(), styledName, min(numComponentsSuccess+1, p.numComponents), p.numComponents)) + } else { + text = lipgloss.NewStyle(). + Align(lipgloss.Left). + Padding(0, 3). + Render(fmt.Sprintf("%s Deploying %s package", p.deploySpinner.View(), styledName)) + } + + if p.complete { + text = lipgloss.NewStyle(). + Align(lipgloss.Left). + Padding(0, 3). + Render(fmt.Sprintf("✅ Package %s deployed", p.name)) + } + return text +} + func (m *Model) preDeployView() string { paddingStyle := lipgloss.NewStyle().Padding(0, 3) header := paddingStyle.Render("🎁 BUNDLE DEFINITION") @@ -105,9 +143,6 @@ func (m *Model) preDeployView() string { m.yamlViewport.SetContent(prettyYAML) headerMsg := "Use mouse wheel to scroll" - //return lipgloss.NewStyle().Padding(0, 3).Render( - // fmt.Sprintf("%s\n%s\n%s\n\n", m.logHeaderView(headerMsg), m.logViewport.View(), m.logFooterView()), - //) // Concatenate header, highlighted YAML, and prompt return fmt.Sprintf("\n%s\n\n%s\n%s\n%s\n\n%s", diff --git a/src/pkg/sources/remote.go b/src/pkg/sources/remote.go index 3d10a570f..e3895eae0 100644 --- a/src/pkg/sources/remote.go +++ b/src/pkg/sources/remote.go @@ -177,6 +177,7 @@ func (r *RemoteBundle) downloadPkgFromRemoteBundle() ([]ocispec.Descriptor, erro 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) @@ -187,7 +188,7 @@ func (r *RemoteBundle) downloadPkgFromRemoteBundle() ([]ocispec.Descriptor, erro numLayersVerified++ if ok { percVerified := numLayersVerified / float64(len(pkgManifest.Layers)) * 100 - deploy.Program.Send(fmt.Sprintf("verified:%v", percVerified)) + deploy.Program.Send(fmt.Sprintf("verifying:%v", int64(percVerified))) estimatedBytes += layer.Size layersInBundle = append(layersInBundle, layer) digest := layer.Digest.Encoded() @@ -215,6 +216,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/utils/utils.go b/src/pkg/utils/utils.go index a7b85de1c..f244368e5 100644 --- a/src/pkg/utils/utils.go +++ b/src/pkg/utils/utils.go @@ -57,7 +57,7 @@ func IsValidTarballPath(path string) bool { } // ConfigureLogs sets up the log file, log cache and output for the CLI -func ConfigureLogs() error { +func ConfigureLogs(op string) error { writer, err := message.UseLogFile("") logFile := writer if err != nil { @@ -82,7 +82,8 @@ func ConfigureLogs() error { logWriter := io.MultiWriter(logFile, CacheLogFile) // use Zarf pterm output if no-tea flag is set - if !config.TeaEnabled || config.CommonOptions.NoTea { + // 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", location) logWriter = io.MultiWriter(os.Stderr, CacheLogFile, logFile) pterm.SetDefaultOutput(logWriter)