package inspect import ( "bytes" "context" "encoding/json" "fmt" "strings" "text/tabwriter" "time" v1 "github.com/google/go-containerregistry/pkg/v1" "edge-infra.dev/pkg/f8n/warehouse" wh "edge-infra.dev/pkg/f8n/warehouse" "edge-infra.dev/pkg/f8n/warehouse/lift" "edge-infra.dev/pkg/f8n/warehouse/lift/cmd/internal" "edge-infra.dev/pkg/f8n/warehouse/oci" "edge-infra.dev/pkg/f8n/warehouse/oci/layout" "edge-infra.dev/pkg/f8n/warehouse/oci/name" "edge-infra.dev/pkg/f8n/warehouse/pallet" "edge-infra.dev/pkg/f8n/warehouse/pallet/resolve" "edge-infra.dev/pkg/lib/cli/rags" "edge-infra.dev/pkg/lib/cli/sink" ) func New(cfg lift.Config) *sink.Command { var ( packer = internal.NewPacker(cfg) manifest bool parameters bool ) cmd := &sink.Command{ Use: "inspect [flags] [package]", Short: "introspect Warehouse packages", Extensions: []sink.Extension{packer}, Flags: []*rags.Rag{ { Name: "manifest", Short: "m", Usage: "print JSON manifest for package. mutually exclusive from --parameters", Value: &rags.Bool{Var: &manifest}, }, { Name: "parameters", Short: "p", Usage: "print required rendering parameters for package. mutually exclusive from --manifest", Value: &rags.Bool{Var: ¶meters}, }, }, Exec: func(_ context.Context, r sink.Run) error { l, err := layout.New(cfg.Cache) if err != nil { return err } idx, err := l.ImageIndex() if err != nil { return err } if len(r.Args()) == 0 { if manifest { return fmt.Errorf("--manifest is invalid when listing all cached packages") } if parameters { return fmt.Errorf("--parameters is invalid when listing all cached packages") } return listAll(tabwriter.NewWriter(r.Out(), 5, 2, 2, ' ', 0), idx) } artifact, err := internal.ResolveArtifact(packer, r.Args()[0]) if err != nil { return err } if manifest { out, err := printRaw(artifact, r.Args()[0]) if err != nil { return err } fmt.Println(out) return nil } for _, p := range packer.Artifacts() { for _, t := range packer.Tags { ref, err := name.Tag(fmt.Sprintf("%s:%s", p.Name(), t)) if err != nil { return fmt.Errorf("failed to parse tag for %s: %w", p.Name(), err) } if err := l.Append(ref, p); err != nil { return fmt.Errorf("failed to write pallet to cache: %v", err) } } } p, err := pallet.New(artifact) if err != nil { return fmt.Errorf("failed to create pallet from artifact: %w", err) } if parameters { out := outputParams(p) fmt.Fprintln(r.Out(), out) return nil } // TODO: try to update this so that it works with the CLI logger (???) w := tabwriter.NewWriter(r.Err(), 5, 2, 2, ' ', 0) return writePlainOutput(w, p) }, } return cmd } func printRaw(artifact oci.Artifact, ref string) (string, error) { raw, err := artifact.RawManifest() if err != nil { return "", fmt.Errorf("failed to read manifest for %s: %w", ref, err) } out := &bytes.Buffer{} if err := json.Indent(out, raw, "", " "); err != nil { return "", err } return out.String(), nil } func parseTime(created string) (time.Time, error) { createdTime, err := time.Parse(time.RFC3339, created) if err != nil { return time.Time{}, fmt.Errorf("failed to parse time: %w", err) } return createdTime, nil } func listAll(w *tabwriter.Writer, idx v1.ImageIndex) error { manifest, err := idx.IndexManifest() if err != nil { return fmt.Errorf("failed to read warehouse index: %w", err) } fmt.Fprintln(w, "TAG\tDIGEST\tTEAM\tREVISION\tCREATED\t") for _, m := range manifest.Manifests { // Retrieve the annotations from the image pointed to by index.json a, err := oci.ArtifactFromIdx(idx, m) if err != nil { // TODO: better error return fmt.Errorf("failed to load artifact: %w", err) } p, err := pallet.New(a) if err != nil { return fmt.Errorf("failed to create pallet from artifact: %w", err) } // Calculate time elapsed since creation createdTime, err := parseTime(p.Metadata().Created) if err != nil { return err } creationRaw := time.Since(createdTime) var creation string switch { case creationRaw.Seconds() < 60: creation = fmt.Sprint(int(creationRaw.Seconds())) + " seconds ago" case creationRaw.Minutes() < 60: creation = fmt.Sprint(int(creationRaw.Minutes())) + " minutes ago" case creationRaw.Hours() > 1 && creationRaw.Hours() <= 24: creation = fmt.Sprint(int(creationRaw.Hours())) + " hours ago" case creationRaw.Hours() > 24 && creationRaw.Hours() <= 168: creation = fmt.Sprint(int(creationRaw.Hours()/24)) + " days ago" case creationRaw.Hours() > 168 && creationRaw.Hours() <= 672: creation = fmt.Sprint(int(creationRaw.Hours()/168)) + " weeks ago" case creationRaw.Hours() > 672 && creationRaw.Hours() < 8064: creation = fmt.Sprint(int(creationRaw.Hours()/672)) + " months ago" default: creation = (createdTime.Format(time.RFC822Z))[0:9] } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t\n", m.Annotations[wh.AnnotationRefName], m.Digest.Hex[0:7], p.Metadata().Team, p.Metadata().Revision[0:7], creation, ) } if err := w.Flush(); err != nil { return fmt.Errorf("failed to render output: %w", err) } return nil } // TODO: this should consistently use the tabwriter w instead of mixing fmt.Printf // in throughout. tabwriter cells should be tab-terminated and we should leverage the // tab columns to organize all data consistently, e.g.: // // NAME lumper-controller // PARAMETERS cluster_hash // // cluster_provider // cluster_uuid // foreman_gcp_project // [...] // // PROVIDERS generic,dsds sha256:d34db33f // // gke sha256:f00b4r // // DEPENDENCIES external-secrets-operator sha256:[...] // // [...] [...] func writePlainOutput(w *tabwriter.Writer, p pallet.Pallet) error { digest, err := p.Digest() if err != nil { return fmt.Errorf("failed to fetch digest of %s: %w", p, err) } createdTime, err := parseTime(p.Metadata().Created) if err != nil { return err } created := (createdTime.Format(time.RFC1123))[5:17] out := outputParams(p) fmt.Printf("NAME:\t\t%s\n", p.Name()) fmt.Printf("PARAMETERS:\t%s\n", out) fmt.Printf("DIGEST:\t\t%s\n", digest.Hex) fmt.Printf("TEAM:\t\t%s\n", p.Metadata().Team) fmt.Printf("REVISION:\t%s\n", p.Metadata().Revision) fmt.Printf("CREATED:\t%s\n", created) pType, err := p.MediaType() if err != nil { return fmt.Errorf("failed to fetch MediaType of %s: %w", p.Name(), err) } // Check if pallet is a CompositePallet isComposite, err := oci.IsComposite(p) if err != nil { return err } switch { case isComposite: // If pallet is a CompositePallet, show underlying pallets fmt.Printf("PALLETS:\n") cp, err := p.RawManifest() if err != nil { return err } manifest := &v1.IndexManifest{} err = json.Unmarshal(cp, manifest) if err != nil { return err } for _, manifest := range manifest.Manifests { fmt.Printf("- %s@%v\n", manifest.Annotations[wh.AnnotationRefName], manifest.Digest) } case pType.IsImage(): providers, err := json.Marshal(p.Providers()[3:]) if err != nil { return fmt.Errorf("failed to marshal providers: %w", err) } fmt.Printf("PROVIDERS:\t%s\n", strings.Trim(strings.Replace(string(providers), "\"", "", -1), "[]")) case pType.IsIndex(): fmt.Printf("PROVIDERS:\n") //Fetch provider variants and digests from underlying artifact cp, err := p.RawManifest() if err != nil { return err } manifest := &v1.IndexManifest{} err = json.Unmarshal(cp, manifest) if err != nil { return err } for _, manifest := range manifest.Manifests { if manifest.Annotations[warehouse.AnnotationRefName] != p.Name() { continue } providers := manifest.Annotations[wh.AnnotationClusterProviders] variantDigest := manifest.Digest fmt.Fprintf(w, "- %s@%v\n", providers, variantDigest) } // Grab and display dependencies if applicable pDeps, err := resolve.Resolve(p) if err != nil { return err } fmt.Printf("DEPENDENCIES:\n") for depName, depDigest := range pDeps { if depName == p.Name() { continue } fmt.Fprintf(w, "- %s@%v\n", depName, depDigest) } } if err := w.Flush(); err != nil { return fmt.Errorf("failed to render output: %w", err) } return nil } func outputParams(p pallet.Pallet) string { var out string params := p.Parameters() switch { case params != nil: out = fmt.Sprint(params) default: out = "No required rendering parameters" } return out }