From 41c08079bd5ed19ef0902ce8477e795ed0f3887d Mon Sep 17 00:00:00 2001 From: Philip Laine Date: Wed, 11 Sep 2024 19:34:39 +0200 Subject: [PATCH] refactor: pull Signed-off-by: Philip Laine --- src/cmd/package.go | 32 ++++-- src/internal/packager2/packager2.go | 164 ++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 7 deletions(-) create mode 100644 src/internal/packager2/packager2.go diff --git a/src/cmd/package.go b/src/cmd/package.go index a40439d53f..753dcba62d 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -8,16 +8,20 @@ import ( "context" "errors" "fmt" + "os" "path/filepath" "regexp" "strings" "github.com/zarf-dev/zarf/src/cmd/common" "github.com/zarf-dev/zarf/src/config/lang" + "github.com/zarf-dev/zarf/src/internal/packager2" "github.com/zarf-dev/zarf/src/pkg/lint" "github.com/zarf-dev/zarf/src/pkg/message" + "github.com/zarf-dev/zarf/src/pkg/packager/filters" "github.com/zarf-dev/zarf/src/pkg/packager/sources" "github.com/zarf-dev/zarf/src/types" + "helm.sh/helm/v3/pkg/time" "oras.land/oras-go/v2/registry" @@ -272,21 +276,34 @@ var packagePublishCmd = &cobra.Command{ }, } +var pullOptions = struct { + OutputDirectory string + Name string +}{} + var packagePullCmd = &cobra.Command{ Use: "pull PACKAGE_SOURCE", Short: lang.CmdPackagePullShort, Example: lang.CmdPackagePullExample, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - pkgConfig.PkgOpts.PackageSource = args[0] - pkgClient, err := packager.New(&pkgConfig) + outputDir := pullOptions.OutputDirectory + if outputDir == "" { + wd, err := os.Getwd() + if err != nil { + return err + } + outputDir = wd + } + name := pullOptions.Name + if name == "" { + name = fmt.Sprintf("zarf-package-%d", time.Now().Unix()) + } + tarPath := filepath.Join(outputDir, name) + err := packager2.Fetch(cmd.Context(), args[0], tarPath, pkgConfig.PkgOpts.Shasum, filters.Empty()) if err != nil { return err } - defer pkgClient.ClearTempPaths() - if err := pkgClient.Pull(cmd.Context()); err != nil { - return fmt.Errorf("failed to pull package: %w", err) - } return nil }, } @@ -483,5 +500,6 @@ func bindPublishFlags(v *viper.Viper) { func bindPullFlags(v *viper.Viper) { pullFlags := packagePullCmd.Flags() - pullFlags.StringVarP(&pkgConfig.PullOpts.OutputDirectory, "output-directory", "o", v.GetString(common.VPkgPullOutputDir), lang.CmdPackagePullFlagOutputDirectory) + pullFlags.StringVarP(&pullOptions.OutputDirectory, "output-directory", "o", v.GetString(common.VPkgPullOutputDir), "Specify the output directory for the pulled Zarf package") + pullFlags.StringVarP(&pullOptions.Name, "name", "n", "", "The name of the output tar file.") } diff --git a/src/internal/packager2/packager2.go b/src/internal/packager2/packager2.go new file mode 100644 index 0000000000..7a3bd194f7 --- /dev/null +++ b/src/internal/packager2/packager2.go @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package packager2 is the new implementation for packager. +package packager2 + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + + "github.com/defenseunicorns/pkg/helpers/v2" + "github.com/defenseunicorns/pkg/oci" + goyaml "github.com/goccy/go-yaml" + "github.com/mholt/archiver/v3" + + "github.com/zarf-dev/zarf/src/api/v1alpha1" + "github.com/zarf-dev/zarf/src/config" + "github.com/zarf-dev/zarf/src/pkg/layout" + "github.com/zarf-dev/zarf/src/pkg/packager/filters" + "github.com/zarf-dev/zarf/src/pkg/utils" + "github.com/zarf-dev/zarf/src/pkg/zoci" +) + +// Fetch fetches the Zarf package from the given sources. +func Fetch(ctx context.Context, src, tarPath, shasum string, filter filters.ComponentFilterStrategy) error { + u, err := url.Parse(src) + if err != nil { + return err + } + if u.Scheme == "" { + return errors.New("scheme cannot be empty") + } + if u.Host == "" { + return errors.New("scheme cannot be empty") + } + switch u.Scheme { + case "oci": + err := fetchOCI(ctx, src, tarPath, shasum, filter) + if err != nil { + return err + } + case "http", "https": + err := fetchHTTP(ctx, src, tarPath, shasum) + if err != nil { + return err + } + default: + return fmt.Errorf("unknown scheme %s", u.Scheme) + } + + // Minimal effort to verify that this is a Zarf package. + // TODO (phillebaba): Expand in the future to include more package verification. + err = archiver.Walk(tarPath, func(f archiver.File) error { + if f.Name() == layout.ZarfYAML { + b, err := io.ReadAll(f) + if err != nil { + return err + } + var pkg v1alpha1.ZarfPackage + err = goyaml.Unmarshal(b, &pkg) + if err != nil { + return err + } + } + return nil + }) + if err != nil { + return err + } + return nil +} + +func fetchOCI(ctx context.Context, src, tarPath, shasum string, filter filters.ComponentFilterStrategy) error { + tmpDir, err := utils.MakeTempDir(config.CommonOptions.TempDirectory) + if err != nil { + return err + } + defer os.Remove(tmpDir) + if shasum != "" { + src = fmt.Sprintf("%s@sha256:%s", src, shasum) + } + arch := config.GetArch() + remote, err := zoci.NewRemote(src, oci.PlatformForArch(arch)) + if err != nil { + return err + } + pkg, err := remote.FetchZarfYAML(ctx) + if err != nil { + return err + } + pkg.Components, err = filter.Apply(pkg) + if err != nil { + return err + } + layersToPull, err := remote.LayersFromRequestedComponents(ctx, pkg.Components) + if err != nil { + return err + } + _, err = remote.PullPackage(ctx, tmpDir, config.CommonOptions.OCIConcurrency, layersToPull...) + if err != nil { + return err + } + allTheLayers, err := filepath.Glob(filepath.Join(tmpDir, "*")) + if err != nil { + return err + } + err = os.Remove(tarPath) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + err = archiver.Archive(allTheLayers, tarPath) + if err != nil { + return err + } + return nil +} + +func fetchHTTP(ctx context.Context, src, tarPath, shasum string) error { + if !config.CommonOptions.Insecure && shasum == "" { + return errors.New("remote package provided without shasum while insecure is not enabled") + } + f, err := os.Create(tarPath) + if err != nil { + return err + } + defer f.Close() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, src, nil) + if err != nil { + return err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + _, err := io.Copy(io.Discard, resp.Body) + if err != nil { + return err + } + return fmt.Errorf("unexpected http response status code %s for source %s", resp.Status, src) + } + _, err = io.Copy(f, resp.Body) + if err != nil { + return err + } + // Check checksum if src included one. + if shasum != "" { + received, err := helpers.GetSHA256OfFile(tarPath) + if err != nil { + return err + } + if received != shasum { + return fmt.Errorf("shasum mismatch for file %s, expected %s bu got %s ", tarPath, shasum, received) + } + } + return nil +}