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
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
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
337
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