Skip to content

Commit

Permalink
feat: add ability to uds create to local output path (#547)
Browse files Browse the repository at this point in the history
  • Loading branch information
TristanHoladay committed Apr 4, 2024
1 parent 217d029 commit e364437
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 9 deletions.
3 changes: 2 additions & 1 deletion src/pkg/bundle/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -154,7 +155,7 @@ func (b *Bundle) ValidateBundleResources(spinner *message.Spinner) error {
}
} else {
// atm we don't support outputting a bundle with local pkgs outputting to OCI
if 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)
}
path := getPkgPath(pkg, bundle.Metadata.Architecture)
Expand Down
11 changes: 6 additions & 5 deletions src/pkg/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package bundler

import (
"github.com/defenseunicorns/uds-cli/src/pkg/utils"
"github.com/defenseunicorns/uds-cli/src/types"
)

Expand Down Expand Up @@ -41,15 +42,15 @@ func NewBundler(opts *Options) *Bundler {

// 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 utils.IsRegistryURL(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
}
Expand Down
17 changes: 14 additions & 3 deletions src/pkg/bundler/localbundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -32,13 +33,15 @@ type LocalBundleOpts struct {
Bundle *types.UDSBundle
TmpDstDir string
SourceDir string
OutputDir string
}

// LocalBundle enables create ops with local bundles
type LocalBundle struct {
bundle *types.UDSBundle
tmpDstDir string
sourceDir string
outputDir string
}

// NewLocalBundle creates a new local bundle
Expand All @@ -47,6 +50,7 @@ func NewLocalBundle(opts *LocalBundleOpts) *LocalBundle {
bundle: opts.Bundle,
tmpDstDir: opts.TmpDstDir,
sourceDir: opts.SourceDir,
outputDir: opts.OutputDir,
}
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)

Expand Down
44 changes: 44 additions & 0 deletions src/pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"

"github.com/defenseunicorns/uds-cli/src/config"
Expand Down Expand Up @@ -139,3 +140,46 @@ 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, "://")
}

// 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 {
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 {
// 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
}

// IsRegistryURL checks if a string is a URL
func IsRegistryURL(s string) bool {
if hasScheme(s) || hasDomain(s) || hasPort(s) {
return true
}

return false
}
93 changes: 93 additions & 0 deletions src/pkg/utils/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package utils

import (
"testing"

"github.com/stretchr/testify/require"
)

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) {
actualResult := IsRegistryURL(tt.args.output)
require.Equal(t, tt.wantResult, actualResult)
})
}
}
16 changes: 16 additions & 0 deletions src/test/e2e/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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), " ")
Expand Down
6 changes: 6 additions & 0 deletions src/test/e2e/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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...)
Expand Down

0 comments on commit e364437

Please sign in to comment.