|
1 | 1 | package composectl |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "encoding/json" |
4 | 5 | "fmt" |
| 6 | + "os" |
| 7 | + |
5 | 8 | "github.com/containerd/containerd/platforms" |
6 | 9 | "github.com/foundriesio/composeapp/pkg/compose" |
7 | | - "github.com/foundriesio/composeapp/pkg/compose/v1" |
| 10 | + v1 "github.com/foundriesio/composeapp/pkg/compose/v1" |
8 | 11 | "github.com/spf13/cobra" |
9 | 12 | ) |
10 | 13 |
|
| 14 | +type inspectOptions struct { |
| 15 | + Format string |
| 16 | +} |
| 17 | + |
11 | 18 | func init() { |
12 | 19 | inspectCmd := &cobra.Command{ |
13 | 20 | Use: "inspect <app ref>", |
14 | 21 | Short: "inspect <ref>", |
15 | 22 | Long: ``, |
16 | 23 | Args: cobra.ExactArgs(1), |
17 | | - Run: inspectApp, |
| 24 | + } |
| 25 | + opts := inspectOptions{} |
| 26 | + inspectCmd.Flags().StringVar(&opts.Format, "format", "plain", |
| 27 | + "Output format; supported: plain, json") |
| 28 | + inspectCmd.Run = func(cmd *cobra.Command, args []string) { |
| 29 | + if opts.Format != "plain" && opts.Format != "json" { |
| 30 | + DieNotNil(cmd.Usage()) |
| 31 | + fmt.Fprintf(os.Stderr, "unsupported `--format` value: %s\n", opts.Format) |
| 32 | + os.Exit(1) |
| 33 | + } |
| 34 | + inspectApp(cmd, args, &opts) |
18 | 35 | } |
19 | 36 | rootCmd.AddCommand(inspectCmd) |
20 | 37 | } |
21 | 38 |
|
22 | | -func inspectApp(cmd *cobra.Command, args []string) { |
| 39 | +func inspectApp(cmd *cobra.Command, args []string, opts *inspectOptions) { |
23 | 40 | appRef := args[0] |
24 | 41 |
|
25 | | - fmt.Printf("Inspecting App %s...", appRef) |
| 42 | + if opts.Format == "plain" { |
| 43 | + fmt.Printf("Inspecting App %s...", appRef) |
| 44 | + } |
26 | 45 | app, err := v1.NewAppLoader().LoadAppTree(cmd.Context(), compose.NewRemoteBlobProviderFromConfig(config), platforms.All, appRef) |
27 | 46 | DieNotNil(err) |
28 | | - fmt.Println("ok") |
29 | | - app.Tree().Print() |
30 | | - fmt.Printf("App tree node count: %d\n", app.NodeCount()) |
| 47 | + if opts.Format == "plain" { |
| 48 | + fmt.Println("ok") |
| 49 | + app.Tree().Print() |
| 50 | + } else { |
| 51 | + // json |
| 52 | + type BlobInfo struct { |
| 53 | + Digest string `json:"digest"` |
| 54 | + Size int64 `json:"size"` |
| 55 | + } |
| 56 | + |
| 57 | + type ManifestInfo struct { |
| 58 | + BlobInfo |
| 59 | + Architecture string `json:"architecture,omitempty"` |
| 60 | + Config BlobInfo `json:"config"` |
| 61 | + Layers []BlobInfo `json:"layers"` |
| 62 | + } |
| 63 | + |
| 64 | + type ImageContentInfo struct { |
| 65 | + Ref string `json:"ref"` |
| 66 | + Manifests []ManifestInfo `json:"manifests"` |
| 67 | + } |
| 68 | + |
| 69 | + type ServiceContentInfo struct { |
| 70 | + Name string `json:"name"` |
| 71 | + Image ImageContentInfo `json:"image"` |
| 72 | + } |
| 73 | + |
| 74 | + type BundleInfo struct { |
| 75 | + BlobInfo |
| 76 | + Services []ServiceContentInfo `json:"services,omitempty"` |
| 77 | + } |
| 78 | + |
| 79 | + type AppInfo struct { |
| 80 | + Name string `json:"name"` |
| 81 | + Ref string `json:"ref"` |
| 82 | + Meta BlobInfo `json:"meta"` |
| 83 | + Index BlobInfo `json:"index"` |
| 84 | + Bundle BundleInfo `json:"bundle"` |
| 85 | + } |
| 86 | + |
| 87 | + currApp := AppInfo{ |
| 88 | + Name: app.Name(), |
| 89 | + Ref: app.Ref().Spec.String(), |
| 90 | + } |
| 91 | + currApp.Bundle = BundleInfo{ |
| 92 | + Services: []ServiceContentInfo{}, |
| 93 | + } |
| 94 | + |
| 95 | + err = app.Tree().Walk(func(node *compose.TreeNode, depth int) error { |
| 96 | + if depth == 1 { |
| 97 | + switch node.Type { |
| 98 | + case compose.BlobTypeAppLayersMeta: |
| 99 | + currApp.Meta = BlobInfo{ |
| 100 | + Digest: node.Descriptor.Digest.String(), |
| 101 | + Size: node.Descriptor.Size, |
| 102 | + } |
| 103 | + case compose.BlobTypeAppIndex: |
| 104 | + currApp.Index = BlobInfo{ |
| 105 | + Digest: node.Descriptor.Digest.String(), |
| 106 | + Size: node.Descriptor.Size, |
| 107 | + } |
| 108 | + case compose.BlobTypeAppBundle: |
| 109 | + currApp.Bundle = BundleInfo{ |
| 110 | + BlobInfo: BlobInfo{ |
| 111 | + Digest: node.Descriptor.Digest.String(), |
| 112 | + Size: node.Descriptor.Size, |
| 113 | + }, |
| 114 | + Services: []ServiceContentInfo{}, |
| 115 | + } |
| 116 | + } |
| 117 | + } else if depth == 2 { |
| 118 | + var manifests []ManifestInfo |
| 119 | + if node.Type == compose.BlobTypeImageManifest { |
| 120 | + manifestInfo := ManifestInfo{ |
| 121 | + BlobInfo: BlobInfo{ |
| 122 | + Digest: node.Descriptor.Digest.String(), |
| 123 | + Size: node.Descriptor.Size, |
| 124 | + }, |
| 125 | + // If an image manifest is found at depth 2, |
| 126 | + // then there is no platform information available, as the manifest is not part of an image index. |
| 127 | + Architecture: "unknown", |
| 128 | + } |
| 129 | + manifests = append(manifests, manifestInfo) |
| 130 | + } |
| 131 | + serviceInfo := ServiceContentInfo{ |
| 132 | + Name: node.Descriptor.Annotations[v1.AnnotationKeyAppServiceName], |
| 133 | + Image: ImageContentInfo{ |
| 134 | + Ref: node.Descriptor.URLs[0], |
| 135 | + Manifests: manifests, |
| 136 | + }, |
| 137 | + } |
| 138 | + currApp.Bundle.Services = append(currApp.Bundle.Services, serviceInfo) |
| 139 | + } else if depth == 3 && node.Type == compose.BlobTypeImageManifest { |
| 140 | + manifestInfo := ManifestInfo{ |
| 141 | + BlobInfo: BlobInfo{ |
| 142 | + Digest: node.Descriptor.Digest.String(), |
| 143 | + Size: node.Descriptor.Size, |
| 144 | + }, |
| 145 | + Architecture: node.Descriptor.Platform.Architecture, |
| 146 | + } |
| 147 | + lastIndex := &currApp.Bundle.Services[len(currApp.Bundle.Services)-1] |
| 148 | + lastIndex.Image.Manifests = append(lastIndex.Image.Manifests, manifestInfo) |
| 149 | + } else if depth == 4 || depth == 3 { |
| 150 | + switch node.Type { |
| 151 | + case compose.BlobTypeImageConfig: |
| 152 | + lastIndex := &currApp.Bundle.Services[len(currApp.Bundle.Services)-1] |
| 153 | + lastIndex.Image.Manifests[len(lastIndex.Image.Manifests)-1].Config = BlobInfo{ |
| 154 | + Digest: node.Descriptor.Digest.String(), |
| 155 | + Size: node.Descriptor.Size, |
| 156 | + } |
| 157 | + case compose.BlobTypeImageLayer: |
| 158 | + lastIndex := &currApp.Bundle.Services[len(currApp.Bundle.Services)-1] |
| 159 | + lastIndex.Image.Manifests[len(lastIndex.Image.Manifests)-1].Layers = append(lastIndex.Image.Manifests[len(lastIndex.Image.Manifests)-1].Layers, BlobInfo{ |
| 160 | + Digest: node.Descriptor.Digest.String(), |
| 161 | + Size: node.Descriptor.Size, |
| 162 | + }) |
| 163 | + } |
| 164 | + } |
| 165 | + return nil |
| 166 | + }) |
| 167 | + DieNotNil(err) |
| 168 | + |
| 169 | + b, err := json.Marshal(currApp) |
| 170 | + DieNotNil(err) |
| 171 | + fmt.Println(string(b)) |
| 172 | + } |
| 173 | + if opts.Format == "plain" { |
| 174 | + fmt.Printf("App tree node count: %d\n", app.NodeCount()) |
| 175 | + } |
31 | 176 | } |
0 commit comments