package pack import ( "context" "fmt" "io/fs" "path/filepath" "strings" "sigs.k8s.io/kustomize/kyaml/filesys" "edge-infra.dev/pkg/f8n/warehouse/lift" "edge-infra.dev/pkg/f8n/warehouse/lift/cmd/internal" "edge-infra.dev/pkg/f8n/warehouse/lift/pack/types" "edge-infra.dev/pkg/f8n/warehouse/oci/layout" "edge-infra.dev/pkg/f8n/warehouse/oci/name" ociremote "edge-infra.dev/pkg/f8n/warehouse/oci/remote" "edge-infra.dev/pkg/f8n/warehouse/pallet/resolve" "edge-infra.dev/pkg/lib/cli/rags" "edge-infra.dev/pkg/lib/cli/sink" ) func New(cfg lift.Config) *sink.Command { var ( packer = internal.NewPacker(cfg) push bool ) cmd := &sink.Command{ Use: "pack [flags] ...", Short: "build and publish Warehouse packages", Flags: []*rags.Rag{ { Name: "push", Usage: "push built pallets to configured Warehouse registry", Value: &rags.Bool{Var: &push}, Short: "p", }, }, Extensions: []sink.Extension{packer}, Exec: func(_ context.Context, r sink.Run) error { if len(r.Args()) == 0 { return fmt.Errorf("error: at least one argument is required") } if push && cfg.Repo == "" { return fmt.Errorf("error: oci repo is required for pushing\n" + "set one via the --warehouse-oci-repo flag or setting the " + "WAREHOUSE_OCI_REPO environment variable") } l, err := layout.New(cfg.Cache) if err != nil { return err } log := r.Log for _, a := range r.Args() { pkgPaths, err := resolvePalletPaths(a, packer.FS) if err != nil { return err } for _, path := range pkgPaths { artifact, err := packer.Pack(path) if err != nil { return err } d, err := artifact.Digest() if err != nil { return err } l := log.WithValues( "sha256", d.Hex[:9], "tag", packer.Tags, ) // TODO(aw185176): This is inefficient for purposes of printing what // pallets were actually packed, because we end up traversing the same // packages multiple times for many builds. plts, err := resolve.Resolve(artifact) if err != nil { return err } if len(plts) > 1 { deps := make([]string, 0, len(plts)) for k, v := range plts { if v.Name() == artifact.Name() { continue } d, err := v.Digest() if err != nil { return fmt.Errorf("failed to get digest for %s: %w", k, err) } deps = append(deps, fmt.Sprintf("%s@%s", k, d.String()[:16])) } l = l.WithValues("deps", deps) } l.Info("built " + artifact.Name()) } } // TODO(aw185176): anything built outside of `lift pack` doesn't land in the // cache for _, p := range packer.Artifacts() { for _, t := range packer.Tags { ref, err := name.Tag(fmt.Sprintf("%s:%s", p.Name(), t)) if err != nil { return fmt.Errorf("failed to parse tag for %s: %w", p.Name(), err) } if err := l.Append(ref, p); err != nil { return fmt.Errorf("failed to write pallet to cache: %v", err) } } if push { d, err := p.Digest() if err != nil { return fmt.Errorf("failed to parse digest for %s: %w", p.Name(), err) } ref, err := name.Digest(fmt.Sprintf("%s/%s@%s", cfg.Repo, p.Name(), d)) if err != nil { return fmt.Errorf("failed to create reference for %s: %w", p.Name(), err) } // create extra refs for tags and push them all concurrently refs := ociremote.Map{ref: p} for _, t := range packer.Tags { tag, err := name.Tag(fmt.Sprintf("%s/%s:%s", cfg.Repo, p.Name(), t)) if err != nil { return fmt.Errorf("failed to create tag for %s: %w", p.Name(), err) } refs[tag] = p } log.Info("pushing", "pkg", p.Name(), "dst", ref) if err := ociremote.MultiWrite(refs); err != nil { return fmt.Errorf("error: failed to push %s: %w", ref, err) } } } // Sort layout index manifest to improve reproducibility return l.Sort() }, } return cmd } func resolvePalletPaths(path string, fsys filesys.FileSystem) ([]string, error) { var pkgPaths []string switch { // if path ends with "pallet.yaml" or "pallet.yml", return immediately case isWarehouseFile(path): return []string{filepath.Dir(path)}, nil // if path ends with "...", walk the path and keep all package paths found case strings.HasSuffix(path, "/..."): path = strings.TrimSuffix(path, "/...") if err := fsys.Walk(path, func(wPath string, info fs.FileInfo, err error) error { var buildPath string switch { case err != nil: return fmt.Errorf("failed to search for Pallet files (%s): %w", types.Files, err) case info.IsDir(): if info.Name() == "testdata" || info.Name() == "test" { return fs.SkipDir } return nil case isWarehouseFile(info.Name()): buildPath = filepath.Dir(wPath) } if buildPath != "" { pkgPaths = append(pkgPaths, buildPath) } return nil }); err != nil { return nil, err } if len(pkgPaths) == 0 { return nil, fmt.Errorf("no packages found for path %s", path) } return pkgPaths, nil default: return []string{path}, nil } } func isWarehouseFile(path string) bool { _, fileName := filepath.Split(path) for _, file := range types.Files { if fileName == file { return true } } return false }