diff --git a/src/pkg/bundle/deploy.go b/src/pkg/bundle/deploy.go index c2b83841..006b7dd1 100644 --- a/src/pkg/bundle/deploy.go +++ b/src/pkg/bundle/deploy.go @@ -281,6 +281,7 @@ func (b *Bundle) loadChartOverrides(pkg types.Package, pkgVars map[string]string return processed, nil } +// PreDeployValidation validates the bundle before deployment 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 diff --git a/src/pkg/bundle/tarball.go b/src/pkg/bundle/tarball.go index 5fbbca57..1213dfd6 100644 --- a/src/pkg/bundle/tarball.go +++ b/src/pkg/bundle/tarball.go @@ -301,7 +301,7 @@ func (tp *tarballBundleProvider) PublishBundle(bundle types.UDSBundle, remote *o retries := 0 // reset retries if a desc was successful - copyOpts.PostCopy = func(_ context.Context, desc ocispec.Descriptor) error { + copyOpts.PostCopy = func(_ context.Context, _ ocispec.Descriptor) error { retries = 0 return nil } diff --git a/src/pkg/bundle/tui/deploy/model.go b/src/pkg/bundle/tui/deploy/model.go index da92b8e3..50835ee3 100644 --- a/src/pkg/bundle/tui/deploy/model.go +++ b/src/pkg/bundle/tui/deploy/model.go @@ -67,6 +67,7 @@ type pkgState struct { isRemote bool } +// Model contains the state of the TUI type Model struct { bndlClient bndlClientShim bundleYAML string @@ -137,12 +138,14 @@ func InitModel(client bndlClientShim) Model { } } +// Init performs some action when BubbleTea starts up func (m *Model) Init() tea.Cmd { return func() tea.Msg { return doPreDeploy } } +// Update updates the model based on the message received func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { select { case err := <-m.errChan: @@ -247,6 +250,9 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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) + if m.isRemoteBundle { + m.packages[m.pkgIdx].downloaded = true + } } case totalPackages: if totalPkgs, err := strconv.Atoi(strings.Split(msg, ":")[1]); err == nil { @@ -276,6 +282,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } +// View returns the view for the TUI func (m *Model) View() string { if m.done { // no errors, clear the controlled Program's output diff --git a/src/pkg/bundle/tui/deploy/model_test.go b/src/pkg/bundle/tui/deploy/model_test.go new file mode 100644 index 00000000..92b00d53 --- /dev/null +++ b/src/pkg/bundle/tui/deploy/model_test.go @@ -0,0 +1,114 @@ +package deploy + +import ( + "testing" + + tea "github.com/charmbracelet/bubbletea" + "github.com/stretchr/testify/require" +) + +func TestDeploy(t *testing.T) { + testPkgs := []pkgState{ + { + name: "test-pkg", + numComponents: 1, + componentStatuses: []bool{true}, + }, { + name: "test-pkg-2", + numComponents: 1, + componentStatuses: []bool{true}, + }, + } + initTestModel := func() *Model { + + m := InitModel(nil) + m.validatingBundle = false + m.totalPkgs = 2 + m.bundleName = "test-bundle" + m.logViewport.Width = 50 + m.logViewport.Height = 50 + m.bundleYAML = "fake bundle YAML" + return &m + } + + t.Run("test deploy", func(t *testing.T) { + m := initTestModel() + + // check pre-deploy view + view := m.View() + require.Contains(t, view, m.bundleYAML) + require.Contains(t, view, "Deploy this bundle? (y/n)") + + // simulate pressing 'y' key to confirm deployment + m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []int32{121}}) + view = m.View() + require.Contains(t, view, "UDS Bundle: test-bundle") + + // deploy first pkg in bundle with simulated components + m.Update("newPackage:test-pkg:0") + m.packages[m.pkgIdx].numComponents = 1 + m.packages[m.pkgIdx].componentStatuses = []bool{true} + view = m.View() + require.Contains(t, view, "Deploying bundle package (1 / 2)") + require.Contains(t, view, "Package test-pkg deploying (1 / 1 components)") + + // simulate package deployment completion + m.Update("complete:test-pkg") + //m.Update(deployTickMsg(time.Time{})) + view = m.View() + require.Contains(t, view, "Package test-pkg deployed") + require.NotContains(t, view, "Package test-pkg deploying") + + // deploy second pkg in bundle with simulated components + m.Update("newPackage:test-pkg-2:1") + m.packages[m.pkgIdx].numComponents = 1 + m.packages[m.pkgIdx].componentStatuses = []bool{true} + view = m.View() + require.Contains(t, view, "Deploying bundle package (2 / 2)") + require.Contains(t, view, "Package test-pkg-2 deploying (1 / 1 components)") + + // simulate package deployment completion + m.Update("complete:test-pkg-2") + view = m.View() + require.Contains(t, view, "Package test-pkg-2 deployed") + require.NotContains(t, view, "Package test-pkg-2 deploying") + }) + + t.Run("test toggle log view", func(t *testing.T) { + m := initTestModel() + + // simulate passing --confirm + m.inProgress = true + m.confirmed = true + m.packages = testPkgs + + view := m.View() + require.Contains(t, view, "Package test-pkg deploying (1 / 1 components)") + + // simulate pressing 'l' key to toggle logs + m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []int32{108}}) + + view = m.View() + require.Contains(t, view, "test-pkg package logs") + + // simulate pressing 'l' key to toggle logs + m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []int32{108}}) + + view = m.View() + require.NotContains(t, view, "test-pkg package logs") + }) + + t.Run("test deploy cancel", func(t *testing.T) { + m := initTestModel() + view := m.View() + require.Contains(t, view, "Deploy this bundle? (y/n)") + + // simulate pressing 'n' key to cancel deployment + m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []int32{110}}) + view = m.View() + + // model's view is cleared after canceling deployment + require.Equal(t, view, "") + }) + +} diff --git a/src/test/e2e/commands_test.go b/src/test/e2e/commands_test.go index 72adfa44..c5209da3 100644 --- a/src/test/e2e/commands_test.go +++ b/src/test/e2e/commands_test.go @@ -105,6 +105,13 @@ func deploy(t *testing.T, tarballPath string) (stdout string, stderr string) { return stdout, stderr } +func deployWithTUI(t *testing.T, source string) (stdout string, stderr string) { + cmd := strings.Split(fmt.Sprintf("deploy %s --confirm", source), " ") + stdout, stderr, err := e2e.UDS(cmd...) + require.NoError(t, err) + return stdout, stderr +} + func runCmd(t *testing.T, input string) (stdout string, stderr string) { cmd := strings.Split(input, " ") stdout, stderr, err := e2e.UDS(cmd...) @@ -124,8 +131,8 @@ func deployResumeFlag(t *testing.T, tarballPath string) { require.NoError(t, err) } -func remove(t *testing.T, tarballPath string) { - cmd := strings.Split(fmt.Sprintf("remove %s --confirm --insecure", tarballPath), " ") +func remove(t *testing.T, source string) { + cmd := strings.Split(fmt.Sprintf("remove %s --confirm --insecure", source), " ") _, _, err := e2e.UDS(cmd...) require.NoError(t, err) } diff --git a/src/test/e2e/tui_test.go b/src/test/e2e/tui_test.go new file mode 100644 index 00000000..fe13093e --- /dev/null +++ b/src/test/e2e/tui_test.go @@ -0,0 +1,58 @@ +package test + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBundleDeploy(t *testing.T) { + deployZarfInit(t) + e2e.CreateZarfPkg(t, "src/test/packages/podinfo", false) + + source := "ghcr.io/defenseunicorns/packages/uds-cli/test/publish/ghcr-test:0.0.1" + stdout, _ := deployWithTUI(t, source) + require.Contains(t, stdout, "Validating bundle") + require.Contains(t, stdout, "UDS Bundle: ghcr-test") + require.Contains(t, stdout, "Verifying podinfo package (0%)") + require.Contains(t, stdout, "Downloading podinfo package (0%)") + require.Contains(t, stdout, "Deploying podinfo package (1 / 1 components)") + require.Contains(t, stdout, "Deploying nginx package (1 / 1 components)") + require.Contains(t, stdout, "✔ Package podinfo deployed") + require.Contains(t, stdout, "✔ Package nginx deployed") + require.Contains(t, stdout, "Verifying nginx package (0%)") + require.Contains(t, stdout, "Downloading nginx package (0%)") + require.Contains(t, stdout, "✨ Bundle ghcr-test deployed successfully") + remove(t, source) +} + +func TestBundleDeployWithBadSource(t *testing.T) { + deployZarfInit(t) + e2e.CreateZarfPkg(t, "src/test/packages/podinfo", false) + + source := "a.bad.source:0.0.1" + stdout, _ := deployWithTUI(t, source) + require.Contains(t, stdout, "❌ Error deploying bundle: a.bad.source:0.0.1: not found") +} + +func TestBundleDeployWithBadPkg(t *testing.T) { + deployZarfInit(t) + + // deploy a good pkg + source := "ghcr.io/defenseunicorns/packages/uds-cli/test/publish/ghcr-test:0.0.1 --packages=nginx" + stdout, _ := deployWithTUI(t, source) + require.Contains(t, stdout, "✨ Bundle ghcr-test deployed successfully") + + // attempt to deploy a conflicting pkg + e2e.CreateZarfPkg(t, "src/test/packages/gitrepo", false) + bundleDir := "src/test/bundles/05-gitrepo" + bundlePath := filepath.Join(bundleDir, fmt.Sprintf("uds-bundle-gitrepo-%s-0.0.1.tar.zst", e2e.Arch)) + + createLocal(t, bundleDir, e2e.Arch) + stdout, _ = deployWithTUI(t, bundlePath) + require.Contains(t, stdout, "❌ Error deploying bundle: unable to deploy component \"nginx-remote\": unable to install helm chart") + require.Contains(t, stdout, "Run uds logs to view deployment logs") + remove(t, source) +}