...

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

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

     1  package inspect
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"strings"
     9  	"text/tabwriter"
    10  	"time"
    11  
    12  	v1 "github.com/google/go-containerregistry/pkg/v1"
    13  
    14  	"edge-infra.dev/pkg/f8n/warehouse"
    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/layout"
    20  	"edge-infra.dev/pkg/f8n/warehouse/oci/name"
    21  	"edge-infra.dev/pkg/f8n/warehouse/pallet"
    22  	"edge-infra.dev/pkg/f8n/warehouse/pallet/resolve"
    23  	"edge-infra.dev/pkg/lib/cli/rags"
    24  	"edge-infra.dev/pkg/lib/cli/sink"
    25  )
    26  
    27  func New(cfg lift.Config) *sink.Command {
    28  	var (
    29  		packer     = internal.NewPacker(cfg)
    30  		manifest   bool
    31  		parameters bool
    32  	)
    33  
    34  	cmd := &sink.Command{
    35  		Use:        "inspect [flags] [package]",
    36  		Short:      "introspect Warehouse packages",
    37  		Extensions: []sink.Extension{packer},
    38  		Flags: []*rags.Rag{
    39  			{
    40  				Name:  "manifest",
    41  				Short: "m",
    42  				Usage: "print JSON manifest for package. mutually exclusive from --parameters",
    43  				Value: &rags.Bool{Var: &manifest},
    44  			},
    45  			{
    46  				Name:  "parameters",
    47  				Short: "p",
    48  				Usage: "print required rendering parameters for package. mutually exclusive from --manifest",
    49  				Value: &rags.Bool{Var: &parameters},
    50  			},
    51  		},
    52  		Exec: func(_ context.Context, r sink.Run) error {
    53  			l, err := layout.New(cfg.Cache)
    54  			if err != nil {
    55  				return err
    56  			}
    57  			idx, err := l.ImageIndex()
    58  			if err != nil {
    59  				return err
    60  			}
    61  
    62  			if len(r.Args()) == 0 {
    63  				if manifest {
    64  					return fmt.Errorf("--manifest is invalid when listing all cached packages")
    65  				}
    66  				if parameters {
    67  					return fmt.Errorf("--parameters is invalid when listing all cached packages")
    68  				}
    69  
    70  				return listAll(tabwriter.NewWriter(r.Out(), 5, 2, 2, ' ', 0), idx)
    71  			}
    72  
    73  			artifact, err := internal.ResolveArtifact(packer, r.Args()[0])
    74  			if err != nil {
    75  				return err
    76  			}
    77  
    78  			if manifest {
    79  				out, err := printRaw(artifact, r.Args()[0])
    80  				if err != nil {
    81  					return err
    82  				}
    83  				fmt.Println(out)
    84  				return nil
    85  			}
    86  			for _, p := range packer.Artifacts() {
    87  				for _, t := range packer.Tags {
    88  					ref, err := name.Tag(fmt.Sprintf("%s:%s", p.Name(), t))
    89  					if err != nil {
    90  						return fmt.Errorf("failed to parse tag for %s: %w", p.Name(), err)
    91  					}
    92  					if err := l.Append(ref, p); err != nil {
    93  						return fmt.Errorf("failed to write pallet to cache: %v", err)
    94  					}
    95  				}
    96  			}
    97  
    98  			p, err := pallet.New(artifact)
    99  			if err != nil {
   100  				return fmt.Errorf("failed to create pallet from artifact: %w", err)
   101  			}
   102  			if parameters {
   103  				out := outputParams(p)
   104  				fmt.Fprintln(r.Out(), out)
   105  				return nil
   106  			}
   107  
   108  			// TODO: try to update this so that it works with the CLI logger (???)
   109  			w := tabwriter.NewWriter(r.Err(), 5, 2, 2, ' ', 0)
   110  			return writePlainOutput(w, p)
   111  		},
   112  	}
   113  	return cmd
   114  }
   115  
   116  func printRaw(artifact oci.Artifact, ref string) (string, error) {
   117  	raw, err := artifact.RawManifest()
   118  	if err != nil {
   119  		return "", fmt.Errorf("failed to read manifest for %s: %w", ref, err)
   120  	}
   121  	out := &bytes.Buffer{}
   122  	if err := json.Indent(out, raw, "", "  "); err != nil {
   123  		return "", err
   124  	}
   125  	return out.String(), nil
   126  }
   127  
   128  func parseTime(created string) (time.Time, error) {
   129  	createdTime, err := time.Parse(time.RFC3339, created)
   130  	if err != nil {
   131  		return time.Time{}, fmt.Errorf("failed to parse time: %w", err)
   132  	}
   133  	return createdTime, nil
   134  }
   135  
   136  func listAll(w *tabwriter.Writer, idx v1.ImageIndex) error {
   137  	manifest, err := idx.IndexManifest()
   138  	if err != nil {
   139  		return fmt.Errorf("failed to read warehouse index: %w", err)
   140  	}
   141  
   142  	fmt.Fprintln(w, "TAG\tDIGEST\tTEAM\tREVISION\tCREATED\t")
   143  	for _, m := range manifest.Manifests {
   144  		// Retrieve the annotations from the image pointed to by index.json
   145  		a, err := oci.ArtifactFromIdx(idx, m)
   146  		if err != nil {
   147  			// TODO: better error
   148  			return fmt.Errorf("failed to load artifact: %w", err)
   149  		}
   150  		p, err := pallet.New(a)
   151  		if err != nil {
   152  			return fmt.Errorf("failed to create pallet from artifact: %w", err)
   153  		}
   154  
   155  		// Calculate time elapsed since creation
   156  		createdTime, err := parseTime(p.Metadata().Created)
   157  		if err != nil {
   158  			return err
   159  		}
   160  		creationRaw := time.Since(createdTime)
   161  
   162  		var creation string
   163  		switch {
   164  		case creationRaw.Seconds() < 60:
   165  			creation = fmt.Sprint(int(creationRaw.Seconds())) + " seconds ago"
   166  		case creationRaw.Minutes() < 60:
   167  			creation = fmt.Sprint(int(creationRaw.Minutes())) + " minutes ago"
   168  		case creationRaw.Hours() > 1 && creationRaw.Hours() <= 24:
   169  			creation = fmt.Sprint(int(creationRaw.Hours())) + " hours ago"
   170  		case creationRaw.Hours() > 24 && creationRaw.Hours() <= 168:
   171  			creation = fmt.Sprint(int(creationRaw.Hours()/24)) + " days ago"
   172  		case creationRaw.Hours() > 168 && creationRaw.Hours() <= 672:
   173  			creation = fmt.Sprint(int(creationRaw.Hours()/168)) + " weeks ago"
   174  		case creationRaw.Hours() > 672 && creationRaw.Hours() < 8064:
   175  			creation = fmt.Sprint(int(creationRaw.Hours()/672)) + " months ago"
   176  		default:
   177  			creation = (createdTime.Format(time.RFC822Z))[0:9]
   178  		}
   179  
   180  		fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t\n",
   181  			m.Annotations[wh.AnnotationRefName],
   182  			m.Digest.Hex[0:7],
   183  			p.Metadata().Team,
   184  			p.Metadata().Revision[0:7],
   185  			creation,
   186  		)
   187  	}
   188  	if err := w.Flush(); err != nil {
   189  		return fmt.Errorf("failed to render output: %w", err)
   190  	}
   191  
   192  	return nil
   193  }
   194  
   195  // TODO: this should consistently use the tabwriter w instead of mixing fmt.Printf
   196  // in throughout. tabwriter cells should be tab-terminated and we should leverage the
   197  // tab columns to organize all data consistently, e.g.:
   198  //
   199  // NAME					lumper-controller
   200  // PARAMETERS		cluster_hash
   201  //
   202  //	cluster_provider
   203  //	cluster_uuid
   204  //	foreman_gcp_project
   205  //	[...]
   206  //
   207  // PROVIDERS		generic,dsds							sha256:d34db33f
   208  //
   209  //	gke												sha256:f00b4r
   210  //
   211  // DEPENDENCIES	external-secrets-operator	sha256:[...]
   212  //
   213  //	[...]											[...]
   214  func writePlainOutput(w *tabwriter.Writer, p pallet.Pallet) error {
   215  	digest, err := p.Digest()
   216  	if err != nil {
   217  		return fmt.Errorf("failed to fetch digest of %s: %w", p, err)
   218  	}
   219  	createdTime, err := parseTime(p.Metadata().Created)
   220  	if err != nil {
   221  		return err
   222  	}
   223  	created := (createdTime.Format(time.RFC1123))[5:17]
   224  	out := outputParams(p)
   225  
   226  	fmt.Printf("NAME:\t\t%s\n", p.Name())
   227  	fmt.Printf("PARAMETERS:\t%s\n", out)
   228  	fmt.Printf("DIGEST:\t\t%s\n", digest.Hex)
   229  	fmt.Printf("TEAM:\t\t%s\n", p.Metadata().Team)
   230  	fmt.Printf("REVISION:\t%s\n", p.Metadata().Revision)
   231  	fmt.Printf("CREATED:\t%s\n", created)
   232  
   233  	pType, err := p.MediaType()
   234  	if err != nil {
   235  		return fmt.Errorf("failed to fetch MediaType of %s: %w", p.Name(), err)
   236  	}
   237  
   238  	// Check if pallet is a CompositePallet
   239  	isComposite, err := oci.IsComposite(p)
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	switch {
   245  	case isComposite:
   246  		// If pallet is a CompositePallet, show underlying pallets
   247  		fmt.Printf("PALLETS:\n")
   248  		cp, err := p.RawManifest()
   249  		if err != nil {
   250  			return err
   251  		}
   252  		manifest := &v1.IndexManifest{}
   253  		err = json.Unmarshal(cp, manifest)
   254  		if err != nil {
   255  			return err
   256  		}
   257  		for _, manifest := range manifest.Manifests {
   258  			fmt.Printf("- %s@%v\n", manifest.Annotations[wh.AnnotationRefName], manifest.Digest)
   259  		}
   260  
   261  	case pType.IsImage():
   262  		providers, err := json.Marshal(p.Providers()[3:])
   263  		if err != nil {
   264  			return fmt.Errorf("failed to marshal providers: %w", err)
   265  		}
   266  		fmt.Printf("PROVIDERS:\t%s\n", strings.Trim(strings.Replace(string(providers), "\"", "", -1), "[]"))
   267  
   268  	case pType.IsIndex():
   269  		fmt.Printf("PROVIDERS:\n")
   270  		//Fetch provider variants and digests from underlying artifact
   271  		cp, err := p.RawManifest()
   272  		if err != nil {
   273  			return err
   274  		}
   275  		manifest := &v1.IndexManifest{}
   276  		err = json.Unmarshal(cp, manifest)
   277  		if err != nil {
   278  			return err
   279  		}
   280  		for _, manifest := range manifest.Manifests {
   281  			if manifest.Annotations[warehouse.AnnotationRefName] != p.Name() {
   282  				continue
   283  			}
   284  			providers := manifest.Annotations[wh.AnnotationClusterProviders]
   285  			variantDigest := manifest.Digest
   286  			fmt.Fprintf(w, "- %s@%v\n", providers, variantDigest)
   287  		}
   288  
   289  		// Grab and display dependencies if applicable
   290  		pDeps, err := resolve.Resolve(p)
   291  		if err != nil {
   292  			return err
   293  		}
   294  		fmt.Printf("DEPENDENCIES:\n")
   295  		for depName, depDigest := range pDeps {
   296  			if depName == p.Name() {
   297  				continue
   298  			}
   299  			fmt.Fprintf(w, "- %s@%v\n", depName, depDigest)
   300  		}
   301  	}
   302  
   303  	if err := w.Flush(); err != nil {
   304  		return fmt.Errorf("failed to render output: %w", err)
   305  	}
   306  
   307  	return nil
   308  }
   309  
   310  func outputParams(p pallet.Pallet) string {
   311  	var out string
   312  	params := p.Parameters()
   313  	switch {
   314  	case params != nil:
   315  		out = fmt.Sprint(params)
   316  	default:
   317  		out = "No required rendering parameters"
   318  	}
   319  	return out
   320  }
   321  

View as plain text