// Package unpack allows for unpacking OCI artifacts, rendering the contents using // supported variables, and applying the artifacts to a K8s cluster. package unpack import ( "fmt" v1 "github.com/google/go-containerregistry/pkg/v1" "edge-infra.dev/pkg/f8n/warehouse/cluster" "edge-infra.dev/pkg/f8n/warehouse/oci" "edge-infra.dev/pkg/f8n/warehouse/oci/layer" "edge-infra.dev/pkg/f8n/warehouse/oci/walk" "edge-infra.dev/pkg/f8n/warehouse/pallet" ) // Fn is called for each visited package in Walk() with a Pallet instantiated // from the root package node and the unpacked layers for that Pallet. type Fn func(p pallet.Pallet, layers []layer.Layer) error // Walk unpacks the input Artifact a and calls fn for each valid node in the // traversal, based on the input options. The caller is passed the result of // unpacking and processing each visited node via the unpacking function fn. func Walk(a oci.Artifact, fn Fn, opts ...Option) error { options, err := makeOptions(opts...) if err != nil { return err } // short circuit if input is standalone image or backed by one switch a := a.(type) { // if artifact is not backed by v1.Image, options.provider must be set to // non-empty value case oci.Unwrapper: if img, ok := a.Unwrap().(v1.Image); ok { return unpackImg(img, fn, options) } // if its a v1.Image straight up, short-circuit directly case v1.Image: return unpackImg(a, fn, options) } // we now know we are dealing with something like v1.ImageIndex // so options.provider is required if options.provider == "" { return cluster.ErrNoProviders } // validate passed provider against artifact providers p, err := pallet.New(a) if err != nil { return err } if err := p.Providers().IsValid(options.provider); err != nil { return err } // create walker for indexes visited := map[string]v1.Hash{} // so we only call fn once per package walker := &walk.Fns{ Image: func(img v1.Image, parent v1.ImageIndex) error { var ( p pallet.Pallet err error ) isPkg, err := oci.IsPackage(img, parent) if !isPkg || err != nil { p, err = pallet.New(parent) if err != nil { return err } } else { p, err = pallet.New(img) if err != nil { return err } } // filter variants other than the one we are specifically looking for. // we know options.provider is present because its required for all // non-Image inputs if !p.Supports(options.provider) { return nil } // calculate digest because we need it for either comparison against // visited nodes or to mark a visited node digest, err := p.Digest() if err != nil { return err } if visited[p.Name()] != (v1.Hash{}) { if visited[p.Name()] != digest { return fmt.Errorf("unpack.Walk: %w", oci.NewConflictErr( p.Name(), visited[p.Name()], digest, )) } // return early if we have already visited this package and there is not // a conflict return nil } layers, err := unpackPallet(p, *options) if err != nil { return err } // mark node as visited before we invoke the unpacking // function visited[p.Name()] = digest return fn(p, layers) }, } return walk.Walk(a, walker) } // unpackImg is a helper when the artifact is a single image func unpackImg(img v1.Image, fn Fn, opts *options) error { p, err := pallet.New(img) if err != nil { return err } layers, err := unpackPallet(p, *opts) if err != nil { return err } return fn(p, layers) }