...

Source file src/edge-infra.dev/pkg/f8n/warehouse/lift/cmd/pack/pack.go

Documentation: edge-infra.dev/pkg/f8n/warehouse/lift/cmd/pack

     1  package pack
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/fs"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"sigs.k8s.io/kustomize/kyaml/filesys"
    11  
    12  	"edge-infra.dev/pkg/f8n/warehouse/lift"
    13  	"edge-infra.dev/pkg/f8n/warehouse/lift/cmd/internal"
    14  	"edge-infra.dev/pkg/f8n/warehouse/lift/pack/types"
    15  	"edge-infra.dev/pkg/f8n/warehouse/oci/layout"
    16  	"edge-infra.dev/pkg/f8n/warehouse/oci/name"
    17  	ociremote "edge-infra.dev/pkg/f8n/warehouse/oci/remote"
    18  	"edge-infra.dev/pkg/f8n/warehouse/pallet/resolve"
    19  	"edge-infra.dev/pkg/lib/cli/rags"
    20  	"edge-infra.dev/pkg/lib/cli/sink"
    21  )
    22  
    23  func New(cfg lift.Config) *sink.Command {
    24  	var (
    25  		packer = internal.NewPacker(cfg)
    26  		push   bool
    27  	)
    28  
    29  	cmd := &sink.Command{
    30  		Use:   "pack [flags] <package paths>...",
    31  		Short: "build and publish Warehouse packages",
    32  		Flags: []*rags.Rag{
    33  			{
    34  				Name:  "push",
    35  				Usage: "push built pallets to configured Warehouse registry",
    36  				Value: &rags.Bool{Var: &push},
    37  				Short: "p",
    38  			},
    39  		},
    40  		Extensions: []sink.Extension{packer},
    41  		Exec: func(_ context.Context, r sink.Run) error {
    42  			if len(r.Args()) == 0 {
    43  				return fmt.Errorf("error: at least one argument is required")
    44  			}
    45  
    46  			if push && cfg.Repo == "" {
    47  				return fmt.Errorf("error: oci repo is required for pushing\n" +
    48  					"set one via the --warehouse-oci-repo flag or setting the " +
    49  					"WAREHOUSE_OCI_REPO environment variable")
    50  			}
    51  
    52  			l, err := layout.New(cfg.Cache)
    53  			if err != nil {
    54  				return err
    55  			}
    56  
    57  			log := r.Log
    58  			for _, a := range r.Args() {
    59  				pkgPaths, err := resolvePalletPaths(a, packer.FS)
    60  				if err != nil {
    61  					return err
    62  				}
    63  				for _, path := range pkgPaths {
    64  					artifact, err := packer.Pack(path)
    65  					if err != nil {
    66  						return err
    67  					}
    68  					d, err := artifact.Digest()
    69  					if err != nil {
    70  						return err
    71  					}
    72  					l := log.WithValues(
    73  						"sha256", d.Hex[:9], "tag", packer.Tags,
    74  					)
    75  
    76  					// TODO(aw185176): This is inefficient for purposes of printing what
    77  					// pallets were actually packed, because we end up traversing the same
    78  					// packages multiple times for many builds.
    79  					plts, err := resolve.Resolve(artifact)
    80  					if err != nil {
    81  						return err
    82  					}
    83  					if len(plts) > 1 {
    84  						deps := make([]string, 0, len(plts))
    85  						for k, v := range plts {
    86  							if v.Name() == artifact.Name() {
    87  								continue
    88  							}
    89  							d, err := v.Digest()
    90  							if err != nil {
    91  								return fmt.Errorf("failed to get digest for %s: %w", k, err)
    92  							}
    93  							deps = append(deps, fmt.Sprintf("%s@%s", k, d.String()[:16]))
    94  						}
    95  						l = l.WithValues("deps", deps)
    96  					}
    97  					l.Info("built " + artifact.Name())
    98  				}
    99  			}
   100  
   101  			// TODO(aw185176): anything built outside of `lift pack` doesn't land in the
   102  			// cache
   103  			for _, p := range packer.Artifacts() {
   104  				for _, t := range packer.Tags {
   105  					ref, err := name.Tag(fmt.Sprintf("%s:%s", p.Name(), t))
   106  					if err != nil {
   107  						return fmt.Errorf("failed to parse tag for %s: %w", p.Name(), err)
   108  					}
   109  					if err := l.Append(ref, p); err != nil {
   110  						return fmt.Errorf("failed to write pallet to cache: %v", err)
   111  					}
   112  				}
   113  				if push {
   114  					d, err := p.Digest()
   115  					if err != nil {
   116  						return fmt.Errorf("failed to parse digest for %s: %w", p.Name(), err)
   117  					}
   118  					ref, err := name.Digest(fmt.Sprintf("%s/%s@%s", cfg.Repo, p.Name(), d))
   119  					if err != nil {
   120  						return fmt.Errorf("failed to create reference for %s: %w", p.Name(), err)
   121  					}
   122  					// create extra refs for tags and push them all concurrently
   123  					refs := ociremote.Map{ref: p}
   124  					for _, t := range packer.Tags {
   125  						tag, err := name.Tag(fmt.Sprintf("%s/%s:%s", cfg.Repo, p.Name(), t))
   126  						if err != nil {
   127  							return fmt.Errorf("failed to create tag for %s: %w", p.Name(), err)
   128  						}
   129  						refs[tag] = p
   130  					}
   131  					log.Info("pushing", "pkg", p.Name(), "dst", ref)
   132  					if err := ociremote.MultiWrite(refs); err != nil {
   133  						return fmt.Errorf("error: failed to push %s: %w", ref, err)
   134  					}
   135  				}
   136  			}
   137  
   138  			// Sort layout index manifest to improve reproducibility
   139  			return l.Sort()
   140  		},
   141  	}
   142  
   143  	return cmd
   144  }
   145  
   146  func resolvePalletPaths(path string, fsys filesys.FileSystem) ([]string, error) {
   147  	var pkgPaths []string
   148  	switch {
   149  	// if path ends with "pallet.yaml" or "pallet.yml", return immediately
   150  	case isWarehouseFile(path):
   151  		return []string{filepath.Dir(path)}, nil
   152  	// if path ends with "...", walk the path and keep all package paths found
   153  	case strings.HasSuffix(path, "/..."):
   154  		path = strings.TrimSuffix(path, "/...")
   155  		if err := fsys.Walk(path, func(wPath string, info fs.FileInfo, err error) error {
   156  			var buildPath string
   157  			switch {
   158  			case err != nil:
   159  				return fmt.Errorf("failed to search for Pallet files (%s): %w",
   160  					types.Files, err)
   161  			case info.IsDir():
   162  				if info.Name() == "testdata" || info.Name() == "test" {
   163  					return fs.SkipDir
   164  				}
   165  				return nil
   166  			case isWarehouseFile(info.Name()):
   167  				buildPath = filepath.Dir(wPath)
   168  			}
   169  			if buildPath != "" {
   170  				pkgPaths = append(pkgPaths, buildPath)
   171  			}
   172  			return nil
   173  		}); err != nil {
   174  			return nil, err
   175  		}
   176  
   177  		if len(pkgPaths) == 0 {
   178  			return nil, fmt.Errorf("no packages found for path %s", path)
   179  		}
   180  		return pkgPaths, nil
   181  	default:
   182  		return []string{path}, nil
   183  	}
   184  }
   185  
   186  func isWarehouseFile(path string) bool {
   187  	_, fileName := filepath.Split(path)
   188  	for _, file := range types.Files {
   189  		if fileName == file {
   190  			return true
   191  		}
   192  	}
   193  	return false
   194  }
   195  

View as plain text