From a933509111a5355299b9df3f57a03756ff1242f7 Mon Sep 17 00:00:00 2001 From: TristanHoladay <40547442+TristanHoladay@users.noreply.github.com> Date: Thu, 4 Apr 2024 08:39:00 -0600 Subject: [PATCH 1/4] feat: add ability to uds create to local output path --- src/pkg/bundle/common.go | 10 +++++++++- src/pkg/bundler/bundler.go | 20 +++++++++++++++----- src/pkg/bundler/localbundle.go | 17 ++++++++++++++--- src/test/e2e/bundle_test.go | 16 ++++++++++++++++ src/test/e2e/commands_test.go | 6 ++++++ 5 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/pkg/bundle/common.go b/src/pkg/bundle/common.go index d6a2548e..12e816ef 100644 --- a/src/pkg/bundle/common.go +++ b/src/pkg/bundle/common.go @@ -76,6 +76,14 @@ func (b *Bundle) ClearPaths() { _ = os.RemoveAll(b.tmp) } +// Checks if string is an oci url +func (b *Bundle) isURL(s string) bool { + if strings.Contains(s, "://") || strings.Contains(s, ".") && len(s) > 1 { + return true + } + return false +} + // ValidateBundleResources validates the bundle's metadata and package references func (b *Bundle) ValidateBundleResources(bundle *types.UDSBundle, spinner *message.Spinner) error { // TODO: need to validate arch of local OS @@ -154,7 +162,7 @@ func (b *Bundle) ValidateBundleResources(bundle *types.UDSBundle, spinner *messa } } else { // atm we don't support outputting a bundle with local pkgs outputting to OCI - if b.cfg.CreateOpts.Output != "" { + if b.isURL(b.cfg.CreateOpts.Output) { return fmt.Errorf("detected local Zarf package: %s, outputting to an OCI registry is not supported when using local Zarf packages", pkg.Name) } var fullPkgName string diff --git a/src/pkg/bundler/bundler.go b/src/pkg/bundler/bundler.go index 1ce7ae02..f2cc9cfe 100644 --- a/src/pkg/bundler/bundler.go +++ b/src/pkg/bundler/bundler.go @@ -5,6 +5,8 @@ package bundler import ( + "strings" + "github.com/defenseunicorns/uds-cli/src/types" ) @@ -39,17 +41,25 @@ func NewBundler(opts *Options) *Bundler { return &b } +// Checks if string is an oci url +func isURL(s string) bool { + if strings.Contains(s, "://") || strings.Contains(s, ".") && len(s) > 1 { + return true + } + return false +} + // Create creates a bundle func (b *Bundler) Create() error { - if b.output == "" { - localBundle := NewLocalBundle(&LocalBundleOpts{Bundle: b.bundle, TmpDstDir: b.tmpDstDir, SourceDir: b.sourceDir}) - err := localBundle.create(nil) + if isURL(b.output) { + remoteBundle := NewRemoteBundle(&RemoteBundleOpts{Bundle: b.bundle, Output: b.output}) + err := remoteBundle.create(nil) if err != nil { return err } } else { - remoteBundle := NewRemoteBundle(&RemoteBundleOpts{Bundle: b.bundle, Output: b.output}) - err := remoteBundle.create(nil) + localBundle := NewLocalBundle(&LocalBundleOpts{Bundle: b.bundle, TmpDstDir: b.tmpDstDir, SourceDir: b.sourceDir, OutputDir: b.output}) + err := localBundle.create(nil) if err != nil { return err } diff --git a/src/pkg/bundler/localbundle.go b/src/pkg/bundler/localbundle.go index 5e4efc2e..768c0aea 100644 --- a/src/pkg/bundler/localbundle.go +++ b/src/pkg/bundler/localbundle.go @@ -18,6 +18,7 @@ import ( "github.com/defenseunicorns/uds-cli/src/types" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/oci" + "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/pkg/zoci" goyaml "github.com/goccy/go-yaml" "github.com/mholt/archiver/v4" @@ -32,6 +33,7 @@ type LocalBundleOpts struct { Bundle *types.UDSBundle TmpDstDir string SourceDir string + OutputDir string } // LocalBundle enables create ops with local bundles @@ -39,6 +41,7 @@ type LocalBundle struct { bundle *types.UDSBundle tmpDstDir string sourceDir string + outputDir string } // NewLocalBundle creates a new local bundle @@ -47,6 +50,7 @@ func NewLocalBundle(opts *LocalBundleOpts) *LocalBundle { bundle: opts.Bundle, tmpDstDir: opts.TmpDstDir, sourceDir: opts.SourceDir, + outputDir: opts.OutputDir, } } @@ -159,8 +163,11 @@ func (lo *LocalBundle) create(signature []byte) error { return err } + if lo.outputDir == "" { + lo.outputDir = lo.sourceDir + } // tarball the bundle - err = writeTarball(bundle, artifactPathMap, lo.sourceDir) + err = writeTarball(bundle, artifactPathMap, lo.outputDir) if err != nil { return err } @@ -207,14 +214,18 @@ func pushManifestConfig(store *ocistore.Store, metadata types.UDSMetadata, build } // writeTarball builds and writes a bundle tarball to disk based on a file map -func writeTarball(bundle *types.UDSBundle, artifactPathMap types.PathMap, sourceDir string) error { +func writeTarball(bundle *types.UDSBundle, artifactPathMap types.PathMap, outputDir string) error { format := archiver.CompressedArchive{ Compression: archiver.Zstd{}, Archival: archiver.Tar{}, } filename := fmt.Sprintf("%s%s-%s-%s.tar.zst", config.BundlePrefix, bundle.Metadata.Name, bundle.Metadata.Architecture, bundle.Metadata.Version) - dst := filepath.Join(sourceDir, filename) + if !helpers.IsDir(outputDir) { + os.MkdirAll(outputDir, 0755) + } + + dst := filepath.Join(outputDir, filename) _ = os.RemoveAll(dst) diff --git a/src/test/e2e/bundle_test.go b/src/test/e2e/bundle_test.go index d17c94c3..4170b2d7 100644 --- a/src/test/e2e/bundle_test.go +++ b/src/test/e2e/bundle_test.go @@ -320,6 +320,22 @@ func TestBundleWithYmlFile(t *testing.T) { remove(t, bundlePath) } +func TestLocalBundleWithOutput(t *testing.T) { + path := "src/test/packages/nginx" + args := strings.Split(fmt.Sprintf("zarf package create %s -o %s --confirm", path, path), " ") + _, _, err := e2e.UDS(args...) + require.NoError(t, err) + + bundleDir := "src/test/bundles/09-uds-bundle-yml" + destDir := "src/test/test/" + bundlePath := filepath.Join(destDir, fmt.Sprintf("uds-bundle-yml-example-%s-0.0.1.tar.zst", e2e.Arch)) + createLocalWithOuputFlag(t, bundleDir, destDir, e2e.Arch) + + cmd := strings.Split(fmt.Sprintf("inspect %s", bundlePath), " ") + _, _, err = e2e.UDS(cmd...) + require.NoError(t, err) +} + func TestLocalBundleWithNoSBOM(t *testing.T) { path := "src/test/packages/nginx" args := strings.Split(fmt.Sprintf("zarf package create %s -o %s --skip-sbom --confirm", path, path), " ") diff --git a/src/test/e2e/commands_test.go b/src/test/e2e/commands_test.go index a89e99bb..55b75bc0 100644 --- a/src/test/e2e/commands_test.go +++ b/src/test/e2e/commands_test.go @@ -36,6 +36,12 @@ func createLocalError(bundlePath string, arch string) (stderr string) { return stderr } +func createLocalWithOuputFlag(t *testing.T, bundlePath string, destPath string, arch string) { + cmd := strings.Split(fmt.Sprintf("create %s -o %s --insecure --confirm -a %s", bundlePath, destPath, arch), " ") + _, _, err := e2e.UDS(cmd...) + require.NoError(t, err) +} + func createRemoteInsecure(t *testing.T, bundlePath, registry, arch string) { cmd := strings.Split(fmt.Sprintf("create %s -o %s --confirm --insecure -a %s", bundlePath, registry, arch), " ") _, _, err := e2e.UDS(cmd...) From 127987da3122228cd1ec397ede592a52de40eb35 Mon Sep 17 00:00:00 2001 From: TristanHoladay <40547442+TristanHoladay@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:01:12 -0600 Subject: [PATCH 2/4] created IsRegistryURL() --- src/pkg/bundle/common.go | 11 ++------- src/pkg/bundler/bundler.go | 13 ++--------- src/pkg/utils/utils.go | 46 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/src/pkg/bundle/common.go b/src/pkg/bundle/common.go index 12e816ef..b059d498 100644 --- a/src/pkg/bundle/common.go +++ b/src/pkg/bundle/common.go @@ -15,6 +15,7 @@ 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/pkg/cluster" "github.com/defenseunicorns/zarf/src/pkg/message" @@ -76,14 +77,6 @@ func (b *Bundle) ClearPaths() { _ = os.RemoveAll(b.tmp) } -// Checks if string is an oci url -func (b *Bundle) isURL(s string) bool { - if strings.Contains(s, "://") || strings.Contains(s, ".") && len(s) > 1 { - return true - } - return false -} - // ValidateBundleResources validates the bundle's metadata and package references func (b *Bundle) ValidateBundleResources(bundle *types.UDSBundle, spinner *message.Spinner) error { // TODO: need to validate arch of local OS @@ -162,7 +155,7 @@ func (b *Bundle) ValidateBundleResources(bundle *types.UDSBundle, spinner *messa } } else { // atm we don't support outputting a bundle with local pkgs outputting to OCI - if b.isURL(b.cfg.CreateOpts.Output) { + if utils.IsRegistryURL(b.cfg.CreateOpts.Output) { return fmt.Errorf("detected local Zarf package: %s, outputting to an OCI registry is not supported when using local Zarf packages", pkg.Name) } var fullPkgName string diff --git a/src/pkg/bundler/bundler.go b/src/pkg/bundler/bundler.go index f2cc9cfe..417530b3 100644 --- a/src/pkg/bundler/bundler.go +++ b/src/pkg/bundler/bundler.go @@ -5,8 +5,7 @@ package bundler import ( - "strings" - + "github.com/defenseunicorns/uds-cli/src/pkg/utils" "github.com/defenseunicorns/uds-cli/src/types" ) @@ -41,17 +40,9 @@ func NewBundler(opts *Options) *Bundler { return &b } -// Checks if string is an oci url -func isURL(s string) bool { - if strings.Contains(s, "://") || strings.Contains(s, ".") && len(s) > 1 { - return true - } - return false -} - // Create creates a bundle func (b *Bundler) Create() error { - if isURL(b.output) { + if utils.IsRegistryURL(b.output) { remoteBundle := NewRemoteBundle(&RemoteBundleOpts{Bundle: b.bundle, Output: b.output}) err := remoteBundle.create(nil) if err != nil { diff --git a/src/pkg/utils/utils.go b/src/pkg/utils/utils.go index 2f941205..7a3196bc 100644 --- a/src/pkg/utils/utils.go +++ b/src/pkg/utils/utils.go @@ -13,6 +13,7 @@ import ( "os/exec" "path/filepath" "regexp" + "strconv" "strings" "github.com/defenseunicorns/uds-cli/src/config" @@ -138,3 +139,48 @@ func ToLocalFile(t any, filePath string) error { func IsRemotePkg(pkg types.Package) bool { return pkg.Repository != "" } + +func hasScheme(s string) bool { + return strings.Contains(s, "://") +} + +func hasDomain(s string) bool { + return strings.Contains(s, ".") && len(s) > 1 +} + +func hasPort(s string) bool { + // look for colon and port (e.g localhost:31999) + colonIndex := strings.Index(s, ":") + firstSlashIndex := strings.Index(s, "/") + endIndex := firstSlashIndex + if firstSlashIndex == -1 { + endIndex = len(s) - 1 + } + if colonIndex != -1 { + port := s[colonIndex+1 : endIndex] + + // port valid number ? + _, err := strconv.Atoi(port) + if err == nil { + return true + } + } + return false +} + +// Checks if string is an oci url +func IsRegistryURL(s string) bool { + if hasScheme(s) { + return true + } + + if hasDomain(s) { + return true + } + + if hasPort(s) { + return true + } + + return false +} From 7a4205aedd5da906197b3786e14d7c72aa3a9537 Mon Sep 17 00:00:00 2001 From: TristanHoladay <40547442+TristanHoladay@users.noreply.github.com> Date: Thu, 4 Apr 2024 13:19:44 -0600 Subject: [PATCH 3/4] added domain cases for IsRegistryURL() and tests --- src/pkg/utils/utils.go | 8 +++- src/pkg/utils/utils_test.go | 92 +++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 src/pkg/utils/utils_test.go diff --git a/src/pkg/utils/utils.go b/src/pkg/utils/utils.go index 7a3196bc..b4dc3c33 100644 --- a/src/pkg/utils/utils.go +++ b/src/pkg/utils/utils.go @@ -144,8 +144,14 @@ func hasScheme(s string) bool { return strings.Contains(s, "://") } +// hasDomain checks if a string contains a domain. +// It assumes the domain is at the beginning of a URL and there is no scheme (e.g., oci://). func hasDomain(s string) bool { - return strings.Contains(s, ".") && len(s) > 1 + dotIndex := strings.Index(s, ".") + firstSlashIndex := strings.Index(s, "/") + + // dot exists; dot is not first char; not preceded by any / if / exists + return dotIndex != -1 && dotIndex != 0 && (firstSlashIndex == -1 || firstSlashIndex > dotIndex) } func hasPort(s string) bool { diff --git a/src/pkg/utils/utils_test.go b/src/pkg/utils/utils_test.go new file mode 100644 index 00000000..d628326b --- /dev/null +++ b/src/pkg/utils/utils_test.go @@ -0,0 +1,92 @@ +package utils + +import ( + "testing" +) + +func Test_IsRegistryURL(t *testing.T) { + type args struct { + output string + } + tests := []struct { + name string + description string + args args + wantResult bool + }{ + { + name: "HasScheme", + description: "Output has a scheme ://", + args: args{output: "oci://ghcr.io/defenseunicorns/dev"}, + wantResult: true, + }, + { + name: "HasDomain", + description: "Output has no scheme but has domain", + args: args{output: "ghcr.io/defenseunicorns/dev"}, + wantResult: true, + }, + { + name: "HasMultiDomain", + description: "Output has no scheme but has domain in form of example.example.com", + args: args{output: "registry.example.io/defenseunicorns/dev"}, + wantResult: true, + }, + { + name: "HasDomainAndNoPath", + description: "Output has no scheme but has domain in form of example.example.com", + args: args{output: "registry.example.io"}, + wantResult: true, + }, + { + name: "HasPort", + description: "Output has no scheme or domain (with .) but has port", + args: args{output: "localhost:31999"}, + wantResult: true, + }, + { + name: "HasPortWithTrailingSlash", + description: "Output has no scheme or domain (with .) but has port with trailing /", + args: args{output: "localhost:31999/path"}, + wantResult: true, + }, + { + name: "IsLocalPath", + description: "Output is to local path", + args: args{output: "local/path"}, + wantResult: false, + }, + { + name: "IsCurrentDirectory", + description: "Output is current directory", + args: args{output: "."}, + wantResult: false, + }, + { + name: "IsHiddenDirectory", + description: "Output is a hidden directory", + args: args{output: ".dev"}, + wantResult: false, + }, + { + name: "IsHiddenDirectoryWithSlashPrefix", + description: "Output is a hidden directory nested in path", + args: args{output: "/pathto/.dev"}, + wantResult: false, + }, + { + name: "HasRareDotInLocalDirectoryPath", + description: "Output is a hidden directory nested in path", + args: args{output: "/pathto/test.dev/"}, + wantResult: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if result := IsRegistryURL(tt.args.output); result != tt.wantResult { + t.Errorf("IsRegistryURL() result = %v, wantResult %v", result, tt.wantResult) + } + }) + } +} From d76404fce5581f4092be168bdb871881b022d7d6 Mon Sep 17 00:00:00 2001 From: TristanHoladay <40547442+TristanHoladay@users.noreply.github.com> Date: Thu, 4 Apr 2024 14:42:43 -0600 Subject: [PATCH 4/4] use require.Equal in utils_tests.go; refactor IsRegistryURL() --- src/pkg/utils/utils.go | 12 ++---------- src/pkg/utils/utils_test.go | 7 ++++--- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/pkg/utils/utils.go b/src/pkg/utils/utils.go index b4dc3c33..fe88cdef 100644 --- a/src/pkg/utils/utils.go +++ b/src/pkg/utils/utils.go @@ -174,17 +174,9 @@ func hasPort(s string) bool { return false } -// Checks if string is an oci url +// IsRegistryURL checks if a string is a URL func IsRegistryURL(s string) bool { - if hasScheme(s) { - return true - } - - if hasDomain(s) { - return true - } - - if hasPort(s) { + if hasScheme(s) || hasDomain(s) || hasPort(s) { return true } diff --git a/src/pkg/utils/utils_test.go b/src/pkg/utils/utils_test.go index d628326b..a0589cf2 100644 --- a/src/pkg/utils/utils_test.go +++ b/src/pkg/utils/utils_test.go @@ -2,6 +2,8 @@ package utils import ( "testing" + + "github.com/stretchr/testify/require" ) func Test_IsRegistryURL(t *testing.T) { @@ -84,9 +86,8 @@ func Test_IsRegistryURL(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if result := IsRegistryURL(tt.args.output); result != tt.wantResult { - t.Errorf("IsRegistryURL() result = %v, wantResult %v", result, tt.wantResult) - } + actualResult := IsRegistryURL(tt.args.output) + require.Equal(t, tt.wantResult, actualResult) }) } }