diff --git a/README.md b/README.md index 05a695e..d00e7e8 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ Tape has the following commands: - `tape images` - examine images referenced by a given set of manifests before packaging them - `tape package` - package an artifcat and push it to a registry - `tape pull` – downlowad and extract contents and attestations from an existing artifact +- `tape view` – inspect an existing artifact ### Example diff --git a/attest/types/types.go b/attest/types/types.go index 994844b..f82bea2 100644 --- a/attest/types/types.go +++ b/attest/types/types.go @@ -232,8 +232,9 @@ func (s Subjects) Export() []toto.Subject { return subjects } -func (s Subjects) MarshalJSON() ([]byte, error) { return json.Marshal(s.Export()) } -func (s *Subjects) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, s) } +func (s Subjects) MarshalJSON() ([]byte, error) { return json.Marshal(s.Export()) } + +//func (s *Subjects) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, s) } func MakePathCheckSummaryCollection(entries ...PathChecker) (*PathCheckSummaryCollection, error) { numEntries := len(entries) diff --git a/tape/app/app.go b/tape/app/app.go index 945f10f..c14b58c 100644 --- a/tape/app/app.go +++ b/tape/app/app.go @@ -85,6 +85,13 @@ func Run() int { OutputManifestDirOptions: OutputManifestDirOptions{}, }, }, + { + name: "view", + short: "View an artefact", + options: &TapeViewCommand{ + tape: tape, + }, + }, } for _, c := range commands { diff --git a/tape/app/view.go b/tape/app/view.go new file mode 100644 index 0000000..a2e6c22 --- /dev/null +++ b/tape/app/view.go @@ -0,0 +1,154 @@ +package app + +import ( + "bufio" + "bytes" + "compress/gzip" + "context" + "encoding/json" + "fmt" + "os" + + toto "github.com/in-toto/in-toto-golang/in_toto" + + //attestTypes "github.com/docker/labs-brown-tape/attest/types" + + "github.com/docker/labs-brown-tape/oci" +) + +type TapeViewCommand struct { + tape *TapeCommand + OutputFormatOptions + + Image string `short:"I" long:"image" description:"Name of the image to view" required:"true"` +} + +type artefactInfo struct { + RawManifests struct { + Index rawManifest[oci.IndexManifest] `json:"index"` + Content rawManifest[oci.Manifest] `json:"content"` + Attest rawManifest[oci.Manifest] `json:"attest"` + } `json:"rawManifests"` + Attestations []toto.Statement `json:"attestations"` +} + +type rawManifest[T oci.Manifest | oci.IndexManifest] struct { + Digest string `json:"digest,omitempty"` + Manifest *T `json:"manifest,omitempty"` +} + +func (c *TapeViewCommand) Execute(args []string) error { + ctx := context.WithValue(c.tape.ctx, "command", "view") + if len(args) != 0 { + return fmt.Errorf("unexpected arguments: %v", args) + } + + if err := c.tape.Init(); err != nil { + return err + } + + client := oci.NewClient(nil) + + outputInfo, err := c.CollectInfo(ctx, client) + if err != nil { + return fmt.Errorf("failed to collect info about artifact: %w", err) + } + + if err := c.PrintInfo(ctx, outputInfo); err != nil { + return fmt.Errorf("failed to print info about artifact: %w", err) + } + + return nil +} + +func (c *TapeViewCommand) CollectInfo(ctx context.Context, client *oci.Client) (*artefactInfo, error) { + artefactInfo := &artefactInfo{} + + imageIndex, indexManifest, _, err := client.GetIndexOrImage(ctx, c.Image) + if err != nil { + return nil, err + } + if indexManifest == nil { + return nil, fmt.Errorf("no index manifest found for %q", c.Image) + } + + imageIndexDigest, err := imageIndex.Digest() + if err != nil { + return nil, err + } + + artefactInfo.RawManifests.Index = rawManifest[oci.IndexManifest]{ + Digest: imageIndexDigest.String(), + Manifest: indexManifest, + } + + imageInfo, manifests, err := client.FetchFromIndexOrImage(ctx, imageIndex, indexManifest, nil) + if err != nil { + return nil, err + } + + if len(imageInfo) == 0 { + return nil, fmt.Errorf("no images found in index %q", c.Image) + } + + for i := range imageInfo { + info := imageInfo[i] + switch info.MediaType { + case oci.ContentMediaType: + case oci.AttestMediaType: + gr, err := gzip.NewReader(info) + if err != nil { + return nil, err + } + scanner := bufio.NewScanner(gr) + for scanner.Scan() { + statement := toto.Statement{} // attestTypes.GenericStatement[any]{} + if err := json.NewDecoder(bytes.NewBuffer(scanner.Bytes())).Decode(&statement); err != nil { + return nil, err + } + artefactInfo.Attestations = append(artefactInfo.Attestations, statement) + } + if err := scanner.Err(); err != nil { + return nil, err + } + if err := gr.Close(); err != nil { + return nil, err + } + } + } + + for digest := range manifests { + m := rawManifest[oci.Manifest]{ + Digest: digest.String(), + Manifest: manifests[digest], + } + switch m.Manifest.Config.MediaType { + case oci.ContentMediaType: + artefactInfo.RawManifests.Content = m + case oci.AttestMediaType: + artefactInfo.RawManifests.Attest = m + } + } + return artefactInfo, nil +} + +func (c *TapeViewCommand) PrintInfo(ctx context.Context, outputInfo *artefactInfo) error { + stdj := json.NewEncoder(os.Stdout) + switch c.OutputFormat { + case OutputFormatDirectJSON: + stdj.SetIndent("", " ") + if err := stdj.Encode(outputInfo); err != nil { + return fmt.Errorf("failed to marshal output: %w", err) + } + case OutputFormatText, OutputFormatDetailedText: + fmt.Printf("%s\n", c.Image) + fmt.Printf(" Digest: %s\n", outputInfo.RawManifests.Index.Digest) + fmt.Printf(" OCI Manifests:\n") + fmt.Printf(" %s %s %d \n", outputInfo.RawManifests.Content.Digest, + outputInfo.RawManifests.Content.Manifest.Config.MediaType, + outputInfo.RawManifests.Content.Manifest.Config.Size) + fmt.Printf(" %s\n", outputInfo.RawManifests.Attest.Digest) + + } + return nil +}