...

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

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

     1  package graph
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"strings"
     9  
    10  	v1 "github.com/google/go-containerregistry/pkg/v1"
    11  	"github.com/google/go-containerregistry/pkg/v1/types"
    12  	"oss.terrastruct.com/d2/d2format"
    13  	"sigs.k8s.io/kustomize/api/resource"
    14  
    15  	wh "edge-infra.dev/pkg/f8n/warehouse"
    16  	"edge-infra.dev/pkg/f8n/warehouse/lift"
    17  	"edge-infra.dev/pkg/f8n/warehouse/lift/cmd/internal"
    18  	"edge-infra.dev/pkg/f8n/warehouse/oci"
    19  	"edge-infra.dev/pkg/f8n/warehouse/oci/layer"
    20  	whname "edge-infra.dev/pkg/f8n/warehouse/oci/name"
    21  	"edge-infra.dev/pkg/f8n/warehouse/oci/walk"
    22  	"edge-infra.dev/pkg/lib/cli/rags"
    23  	"edge-infra.dev/pkg/lib/cli/sink"
    24  )
    25  
    26  const (
    27  	outputDot  = "dot"
    28  	outputD2   = "d2"
    29  	outputTree = "tree"
    30  )
    31  
    32  var outputFmts = []string{outputTree, outputDot, outputD2}
    33  
    34  func New(cfg lift.Config) *sink.Command {
    35  	var (
    36  		packer         = internal.NewPacker(cfg)
    37  		outputType     string
    38  		providerLabels bool
    39  		depth          int
    40  		outpath        string
    41  	)
    42  
    43  	cmd := &sink.Command{
    44  		Use:   "graph [flags] <artifact>",
    45  		Short: "render a graph for a Warehouse artifact in multiple formats",
    46  		Flags: []*rags.Rag{
    47  			{
    48  				Name:  "type",
    49  				Short: "t",
    50  				Usage: fmt.Sprintf("output format. one of: %s", outputFmts),
    51  				Value: &rags.StringEnum{
    52  					Var:     &outputType,
    53  					Default: outputTree,
    54  					Valid:   []string{outputTree, outputDot, outputD2},
    55  				},
    56  			},
    57  			{
    58  				Name:  "add-provider-labels",
    59  				Usage: "add labels to edges for provider variants. only applicable for --depth=1 or higher.",
    60  				Value: &rags.Bool{Var: &providerLabels},
    61  			},
    62  			{
    63  				Name:  "depth",
    64  				Short: "d",
    65  				Usage: "depth to draw the graph. only renders package nodes by default, increasing this value will render provider variant images, layers, and layer contents in that order.",
    66  				Value: &rags.Int{Var: &depth},
    67  			},
    68  			{
    69  				Name:  "out",
    70  				Short: "o",
    71  				Usage: "write output to file instead of stdout",
    72  				Value: &rags.String{Var: &outpath},
    73  			},
    74  		},
    75  		Extensions: []sink.Extension{packer},
    76  		Exec: func(ctx context.Context, r sink.Run) error {
    77  			switch len(r.Args()) {
    78  			case 0:
    79  				return fmt.Errorf("error: must provide an artifact")
    80  			case 1:
    81  			default:
    82  				return fmt.Errorf("error: only one artifact can be provided")
    83  			}
    84  
    85  			if depth == 0 {
    86  				f := false
    87  				providerLabels = f
    88  			}
    89  
    90  			a, err := internal.ResolveArtifact(packer, r.Args()[0])
    91  			if err != nil {
    92  				return err
    93  			}
    94  			g, err := buildGraph(a, depth)
    95  			if err != nil {
    96  				return err
    97  			}
    98  
    99  			var out string
   100  			switch outputType {
   101  			case outputD2:
   102  				d2, err := g.toD2(ctx, providerLabels)
   103  				if err != nil {
   104  					return err
   105  				}
   106  				out = d2format.Format(d2.g.AST)
   107  			case outputTree:
   108  				tree, err := g.toTree(providerLabels)
   109  				if err != nil {
   110  					return err
   111  				}
   112  				out = tree.String()
   113  			case outputDot:
   114  				dot, err := g.toDot()
   115  				if err != nil {
   116  					return err
   117  				}
   118  				out = dot.String()
   119  			}
   120  
   121  			if outpath != "" {
   122  				return os.WriteFile(outpath, []byte(out), 0600)
   123  			}
   124  			fmt.Fprintln(r.Out(), out)
   125  
   126  			return nil
   127  		},
   128  	}
   129  	return cmd
   130  }
   131  
   132  type graph struct {
   133  	nodes map[string]*node
   134  	keys  []string // ordered list of visited keys so that we produce consistent results
   135  }
   136  
   137  const (
   138  	idxNode     = "idx"
   139  	imgNode     = "img"
   140  	layerNode   = "layer"
   141  	objectsNode = "objects"
   142  )
   143  
   144  type node struct {
   145  	nodeType string
   146  	name     string
   147  	data     string // node types may optionally take in extra data
   148  	digest   v1.Hash
   149  	labels   map[string]string
   150  	deps     []*node
   151  }
   152  
   153  func (n *node) label() string {
   154  	return fmt.Sprintf("%s@sha256:%s", n.name, n.digest.Hex[:7])
   155  }
   156  
   157  func buildGraph(a oci.Artifact, depth int) (*graph, error) {
   158  	g := &graph{nodes: make(map[string]*node)}
   159  	visited := make(map[v1.Hash]bool)
   160  
   161  	walker := &walk.Fns{
   162  		Index: func(idx v1.ImageIndex, _ v1.ImageIndex) error {
   163  			n, err := nodeFromArtifact(g, idx)
   164  			if err != nil {
   165  				return err
   166  			}
   167  			if visited[n.digest] {
   168  				return nil
   169  			}
   170  
   171  			m, err := idx.IndexManifest()
   172  			if err != nil {
   173  				return err
   174  			}
   175  			for _, manifest := range m.Manifests {
   176  				if depth == 0 && descRefsIdx(m, manifest) {
   177  					continue
   178  				}
   179  				child := g.nodes[manifest.Digest.Hex]
   180  				if child == nil {
   181  					child = &node{
   182  						nodeType: nodeTypeFromMediaType(manifest.MediaType),
   183  						digest:   manifest.Digest,
   184  						name:     manifest.Annotations[wh.AnnotationRefName],
   185  					}
   186  					g.nodes[manifest.Digest.Hex] = child
   187  					g.keys = append(g.keys, manifest.Digest.Hex)
   188  				}
   189  				child.labels = map[string]string{
   190  					"providers": manifest.Annotations[wh.AnnotationClusterProviders],
   191  				}
   192  				n.deps = append(n.deps, child)
   193  			}
   194  			visited[n.digest] = true
   195  
   196  			return nil
   197  		},
   198  		Image: func(img v1.Image, parent v1.ImageIndex) error {
   199  			isPkg, err := oci.IsPackage(img, parent)
   200  			switch {
   201  			case err != nil:
   202  				return err
   203  			case !isPkg && depth < 2:
   204  				return nil
   205  			}
   206  
   207  			n, err := nodeFromArtifact(g, img)
   208  			if err != nil {
   209  				return err
   210  			}
   211  			if visited[n.digest] {
   212  				return nil
   213  			}
   214  
   215  			layers, err := layer.FromImage(img)
   216  			if err != nil {
   217  				return err
   218  			}
   219  			for _, l := range layers {
   220  				digest, err := l.Digest()
   221  				if err != nil {
   222  					return err
   223  				}
   224  				annos := l.Annotations()
   225  
   226  				child := g.nodes[digest.Hex]
   227  				if child == nil {
   228  					name := annos[wh.AnnotationLayerType]
   229  					switch {
   230  					case annos[wh.AnnotationLayerRuntimeCapability] != "":
   231  						name = fmt.Sprintf("%s/%s", name,
   232  							annos[wh.AnnotationLayerRuntimeCapability])
   233  					case name == layer.Infra.String():
   234  						name = name[:5]
   235  					}
   236  
   237  					child = &node{
   238  						nodeType: layerNode,
   239  						digest:   digest,
   240  						name:     name,
   241  					}
   242  					g.nodes[digest.Hex] = child
   243  					g.keys = append(g.keys, digest.Hex)
   244  				}
   245  				n.deps = append(n.deps, child)
   246  
   247  				if depth < 3 {
   248  					continue
   249  				}
   250  
   251  				objsChild := g.nodes[digest.Hex+"-objs"]
   252  				if objsChild == nil {
   253  					objsChild = &node{
   254  						nodeType: objectsNode,
   255  						digest:   digest,
   256  						name:     child.name,
   257  					}
   258  
   259  					reader, err := l.Uncompressed()
   260  					if err != nil {
   261  						return err
   262  					}
   263  					content, err := io.ReadAll(reader)
   264  					if err != nil {
   265  						return err
   266  					}
   267  					f := resource.Factory{}
   268  					resources, err := f.SliceFromBytes(content)
   269  					if err != nil {
   270  						return err
   271  					}
   272  					objIDs := make([]string, len(resources))
   273  					for i, r := range resources {
   274  						objIDs[i] = fmt.Sprintf("%s/%s", r.GetKind(), r.GetName())
   275  					}
   276  					objsChild.data = strings.Join(objIDs, "\n")
   277  
   278  					g.nodes[digest.Hex+"-objs"] = objsChild
   279  					child.deps = append(child.deps, objsChild)
   280  				}
   281  			}
   282  			visited[n.digest] = true
   283  			return nil
   284  		},
   285  	}
   286  
   287  	if err := walk.Walk(a, walker); err != nil {
   288  		return nil, err
   289  	}
   290  
   291  	return g, nil
   292  }
   293  
   294  func nodeTypeFromMediaType(mt types.MediaType) string {
   295  	switch {
   296  	case mt.IsIndex():
   297  		return idxNode
   298  	case mt.IsImage():
   299  		return imgNode
   300  	case mt.IsLayer():
   301  		return layerNode
   302  	default:
   303  		return ""
   304  	}
   305  }
   306  
   307  func nodeFromArtifact(g *graph, a oci.Artifact) (*node, error) {
   308  	digest, err := a.Digest()
   309  	if err != nil {
   310  		return nil, err
   311  	}
   312  	if g.nodes[digest.Hex] != nil {
   313  		return g.nodes[digest.Hex], nil
   314  	}
   315  
   316  	mt, err := a.MediaType()
   317  	if err != nil {
   318  		return nil, err
   319  	}
   320  
   321  	name, err := whname.FromArtifact(a)
   322  	if err != nil {
   323  		return nil, err
   324  	}
   325  
   326  	n := &node{
   327  		nodeType: nodeTypeFromMediaType(mt),
   328  		digest:   digest,
   329  		name:     name,
   330  	}
   331  	g.nodes[digest.Hex] = n
   332  	g.keys = append(g.keys, digest.Hex)
   333  	return n, nil
   334  }
   335  
   336  // descRefsIdx returns true if the input descriptor is referencing the input
   337  // image manifest, e.g., provider variants of a package
   338  func descRefsIdx(idx *v1.IndexManifest, d v1.Descriptor) bool {
   339  	return idx.Annotations[wh.AnnotationName] == d.Annotations[wh.AnnotationRefName]
   340  }
   341  

View as plain text