diff --git a/.gitignore b/.gitignore index 2a68c38107..9c90e0b138 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ zarf-sbom/ test-*.txt __debug_bin .netlify +!**/testdata/** coverage.out diff --git a/src/cmd/common/setup.go b/src/cmd/common/setup.go index ae2052b882..72fd0d6722 100644 --- a/src/cmd/common/setup.go +++ b/src/cmd/common/setup.go @@ -5,8 +5,10 @@ package common import ( + "fmt" "io" "os" + "time" "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/config/lang" @@ -51,14 +53,20 @@ func SetupCLI() { } if !config.SkipLogFile { - logFile, err := message.UseLogFile("") + ts := time.Now().Format("2006-01-02-15-04-05") + + f, err := os.CreateTemp("", fmt.Sprintf("zarf-%s-*.log", ts)) + if err != nil { + message.WarnErr(err, "Error creating a log file in a temporary directory") + return + } + logFile, err := message.UseLogFile(f) if err != nil { message.WarnErr(err, "Error saving a log file to a temporary directory") return } pterm.SetDefaultOutput(io.MultiWriter(os.Stderr, logFile)) - location := message.LogFileLocation() - message.Notef("Saving log file to %s", location) + message.Notef("Saving log file to %s", f.Name()) } } diff --git a/src/pkg/message/credentials.go b/src/pkg/message/credentials.go index 60ca971119..07b85525bc 100644 --- a/src/pkg/message/credentials.go +++ b/src/pkg/message/credentials.go @@ -32,8 +32,8 @@ func PrintCredentialTable(state *types.ZarfState, componentsToDeploy []types.Dep // Pause the logfile's output to avoid credentials being printed to the log file if logFile != nil { - logFile.pause() - defer logFile.resume() + logFile.Pause() + defer logFile.Resume() } loginData := [][]string{} @@ -95,8 +95,8 @@ func PrintComponentCredential(state *types.ZarfState, componentName string) { func PrintCredentialUpdates(oldState *types.ZarfState, newState *types.ZarfState, services []string) { // Pause the logfile's output to avoid credentials being printed to the log file if logFile != nil { - logFile.pause() - defer logFile.resume() + logFile.Pause() + defer logFile.Resume() } for _, service := range services { diff --git a/src/pkg/message/message.go b/src/pkg/message/message.go index 1801c7b9f5..713bb90cbe 100644 --- a/src/pkg/message/message.go +++ b/src/pkg/message/message.go @@ -7,7 +7,6 @@ package message import ( "encoding/json" "fmt" - "io" "net/http" "os" "runtime/debug" @@ -49,7 +48,7 @@ var RuleLine = strings.Repeat("━", TermWidth) var logLevel = InfoLevel // logFile acts as a buffer for logFile generation -var logFile *pausableLogFile +var logFile *PausableWriter // DebugWriter represents a writer interface that writes to message.Debug type DebugWriter struct{} @@ -77,32 +76,14 @@ func init() { pterm.SetDefaultOutput(os.Stderr) } -// UseLogFile writes output to stderr and a logFile. -func UseLogFile(dir string) (io.Writer, error) { - // Prepend the log filename with a timestamp. - ts := time.Now().Format("2006-01-02-15-04-05") - - f, err := os.CreateTemp(dir, fmt.Sprintf("zarf-%s-*.log", ts)) - if err != nil { - return nil, err - } - - logFile = &pausableLogFile{ - wr: f, - f: f, - } +// UseLogFile wraps a given file in a PausableWriter +// and sets it as the log file used by the message package. +func UseLogFile(f *os.File) (*PausableWriter, error) { + logFile = NewPausableWriter(f) return logFile, nil } -// LogFileLocation returns the location of the log file. -func LogFileLocation() string { - if logFile == nil { - return "" - } - return logFile.f.Name() -} - // SetLogLevel sets the log level. func SetLogLevel(lvl LogLevel) { logLevel = lvl diff --git a/src/pkg/message/pausable.go b/src/pkg/message/pausable.go index ee8d7b6a67..b9e8fae1c7 100644 --- a/src/pkg/message/pausable.go +++ b/src/pkg/message/pausable.go @@ -6,26 +6,29 @@ package message import ( "io" - "os" ) -// pausableLogFile is a pausable log file -type pausableLogFile struct { - wr io.Writer - f *os.File +// PausableWriter is a pausable writer +type PausableWriter struct { + out, wr io.Writer } -// pause the log file -func (l *pausableLogFile) pause() { - l.wr = io.Discard +// NewPausableWriter creates a new pausable writer +func NewPausableWriter(wr io.Writer) *PausableWriter { + return &PausableWriter{out: wr, wr: wr} } -// resume the log file -func (l *pausableLogFile) resume() { - l.wr = l.f +// Pause sets the output writer to io.Discard +func (pw *PausableWriter) Pause() { + pw.out = io.Discard } -// Write writes the data to the log file -func (l *pausableLogFile) Write(p []byte) (n int, err error) { - return l.wr.Write(p) +// Resume sets the output writer back to the original writer +func (pw *PausableWriter) Resume() { + pw.out = pw.wr +} + +// Write writes the data to the underlying output writer +func (pw *PausableWriter) Write(p []byte) (n int, err error) { + return pw.out.Write(p) } diff --git a/src/pkg/message/pausable_test.go b/src/pkg/message/pausable_test.go new file mode 100644 index 0000000000..2cfeb2c827 --- /dev/null +++ b/src/pkg/message/pausable_test.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package message + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPausableWriter(t *testing.T) { + var buf bytes.Buffer + + pw := NewPausableWriter(&buf) + + n, err := pw.Write([]byte("foo")) + require.NoError(t, err) + require.Equal(t, 3, n) + + require.Equal(t, "foo", buf.String()) + + pw.Pause() + + n, err = pw.Write([]byte("bar")) + require.NoError(t, err) + require.Equal(t, 3, n) + + require.Equal(t, "foo", buf.String()) + + pw.Resume() + + n, err = pw.Write([]byte("baz")) + require.NoError(t, err) + require.Equal(t, 3, n) + + require.Equal(t, "foobaz", buf.String()) +} diff --git a/src/pkg/packager/sources/new_test.go b/src/pkg/packager/sources/new_test.go index b1cdea51bf..2e20bc1c21 100644 --- a/src/pkg/packager/sources/new_test.go +++ b/src/pkg/packager/sources/new_test.go @@ -5,49 +5,181 @@ package sources import ( + "encoding/json" "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" "testing" - "github.com/defenseunicorns/zarf/src/types" "github.com/stretchr/testify/require" + + "github.com/defenseunicorns/zarf/src/pkg/layout" + "github.com/defenseunicorns/zarf/src/pkg/packager/filters" + "github.com/defenseunicorns/zarf/src/types" ) -var ociS *OCISource -var urlS *URLSource -var tarballS *TarballSource -var splitS *SplitTarballSource -var packageS *PackageSource +func TestNewPackageSource(t *testing.T) { + t.Parallel() -type source struct { - pkgSrc string - srcType string - source PackageSource -} + tests := []struct { + name string + src string + expectedIdentify string + expectedType PackageSource + }{ + { + name: "oci", + src: "oci://ghcr.io/defenseunicorns/packages/init:1.0.0", + expectedIdentify: "oci", + expectedType: &OCISource{}, + }, + { + name: "sget with sub path", + src: "sget://github.com/defenseunicorns/zarf-hello-world:x86", + expectedIdentify: "sget", + expectedType: &URLSource{}, + }, + { + name: "sget without host", + src: "sget://defenseunicorns/zarf-hello-world:x86_64", + expectedIdentify: "sget", + expectedType: &URLSource{}, + }, + { + name: "https", + src: "https://github.com/defenseunicorns/zarf/releases/download/v1.0.0/zarf-init-amd64-v1.0.0.tar.zst", + expectedIdentify: "https", + expectedType: &URLSource{}, + }, + { + name: "http", + src: "http://github.com/defenseunicorns/zarf/releases/download/v1.0.0/zarf-init-amd64-v1.0.0.tar.zst", + expectedIdentify: "http", + expectedType: &URLSource{}, + }, + { + name: "local tar init zst", + src: "zarf-init-amd64-v1.0.0.tar.zst", + expectedIdentify: "tarball", + expectedType: &TarballSource{}, + }, + { + name: "local tar", + src: "zarf-package-manifests-amd64-v1.0.0.tar", + expectedIdentify: "tarball", + expectedType: &TarballSource{}, + }, + { + name: "local tar manifest zst", + src: "zarf-package-manifests-amd64-v1.0.0.tar.zst", + expectedIdentify: "tarball", + expectedType: &TarballSource{}, + }, + { + name: "local tar split", + src: "testdata/.part000", + expectedIdentify: "split", + expectedType: &SplitTarballSource{}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() -var sources = []source{ - {pkgSrc: "oci://ghcr.io/defenseunicorns/packages/init:1.0.0", srcType: "oci", source: ociS}, - {pkgSrc: "sget://github.com/defenseunicorns/zarf-hello-world:x86", srcType: "sget", source: urlS}, - {pkgSrc: "sget://defenseunicorns/zarf-hello-world:x86_64", srcType: "sget", source: urlS}, - {pkgSrc: "https://github.com/defenseunicorns/zarf/releases/download/v1.0.0/zarf-init-amd64-v1.0.0.tar.zst", srcType: "https", source: urlS}, - {pkgSrc: "http://github.com/defenseunicorns/zarf/releases/download/v1.0.0/zarf-init-amd64-v1.0.0.tar.zst", srcType: "http", source: urlS}, - {pkgSrc: "zarf-init-amd64-v1.0.0.tar.zst", srcType: "tarball", source: tarballS}, - {pkgSrc: "zarf-package-manifests-amd64-v1.0.0.tar", srcType: "tarball", source: tarballS}, - {pkgSrc: "zarf-package-manifests-amd64-v1.0.0.tar.zst", srcType: "tarball", source: tarballS}, - {pkgSrc: "some-dir/.part000", srcType: "split", source: splitS}, + require.Equal(t, tt.expectedIdentify, Identify(tt.src)) + ps, err := New(&types.ZarfPackageOptions{PackageSource: tt.src}) + require.NoError(t, err) + require.IsType(t, tt.expectedType, ps) + }) + } } -func Test_identifySourceType(t *testing.T) { - for _, source := range sources { - actual := Identify(source.pkgSrc) - require.Equalf(t, source.srcType, actual, fmt.Sprintf("source: %s", source)) +func TestPackageSource(t *testing.T) { + t.Parallel() + + // Copy tar to a temp directory, otherwise Collect will delete it. + tarName := "zarf-package-wordpress-amd64-16.0.4.tar.zst" + testDir := t.TempDir() + src, err := os.Open(filepath.Join("testdata", tarName)) + require.NoError(t, err) + tarPath := filepath.Join(testDir, tarName) + dst, err := os.Create(tarPath) + require.NoError(t, err) + _, err = io.Copy(dst, src) + require.NoError(t, err) + src.Close() + dst.Close() + + b, err := os.ReadFile("./testdata/expected-pkg.json") + require.NoError(t, err) + expectedPkg := types.ZarfPackage{} + err = json.Unmarshal(b, &expectedPkg) + require.NoError(t, err) + + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + _, fp := filepath.Split(req.URL.Path) + f, err := os.Open(filepath.Join("testdata", fp)) + if err != nil { + rw.WriteHeader(http.StatusNotFound) + return + } + defer f.Close() + io.Copy(rw, f) + })) + + tests := []struct { + name string + src string + shasum string + }{ + { + name: "local", + src: tarPath, + }, + { + name: "http", + src: fmt.Sprintf("%s/zarf-package-wordpress-amd64-16.0.4.tar.zst", ts.URL), + shasum: "835b06fc509e639497fb45f45d432e5c4cbd5d84212db5357b16bc69724b0e26", + }, } -} + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + opts := &types.ZarfPackageOptions{ + PackageSource: tt.src, + Shasum: tt.shasum, + } + + ps, err := New(opts) + require.NoError(t, err) + packageDir := t.TempDir() + pkgLayout := layout.New(packageDir) + pkg, warnings, err := ps.LoadPackage(pkgLayout, filters.Empty(), false) + require.NoError(t, err) + require.Empty(t, warnings) + require.Equal(t, expectedPkg, pkg) + + ps, err = New(opts) + require.NoError(t, err) + metadataDir := t.TempDir() + metadataLayout := layout.New(metadataDir) + metadata, warnings, err := ps.LoadPackageMetadata(metadataLayout, true, false) + require.NoError(t, err) + require.Empty(t, warnings) + require.Equal(t, expectedPkg, metadata) -func TestNew(t *testing.T) { - for _, source := range sources { - actual, err := New(&types.ZarfPackageOptions{PackageSource: source.pkgSrc}) - require.NoError(t, err) - require.IsType(t, source.source, actual) - require.Implements(t, packageS, actual) + ps, err = New(opts) + require.NoError(t, err) + collectDir := t.TempDir() + fp, err := ps.Collect(collectDir) + require.NoError(t, err) + require.Equal(t, filepath.Join(collectDir, filepath.Base(tt.src)), fp) + }) } } diff --git a/src/pkg/packager/sources/tarball.go b/src/pkg/packager/sources/tarball.go index 55ba0c6b2e..2172d7ef13 100644 --- a/src/pkg/packager/sources/tarball.go +++ b/src/pkg/packager/sources/tarball.go @@ -204,5 +204,34 @@ func (s *TarballSource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM b // Collect for the TarballSource is essentially an `mv` func (s *TarballSource) Collect(dir string) (string, error) { dst := filepath.Join(dir, filepath.Base(s.PackageSource)) - return dst, os.Rename(s.PackageSource, dst) + err := os.Rename(s.PackageSource, dst) + linkErr := &os.LinkError{} + isLinkErr := errors.As(err, &linkErr) + if err != nil && !isLinkErr { + return "", err + } + if err == nil { + return dst, nil + } + + // Copy file if rename is not possible due to existing on different partitions. + srcFile, err := os.Open(linkErr.Old) + if err != nil { + return "", err + } + defer srcFile.Close() + dstFile, err := os.Create(linkErr.New) + if err != nil { + return "", err + } + defer dstFile.Close() + _, err = io.Copy(dstFile, srcFile) + if err != nil { + return "", err + } + err = os.Remove(linkErr.Old) + if err != nil { + return "", err + } + return dst, nil } diff --git a/src/pkg/packager/sources/testdata/expected-pkg.json b/src/pkg/packager/sources/testdata/expected-pkg.json new file mode 100644 index 0000000000..6f1e946899 --- /dev/null +++ b/src/pkg/packager/sources/testdata/expected-pkg.json @@ -0,0 +1,23 @@ +{ + "kind": "ZarfPackageConfig", + "metadata": { + "name": "wordpress", + "description": "\"A Zarf Package that deploys the WordPress blogging and content management platform\"\n", + "version": "16.0.4", + "architecture": "amd64", + "aggregateChecksum": "cd6cc0c238c45982d80b55d3dd3d27497d4f3b264e0f037a37c464be34a3640e" + }, + "build": { + "terminal": "control-center", + "user": "philip", + "architecture": "amd64", + "timestamp": "Wed, 22 May 2024 10:16:55 +0200", + "version": "v0.33.1", + "migrations": [ + "scripts-to-actions", + "pluralize-set-variable" + ], + "lastNonBreakingVersion": "v0.27.0" + }, + "components": [] +} \ No newline at end of file diff --git a/src/pkg/packager/sources/testdata/zarf-package-wordpress-amd64-16.0.4.tar.zst b/src/pkg/packager/sources/testdata/zarf-package-wordpress-amd64-16.0.4.tar.zst new file mode 100644 index 0000000000..b5800b3d54 Binary files /dev/null and b/src/pkg/packager/sources/testdata/zarf-package-wordpress-amd64-16.0.4.tar.zst differ diff --git a/src/pkg/packager/sources/testdata/zarf.yaml b/src/pkg/packager/sources/testdata/zarf.yaml new file mode 100644 index 0000000000..0e81803fa9 --- /dev/null +++ b/src/pkg/packager/sources/testdata/zarf.yaml @@ -0,0 +1,6 @@ +kind: ZarfPackageConfig +metadata: + name: wordpress + version: 16.0.4 + description: | + "A Zarf Package that deploys the WordPress blogging and content management platform"