169 lines
6.2 KiB
Go
169 lines
6.2 KiB
Go
package download
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
|
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
|
|
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
|
|
"github.com/cli/cli/v2/pkg/cmd/attestation/auth"
|
|
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
|
|
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
|
|
"github.com/cli/cli/v2/pkg/cmdutil"
|
|
ghauth "github.com/cli/go-gh/v2/pkg/auth"
|
|
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func NewDownloadCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command {
|
|
opts := &Options{}
|
|
downloadCmd := &cobra.Command{
|
|
Use: "download [<file-path> | oci://<image-uri>] [--owner | --repo]",
|
|
Args: cmdutil.ExactArgs(1, "must specify file path or container image URI, as well as one of --owner or --repo"),
|
|
Short: "Download an artifact's attestations for offline use",
|
|
Long: heredoc.Docf(`
|
|
### NOTE: This feature is currently in public preview, and subject to change.
|
|
|
|
Download attestations associated with an artifact for offline use.
|
|
|
|
The command requires either:
|
|
* a file path to an artifact, or
|
|
* a container image URI (e.g. %[1]soci://<image-uri>%[1]s)
|
|
* (note that if you provide an OCI URL, you must already be authenticated with
|
|
its container registry)
|
|
|
|
In addition, the command requires either:
|
|
* the %[1]s--repo%[1]s flag (e.g. --repo github/example).
|
|
* the %[1]s--owner%[1]s flag (e.g. --owner github), or
|
|
|
|
The %[1]s--repo%[1]s flag value must match the name of the GitHub repository
|
|
that the artifact is linked with.
|
|
|
|
The %[1]s--owner%[1]s flag value must match the name of the GitHub organization
|
|
that the artifact's linked repository belongs to.
|
|
|
|
Any associated bundle(s) will be written to a file in the
|
|
current directory named after the artifact's digest. For example, if the
|
|
digest is "sha256:1234", the file will be named "sha256:1234.jsonl".
|
|
|
|
Colons are special characters on Windows and cannot be used in
|
|
file names. To accommodate, a dash will be used to separate the algorithm
|
|
from the digest in the attestations file name. For example, if the digest
|
|
is "sha256:1234", the file will be named "sha256-1234.jsonl".
|
|
`, "`"),
|
|
Example: heredoc.Doc(`
|
|
# Download attestations for a local artifact linked with an organization
|
|
$ gh attestation download example.bin -o github
|
|
|
|
# Download attestations for a local artifact linked with a repository
|
|
$ gh attestation download example.bin -R github/example
|
|
|
|
# Download attestations for an OCI image linked with an organization
|
|
$ gh attestation download oci://example.com/foo/bar:latest -o github
|
|
`),
|
|
// PreRunE is used to validate flags before the command is run
|
|
// If an error is returned, its message will be printed to the terminal
|
|
// along with information about how use the command
|
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
|
// Create a logger for use throughout the download command
|
|
opts.Logger = io.NewHandler(f.IOStreams)
|
|
|
|
// set the artifact path
|
|
opts.ArtifactPath = args[0]
|
|
|
|
// check that the provided flags are valid
|
|
if err := opts.AreFlagsValid(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
hc, err := f.HttpClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if opts.Hostname == "" {
|
|
opts.Hostname, _ = ghauth.DefaultHost()
|
|
}
|
|
if err := auth.IsHostSupported(opts.Hostname); err != nil {
|
|
return err
|
|
}
|
|
|
|
opts.APIClient = api.NewLiveClient(hc, opts.Hostname, opts.Logger)
|
|
opts.OCIClient = oci.NewLiveClient()
|
|
opts.Store = NewLiveStore("")
|
|
|
|
if runF != nil {
|
|
return runF(opts)
|
|
}
|
|
|
|
if err := runDownload(opts); err != nil {
|
|
return fmt.Errorf("Failed to download the artifact's bundle(s): %v", err)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
downloadCmd.Flags().StringVarP(&opts.Owner, "owner", "o", "", "a GitHub organization to scope attestation lookup by")
|
|
downloadCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format <owner>/<repo>")
|
|
downloadCmd.MarkFlagsMutuallyExclusive("owner", "repo")
|
|
downloadCmd.MarkFlagsOneRequired("owner", "repo")
|
|
downloadCmd.Flags().StringVarP(&opts.PredicateType, "predicate-type", "", "", "Filter attestations by provided predicate type")
|
|
cmdutil.StringEnumFlag(downloadCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact")
|
|
downloadCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch")
|
|
downloadCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use")
|
|
|
|
return downloadCmd
|
|
}
|
|
|
|
func runDownload(opts *Options) error {
|
|
artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to digest artifact: %v", err)
|
|
}
|
|
|
|
opts.Logger.VerbosePrintf("Downloading trusted metadata for artifact %s\n\n", opts.ArtifactPath)
|
|
|
|
params := verification.FetchRemoteAttestationsParams{
|
|
Digest: artifact.DigestWithAlg(),
|
|
Limit: opts.Limit,
|
|
Owner: opts.Owner,
|
|
Repo: opts.Repo,
|
|
}
|
|
attestations, err := verification.GetRemoteAttestations(opts.APIClient, params)
|
|
if err != nil {
|
|
if errors.Is(err, api.ErrNoAttestations{}) {
|
|
fmt.Fprintf(opts.Logger.IO.Out, "No attestations found for %s\n", opts.ArtifactPath)
|
|
return nil
|
|
}
|
|
return fmt.Errorf("failed to fetch attestations: %v", err)
|
|
}
|
|
|
|
// Apply predicate type filter to returned attestations
|
|
if opts.PredicateType != "" {
|
|
filteredAttestations := verification.FilterAttestations(opts.PredicateType, attestations)
|
|
|
|
if len(filteredAttestations) == 0 {
|
|
return fmt.Errorf("no attestations found with predicate type: %s", opts.PredicateType)
|
|
}
|
|
|
|
attestations = filteredAttestations
|
|
}
|
|
|
|
metadataFilePath, err := opts.Store.createMetadataFile(artifact.DigestWithAlg(), attestations)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write attestation: %v", err)
|
|
}
|
|
fmt.Fprintf(opts.Logger.IO.Out, "Wrote attestations to file %s.\nAny previous content has been overwritten\n\n", metadataFilePath)
|
|
|
|
fmt.Fprint(opts.Logger.IO.Out,
|
|
opts.Logger.ColorScheme.Greenf(
|
|
"The trusted metadata is now available at %s\n", metadataFilePath,
|
|
),
|
|
)
|
|
|
|
return nil
|
|
}
|