mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-18 00:47:48 +08:00
Merge pull request #2891 from tonistiigi/history-command-initial
Add buildx history command
This commit is contained in:
commit
a18ff4d5ef
486
commands/history/inspect.go
Normal file
486
commands/history/inspect.go
Normal file
@ -0,0 +1,486 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/content"
|
||||
"github.com/containerd/containerd/v2/core/content/proxy"
|
||||
"github.com/containerd/containerd/v2/core/images"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/localstate"
|
||||
"github.com/docker/buildx/util/cobrautil/completion"
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/docker/buildx/util/desktop"
|
||||
"github.com/docker/cli/cli/command"
|
||||
slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
|
||||
slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
|
||||
controlapi "github.com/moby/buildkit/api/services/control"
|
||||
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tonistiigi/go-csvvalue"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
type inspectOptions struct {
|
||||
builder string
|
||||
ref string
|
||||
}
|
||||
|
||||
func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error {
|
||||
b, err := builder.New(dockerCli, builder.WithName(opts.builder))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodes, err := b.LoadNodes(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node.Err != nil {
|
||||
return node.Err
|
||||
}
|
||||
}
|
||||
|
||||
recs, err := queryRecords(ctx, opts.ref, nodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(recs) == 0 {
|
||||
if opts.ref == "" {
|
||||
return errors.New("no records found")
|
||||
}
|
||||
return errors.Errorf("no record found for ref %q", opts.ref)
|
||||
}
|
||||
|
||||
if opts.ref == "" {
|
||||
slices.SortFunc(recs, func(a, b historyRecord) int {
|
||||
return b.CreatedAt.AsTime().Compare(a.CreatedAt.AsTime())
|
||||
})
|
||||
}
|
||||
|
||||
rec := &recs[0]
|
||||
|
||||
ls, err := localstate.New(confutil.NewConfig(dockerCli))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st, _ := ls.ReadRef(rec.node.Builder, rec.node.Name, rec.Ref)
|
||||
|
||||
tw := tabwriter.NewWriter(dockerCli.Out(), 1, 8, 1, '\t', 0)
|
||||
|
||||
attrs := rec.FrontendAttrs
|
||||
delete(attrs, "frontend.caps")
|
||||
|
||||
writeAttr := func(k, name string, f func(v string) (string, bool)) {
|
||||
if v, ok := attrs[k]; ok {
|
||||
if f != nil {
|
||||
v, ok = f(v)
|
||||
}
|
||||
if ok {
|
||||
fmt.Fprintf(tw, "%s:\t%s\n", name, v)
|
||||
}
|
||||
}
|
||||
delete(attrs, k)
|
||||
}
|
||||
|
||||
var context string
|
||||
var dockerfile string
|
||||
if st != nil {
|
||||
context = st.LocalPath
|
||||
dockerfile = st.DockerfilePath
|
||||
wd, _ := os.Getwd()
|
||||
|
||||
if dockerfile != "" && dockerfile != "-" {
|
||||
if rel, err := filepath.Rel(context, dockerfile); err == nil {
|
||||
if !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||
dockerfile = rel
|
||||
}
|
||||
}
|
||||
}
|
||||
if context != "" {
|
||||
if rel, err := filepath.Rel(wd, context); err == nil {
|
||||
if !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||
context = rel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if v, ok := attrs["context"]; ok && context == "" {
|
||||
delete(attrs, "context")
|
||||
context = v
|
||||
}
|
||||
if dockerfile == "" {
|
||||
if v, ok := attrs["filename"]; ok {
|
||||
dockerfile = v
|
||||
if dfdir, ok := attrs["vcs:localdir:dockerfile"]; ok {
|
||||
dockerfile = filepath.Join(dfdir, dockerfile)
|
||||
}
|
||||
}
|
||||
}
|
||||
delete(attrs, "filename")
|
||||
|
||||
if context != "" {
|
||||
fmt.Fprintf(tw, "Context:\t%s\n", context)
|
||||
}
|
||||
if dockerfile != "" {
|
||||
fmt.Fprintf(tw, "Dockerfile:\t%s\n", dockerfile)
|
||||
}
|
||||
if _, ok := attrs["context"]; !ok {
|
||||
if src, ok := attrs["vcs:source"]; ok {
|
||||
fmt.Fprintf(tw, "VCS Repository:\t%s\n", src)
|
||||
}
|
||||
if rev, ok := attrs["vcs:revision"]; ok {
|
||||
fmt.Fprintf(tw, "VCS Revision:\t%s\n", rev)
|
||||
}
|
||||
}
|
||||
|
||||
writeAttr("target", "Target", nil)
|
||||
writeAttr("platform", "Platform", func(v string) (string, bool) {
|
||||
return tryParseValue(v, func(v string) (string, error) {
|
||||
var pp []string
|
||||
for _, v := range strings.Split(v, ",") {
|
||||
p, err := platforms.Parse(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pp = append(pp, platforms.FormatAll(platforms.Normalize(p)))
|
||||
}
|
||||
return strings.Join(pp, ", "), nil
|
||||
}), true
|
||||
})
|
||||
writeAttr("build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR", "Keep Git Dir", func(v string) (string, bool) {
|
||||
return tryParseValue(v, func(v string) (string, error) {
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strconv.FormatBool(b), nil
|
||||
}), true
|
||||
})
|
||||
|
||||
tw.Flush()
|
||||
|
||||
fmt.Fprintln(dockerCli.Out())
|
||||
|
||||
printTable(dockerCli.Out(), attrs, "context:", "Named Context")
|
||||
|
||||
tw = tabwriter.NewWriter(dockerCli.Out(), 1, 8, 1, '\t', 0)
|
||||
|
||||
fmt.Fprintf(tw, "Started:\t%s\n", rec.CreatedAt.AsTime().Format("2006-01-02 15:04:05"))
|
||||
var duration time.Duration
|
||||
var status string
|
||||
if rec.CompletedAt != nil {
|
||||
duration = rec.CompletedAt.AsTime().Sub(rec.CreatedAt.AsTime())
|
||||
} else {
|
||||
duration = rec.currentTimestamp.Sub(rec.CreatedAt.AsTime())
|
||||
status = " (running)"
|
||||
}
|
||||
fmt.Fprintf(tw, "Duration:\t%s%s\n", formatDuration(duration), status)
|
||||
if rec.Error != nil {
|
||||
if codes.Code(rec.Error.Code) == codes.Canceled {
|
||||
fmt.Fprintf(tw, "Status:\tCanceled\n")
|
||||
} else {
|
||||
fmt.Fprintf(tw, "Error:\t%s %s\n", codes.Code(rec.Error.Code).String(), rec.Error.Message)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(tw, "Build Steps:\t%d/%d (%.0f%% cached)\n", rec.NumCompletedSteps, rec.NumTotalSteps, float64(rec.NumCachedSteps)/float64(rec.NumTotalSteps)*100)
|
||||
tw.Flush()
|
||||
|
||||
fmt.Fprintln(dockerCli.Out())
|
||||
|
||||
tw = tabwriter.NewWriter(dockerCli.Out(), 1, 8, 1, '\t', 0)
|
||||
|
||||
writeAttr("force-network-mode", "Network", nil)
|
||||
writeAttr("hostname", "Hostname", nil)
|
||||
writeAttr("add-hosts", "Extra Hosts", func(v string) (string, bool) {
|
||||
return tryParseValue(v, func(v string) (string, error) {
|
||||
fields, err := csvvalue.Fields(v, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.Join(fields, ", "), nil
|
||||
}), true
|
||||
})
|
||||
writeAttr("cgroup-parent", "Cgroup Parent", nil)
|
||||
writeAttr("image-resolve-mode", "Image Resolve Mode", nil)
|
||||
writeAttr("multi-platform", "Force Multi-Platform", nil)
|
||||
writeAttr("build-arg:BUILDKIT_MULTI_PLATFORM", "Force Multi-Platform", nil)
|
||||
writeAttr("no-cache", "Disable Cache", func(v string) (string, bool) {
|
||||
if v == "" {
|
||||
return "true", true
|
||||
}
|
||||
return v, true
|
||||
})
|
||||
writeAttr("shm-size", "Shm Size", nil)
|
||||
writeAttr("ulimit", "Resource Limits", nil)
|
||||
writeAttr("build-arg:BUILDKIT_CACHE_MOUNT_NS", "Cache Mount Namespace", nil)
|
||||
writeAttr("build-arg:BUILDKIT_DOCKERFILE_CHECK", "Dockerfile Check Config", nil)
|
||||
writeAttr("build-arg:SOURCE_DATE_EPOCH", "Source Date Epoch", nil)
|
||||
writeAttr("build-arg:SANDBOX_HOSTNAME", "Sandbox Hostname", nil)
|
||||
|
||||
var unusedAttrs []string
|
||||
for k := range attrs {
|
||||
if strings.HasPrefix(k, "vcs:") || strings.HasPrefix(k, "build-arg:") || strings.HasPrefix(k, "label:") || strings.HasPrefix(k, "context:") || strings.HasPrefix(k, "attest:") {
|
||||
continue
|
||||
}
|
||||
unusedAttrs = append(unusedAttrs, k)
|
||||
}
|
||||
slices.Sort(unusedAttrs)
|
||||
|
||||
for _, k := range unusedAttrs {
|
||||
fmt.Fprintf(tw, "%s:\t%s\n", k, attrs[k])
|
||||
}
|
||||
|
||||
tw.Flush()
|
||||
|
||||
fmt.Fprintln(dockerCli.Out())
|
||||
|
||||
printTable(dockerCli.Out(), attrs, "build-arg:", "Build Arg")
|
||||
printTable(dockerCli.Out(), attrs, "label:", "Label")
|
||||
|
||||
c, err := rec.node.Driver.Client(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
store := proxy.NewContentStore(c.ContentClient())
|
||||
|
||||
attachments, err := allAttachments(ctx, store, *rec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
provIndex := slices.IndexFunc(attachments, func(a attachment) bool {
|
||||
return descrType(a.descr) == slsa02.PredicateSLSAProvenance
|
||||
})
|
||||
if provIndex != -1 {
|
||||
prov := attachments[provIndex]
|
||||
|
||||
dt, err := content.ReadBlob(ctx, store, prov.descr)
|
||||
if err != nil {
|
||||
return errors.Errorf("failed to read provenance %s: %v", prov.descr.Digest, err)
|
||||
}
|
||||
|
||||
var pred provenancetypes.ProvenancePredicate
|
||||
if err := json.Unmarshal(dt, &pred); err != nil {
|
||||
return errors.Errorf("failed to unmarshal provenance %s: %v", prov.descr.Digest, err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(dockerCli.Out(), "Materials:")
|
||||
tw = tabwriter.NewWriter(dockerCli.Out(), 1, 8, 1, '\t', 0)
|
||||
fmt.Fprintf(tw, "URI\tDIGEST\n")
|
||||
for _, m := range pred.Materials {
|
||||
fmt.Fprintf(tw, "%s\t%s\n", m.URI, strings.Join(digestSetToDigests(m.Digest), ", "))
|
||||
}
|
||||
tw.Flush()
|
||||
fmt.Fprintln(dockerCli.Out())
|
||||
}
|
||||
|
||||
if len(attachments) > 0 {
|
||||
fmt.Fprintf(tw, "Attachments:\n")
|
||||
tw = tabwriter.NewWriter(dockerCli.Out(), 1, 8, 1, '\t', 0)
|
||||
fmt.Fprintf(tw, "DIGEST\tPLATFORM\tTYPE\n")
|
||||
for _, a := range attachments {
|
||||
p := ""
|
||||
if a.platform != nil {
|
||||
p = platforms.FormatAll(*a.platform)
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\n", a.descr.Digest, p, descrType(a.descr))
|
||||
}
|
||||
tw.Flush()
|
||||
fmt.Fprintln(dockerCli.Out())
|
||||
}
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "Print build logs: docker buildx history logs %s\n", rec.Ref)
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "View build in Docker Desktop: %s\n", desktop.BuildURL(rec.Ref))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func inspectCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
|
||||
var options inspectOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [OPTIONS] [REF]",
|
||||
Short: "Inspect a build",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
options.ref = args[0]
|
||||
}
|
||||
options.builder = *rootOpts.Builder
|
||||
return runInspect(cmd.Context(), dockerCli, options)
|
||||
},
|
||||
ValidArgsFunction: completion.Disable,
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
attachmentCmd(dockerCli, rootOpts),
|
||||
)
|
||||
|
||||
// flags := cmd.Flags()
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
type attachment struct {
|
||||
platform *ocispecs.Platform
|
||||
descr ocispecs.Descriptor
|
||||
}
|
||||
|
||||
func allAttachments(ctx context.Context, store content.Store, rec historyRecord) ([]attachment, error) {
|
||||
var attachments []attachment
|
||||
|
||||
if rec.Result != nil {
|
||||
for _, a := range rec.Result.Attestations {
|
||||
attachments = append(attachments, attachment{
|
||||
descr: ociDesc(a),
|
||||
})
|
||||
}
|
||||
for _, r := range rec.Result.Results {
|
||||
attachments = append(attachments, walkAttachments(ctx, store, ociDesc(r), nil)...)
|
||||
}
|
||||
}
|
||||
|
||||
for key, ri := range rec.Results {
|
||||
p, err := platforms.Parse(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, a := range ri.Attestations {
|
||||
attachments = append(attachments, attachment{
|
||||
platform: &p,
|
||||
descr: ociDesc(a),
|
||||
})
|
||||
}
|
||||
for _, r := range ri.Results {
|
||||
attachments = append(attachments, walkAttachments(ctx, store, ociDesc(r), &p)...)
|
||||
}
|
||||
}
|
||||
|
||||
slices.SortFunc(attachments, func(a, b attachment) int {
|
||||
pCmp := 0
|
||||
if a.platform == nil && b.platform != nil {
|
||||
return -1
|
||||
} else if a.platform != nil && b.platform == nil {
|
||||
return 1
|
||||
} else if a.platform != nil && b.platform != nil {
|
||||
pCmp = cmp.Compare(platforms.FormatAll(*a.platform), platforms.FormatAll(*b.platform))
|
||||
}
|
||||
return cmp.Or(
|
||||
pCmp,
|
||||
cmp.Compare(descrType(a.descr), descrType(b.descr)),
|
||||
)
|
||||
})
|
||||
|
||||
return attachments, nil
|
||||
}
|
||||
|
||||
func walkAttachments(ctx context.Context, store content.Store, desc ocispecs.Descriptor, platform *ocispecs.Platform) []attachment {
|
||||
_, err := store.Info(ctx, desc.Digest)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var out []attachment
|
||||
|
||||
if desc.Annotations["vnd.docker.reference.type"] != "attestation-manifest" {
|
||||
out = append(out, attachment{platform: platform, descr: desc})
|
||||
}
|
||||
|
||||
if desc.MediaType != ocispecs.MediaTypeImageIndex && desc.MediaType != images.MediaTypeDockerSchema2ManifestList {
|
||||
return out
|
||||
}
|
||||
|
||||
dt, err := content.ReadBlob(ctx, store, desc)
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
|
||||
var idx ocispecs.Index
|
||||
if err := json.Unmarshal(dt, &idx); err != nil {
|
||||
return out
|
||||
}
|
||||
|
||||
for _, d := range idx.Manifests {
|
||||
p := platform
|
||||
if d.Platform != nil {
|
||||
p = d.Platform
|
||||
}
|
||||
out = append(out, walkAttachments(ctx, store, d, p)...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func ociDesc(in *controlapi.Descriptor) ocispecs.Descriptor {
|
||||
return ocispecs.Descriptor{
|
||||
MediaType: in.MediaType,
|
||||
Digest: digest.Digest(in.Digest),
|
||||
Size: in.Size,
|
||||
Annotations: in.Annotations,
|
||||
}
|
||||
}
|
||||
func descrType(desc ocispecs.Descriptor) string {
|
||||
if typ, ok := desc.Annotations["in-toto.io/predicate-type"]; ok {
|
||||
return typ
|
||||
}
|
||||
return desc.MediaType
|
||||
}
|
||||
|
||||
func tryParseValue(s string, f func(string) (string, error)) string {
|
||||
v, err := f(s)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%s (%v)", s, err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func printTable(w io.Writer, attrs map[string]string, prefix, title string) {
|
||||
var keys []string
|
||||
for k := range attrs {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
keys = append(keys, strings.TrimPrefix(k, prefix))
|
||||
}
|
||||
}
|
||||
slices.Sort(keys)
|
||||
|
||||
if len(keys) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0)
|
||||
fmt.Fprintf(tw, "%s\tVALUE\n", strings.ToUpper(title))
|
||||
for _, k := range keys {
|
||||
fmt.Fprintf(tw, "%s\t%s\n", k, attrs[prefix+k])
|
||||
}
|
||||
tw.Flush()
|
||||
fmt.Fprintln(w)
|
||||
}
|
||||
|
||||
func digestSetToDigests(ds slsa.DigestSet) []string {
|
||||
var out []string
|
||||
for k, v := range ds {
|
||||
out = append(out, fmt.Sprintf("%s:%s", k, v))
|
||||
}
|
||||
return out
|
||||
}
|
152
commands/history/inspect_attachment.go
Normal file
152
commands/history/inspect_attachment.go
Normal file
@ -0,0 +1,152 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"slices"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/content/proxy"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/util/cobrautil/completion"
|
||||
"github.com/docker/cli/cli/command"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type attachmentOptions struct {
|
||||
builder string
|
||||
typ string
|
||||
platform string
|
||||
ref string
|
||||
digest digest.Digest
|
||||
}
|
||||
|
||||
func runAttachment(ctx context.Context, dockerCli command.Cli, opts attachmentOptions) error {
|
||||
b, err := builder.New(dockerCli, builder.WithName(opts.builder))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodes, err := b.LoadNodes(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node.Err != nil {
|
||||
return node.Err
|
||||
}
|
||||
}
|
||||
|
||||
recs, err := queryRecords(ctx, opts.ref, nodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(recs) == 0 {
|
||||
if opts.ref == "" {
|
||||
return errors.New("no records found")
|
||||
}
|
||||
return errors.Errorf("no record found for ref %q", opts.ref)
|
||||
}
|
||||
|
||||
if opts.ref == "" {
|
||||
slices.SortFunc(recs, func(a, b historyRecord) int {
|
||||
return b.CreatedAt.AsTime().Compare(a.CreatedAt.AsTime())
|
||||
})
|
||||
}
|
||||
|
||||
rec := &recs[0]
|
||||
|
||||
c, err := rec.node.Driver.Client(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
store := proxy.NewContentStore(c.ContentClient())
|
||||
|
||||
if opts.digest != "" {
|
||||
ra, err := store.ReaderAt(ctx, ocispecs.Descriptor{Digest: opts.digest})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(dockerCli.Out(), io.NewSectionReader(ra, 0, ra.Size()))
|
||||
return err
|
||||
}
|
||||
|
||||
attachments, err := allAttachments(ctx, store, *rec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
typ := opts.typ
|
||||
switch typ {
|
||||
case "index":
|
||||
typ = ocispecs.MediaTypeImageIndex
|
||||
case "manifest":
|
||||
typ = ocispecs.MediaTypeImageManifest
|
||||
case "image":
|
||||
typ = ocispecs.MediaTypeImageConfig
|
||||
case "provenance":
|
||||
typ = slsa02.PredicateSLSAProvenance
|
||||
case "sbom":
|
||||
typ = intoto.PredicateSPDX
|
||||
}
|
||||
|
||||
for _, a := range attachments {
|
||||
if opts.platform != "" && (a.platform == nil || platforms.FormatAll(*a.platform) != opts.platform) {
|
||||
continue
|
||||
}
|
||||
if typ != "" && descrType(a.descr) != typ {
|
||||
continue
|
||||
}
|
||||
ra, err := store.ReaderAt(ctx, a.descr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(dockerCli.Out(), io.NewSectionReader(ra, 0, ra.Size()))
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.Errorf("no matching attachment found for ref %q", opts.ref)
|
||||
}
|
||||
|
||||
func attachmentCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
|
||||
var options attachmentOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "attachment [OPTIONS] REF [DIGEST]",
|
||||
Short: "Inspect a build attachment",
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
options.ref = args[0]
|
||||
}
|
||||
if len(args) > 1 {
|
||||
dgst, err := digest.Parse(args[1])
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "invalid digest %q", args[1])
|
||||
}
|
||||
options.digest = dgst
|
||||
}
|
||||
|
||||
if options.digest == "" && options.platform == "" && options.typ == "" {
|
||||
return errors.New("at least one of --type, --platform or DIGEST must be specified")
|
||||
}
|
||||
|
||||
options.builder = *rootOpts.Builder
|
||||
return runAttachment(cmd.Context(), dockerCli, options)
|
||||
},
|
||||
ValidArgsFunction: completion.Disable,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&options.typ, "type", "", "Type of attachment")
|
||||
flags.StringVar(&options.platform, "platform", "", "Platform of attachment")
|
||||
|
||||
return cmd
|
||||
}
|
124
commands/history/logs.go
Normal file
124
commands/history/logs.go
Normal file
@ -0,0 +1,124 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/util/cobrautil/completion"
|
||||
"github.com/docker/buildx/util/progress"
|
||||
"github.com/docker/cli/cli/command"
|
||||
controlapi "github.com/moby/buildkit/api/services/control"
|
||||
"github.com/moby/buildkit/client"
|
||||
"github.com/moby/buildkit/util/progress/progressui"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type logsOptions struct {
|
||||
builder string
|
||||
ref string
|
||||
progress string
|
||||
}
|
||||
|
||||
func runLogs(ctx context.Context, dockerCli command.Cli, opts logsOptions) error {
|
||||
b, err := builder.New(dockerCli, builder.WithName(opts.builder))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodes, err := b.LoadNodes(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node.Err != nil {
|
||||
return node.Err
|
||||
}
|
||||
}
|
||||
|
||||
recs, err := queryRecords(ctx, opts.ref, nodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(recs) == 0 {
|
||||
if opts.ref == "" {
|
||||
return errors.New("no records found")
|
||||
}
|
||||
return errors.Errorf("no record found for ref %q", opts.ref)
|
||||
}
|
||||
|
||||
if opts.ref == "" {
|
||||
slices.SortFunc(recs, func(a, b historyRecord) int {
|
||||
return b.CreatedAt.AsTime().Compare(a.CreatedAt.AsTime())
|
||||
})
|
||||
}
|
||||
|
||||
rec := &recs[0]
|
||||
c, err := rec.node.Driver.Client(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cl, err := c.ControlClient().Status(ctx, &controlapi.StatusRequest{
|
||||
Ref: rec.Ref,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var mode progressui.DisplayMode = progressui.DisplayMode(opts.progress)
|
||||
if mode == progressui.AutoMode {
|
||||
mode = progressui.PlainMode
|
||||
}
|
||||
printer, err := progress.NewPrinter(context.TODO(), os.Stderr, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loop0:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
cl.CloseSend()
|
||||
return context.Cause(ctx)
|
||||
default:
|
||||
ev, err := cl.Recv()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break loop0
|
||||
}
|
||||
return err
|
||||
}
|
||||
printer.Write(client.NewSolveStatus(ev))
|
||||
}
|
||||
}
|
||||
|
||||
return printer.Wait()
|
||||
}
|
||||
|
||||
func logsCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
|
||||
var options logsOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "logs [OPTIONS] [REF]",
|
||||
Short: "Print the logs of a build",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
options.ref = args[0]
|
||||
}
|
||||
options.builder = *rootOpts.Builder
|
||||
return runLogs(cmd.Context(), dockerCli, options)
|
||||
},
|
||||
ValidArgsFunction: completion.Disable,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&options.progress, "progress", "plain", "Set type of progress output (plain, rawjson, tty)")
|
||||
|
||||
return cmd
|
||||
}
|
234
commands/history/ls.go
Normal file
234
commands/history/ls.go
Normal file
@ -0,0 +1,234 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/localstate"
|
||||
"github.com/docker/buildx/util/cobrautil/completion"
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/docker/buildx/util/desktop"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
lsHeaderBuildID = "BUILD ID"
|
||||
lsHeaderName = "NAME"
|
||||
lsHeaderStatus = "STATUS"
|
||||
lsHeaderCreated = "CREATED AT"
|
||||
lsHeaderDuration = "DURATION"
|
||||
lsHeaderLink = ""
|
||||
|
||||
lsDefaultTableFormat = "table {{.Ref}}\t{{.Name}}\t{{.Status}}\t{{.CreatedAt}}\t{{.Duration}}\t{{.Link}}"
|
||||
|
||||
headerKeyTimestamp = "buildkit-current-timestamp"
|
||||
)
|
||||
|
||||
type lsOptions struct {
|
||||
builder string
|
||||
format string
|
||||
noTrunc bool
|
||||
}
|
||||
|
||||
func runLs(ctx context.Context, dockerCli command.Cli, opts lsOptions) error {
|
||||
b, err := builder.New(dockerCli, builder.WithName(opts.builder))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodes, err := b.LoadNodes(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node.Err != nil {
|
||||
return node.Err
|
||||
}
|
||||
}
|
||||
|
||||
out, err := queryRecords(ctx, "", nodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ls, err := localstate.New(confutil.NewConfig(dockerCli))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, rec := range out {
|
||||
st, _ := ls.ReadRef(rec.node.Builder, rec.node.Name, rec.Ref)
|
||||
rec.name = buildName(rec.FrontendAttrs, st)
|
||||
out[i] = rec
|
||||
}
|
||||
|
||||
return lsPrint(dockerCli, out, opts)
|
||||
}
|
||||
|
||||
func lsCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
|
||||
var options lsOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls",
|
||||
Short: "List build records",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
options.builder = *rootOpts.Builder
|
||||
return runLs(cmd.Context(), dockerCli, options)
|
||||
},
|
||||
ValidArgsFunction: completion.Disable,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&options.format, "format", formatter.TableFormatKey, "Format the output")
|
||||
flags.BoolVar(&options.noTrunc, "no-trunc", false, "Don't truncate output")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func lsPrint(dockerCli command.Cli, records []historyRecord, in lsOptions) error {
|
||||
if in.format == formatter.TableFormatKey {
|
||||
in.format = lsDefaultTableFormat
|
||||
}
|
||||
|
||||
ctx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.Format(in.format),
|
||||
Trunc: !in.noTrunc,
|
||||
}
|
||||
|
||||
slices.SortFunc(records, func(a, b historyRecord) int {
|
||||
if a.CompletedAt == nil && b.CompletedAt != nil {
|
||||
return -1
|
||||
}
|
||||
if a.CompletedAt != nil && b.CompletedAt == nil {
|
||||
return 1
|
||||
}
|
||||
return b.CreatedAt.AsTime().Compare(a.CreatedAt.AsTime())
|
||||
})
|
||||
|
||||
var term bool
|
||||
if _, err := console.ConsoleFromFile(os.Stdout); err == nil {
|
||||
term = true
|
||||
}
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, r := range records {
|
||||
if err := format(&lsContext{
|
||||
format: formatter.Format(in.format),
|
||||
isTerm: term,
|
||||
trunc: !in.noTrunc,
|
||||
record: &r,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
lsCtx := lsContext{
|
||||
isTerm: term,
|
||||
trunc: !in.noTrunc,
|
||||
}
|
||||
lsCtx.Header = formatter.SubHeaderContext{
|
||||
"Ref": lsHeaderBuildID,
|
||||
"Name": lsHeaderName,
|
||||
"Status": lsHeaderStatus,
|
||||
"CreatedAt": lsHeaderCreated,
|
||||
"Duration": lsHeaderDuration,
|
||||
"Link": lsHeaderLink,
|
||||
}
|
||||
|
||||
return ctx.Write(&lsCtx, render)
|
||||
}
|
||||
|
||||
type lsContext struct {
|
||||
formatter.HeaderContext
|
||||
|
||||
isTerm bool
|
||||
trunc bool
|
||||
format formatter.Format
|
||||
record *historyRecord
|
||||
}
|
||||
|
||||
func (c *lsContext) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]interface{}{
|
||||
"ref": c.FullRef(),
|
||||
"name": c.Name(),
|
||||
"status": c.Status(),
|
||||
"created_at": c.record.CreatedAt.AsTime().Format(time.RFC3339Nano),
|
||||
"total_steps": c.record.NumTotalSteps,
|
||||
"completed_steps": c.record.NumCompletedSteps,
|
||||
"cached_steps": c.record.NumCachedSteps,
|
||||
}
|
||||
if c.record.CompletedAt != nil {
|
||||
m["completed_at"] = c.record.CompletedAt.AsTime().Format(time.RFC3339Nano)
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
func (c *lsContext) Ref() string {
|
||||
return c.record.Ref
|
||||
}
|
||||
|
||||
func (c *lsContext) FullRef() string {
|
||||
return fmt.Sprintf("%s/%s/%s", c.record.node.Builder, c.record.node.Name, c.record.Ref)
|
||||
}
|
||||
|
||||
func (c *lsContext) Name() string {
|
||||
name := c.record.name
|
||||
if c.trunc && c.format.IsTable() {
|
||||
return trimBeginning(name, 36)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func (c *lsContext) Status() string {
|
||||
if c.record.CompletedAt != nil {
|
||||
if c.record.Error != nil {
|
||||
return "Error"
|
||||
}
|
||||
return "Completed"
|
||||
}
|
||||
return "Running"
|
||||
}
|
||||
|
||||
func (c *lsContext) CreatedAt() string {
|
||||
return units.HumanDuration(time.Since(c.record.CreatedAt.AsTime())) + " ago"
|
||||
}
|
||||
|
||||
func (c *lsContext) Duration() string {
|
||||
lastTime := c.record.currentTimestamp
|
||||
if c.record.CompletedAt != nil {
|
||||
tm := c.record.CompletedAt.AsTime()
|
||||
lastTime = &tm
|
||||
}
|
||||
if lastTime == nil {
|
||||
return ""
|
||||
}
|
||||
v := formatDuration(lastTime.Sub(c.record.CreatedAt.AsTime()))
|
||||
if c.record.CompletedAt == nil {
|
||||
v += "+"
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (c *lsContext) Link() string {
|
||||
url := desktop.BuildURL(c.FullRef())
|
||||
if c.format.IsTable() {
|
||||
if c.isTerm {
|
||||
return desktop.ANSIHyperlink(url, "Open")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return url
|
||||
}
|
80
commands/history/open.go
Normal file
80
commands/history/open.go
Normal file
@ -0,0 +1,80 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/util/cobrautil/completion"
|
||||
"github.com/docker/buildx/util/desktop"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/pkg/browser"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type openOptions struct {
|
||||
builder string
|
||||
ref string
|
||||
}
|
||||
|
||||
func runOpen(ctx context.Context, dockerCli command.Cli, opts openOptions) error {
|
||||
b, err := builder.New(dockerCli, builder.WithName(opts.builder))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodes, err := b.LoadNodes(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node.Err != nil {
|
||||
return node.Err
|
||||
}
|
||||
}
|
||||
|
||||
recs, err := queryRecords(ctx, opts.ref, nodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(recs) == 0 {
|
||||
if opts.ref == "" {
|
||||
return errors.New("no records found")
|
||||
}
|
||||
return errors.Errorf("no record found for ref %q", opts.ref)
|
||||
}
|
||||
|
||||
if opts.ref == "" {
|
||||
slices.SortFunc(recs, func(a, b historyRecord) int {
|
||||
return b.CreatedAt.AsTime().Compare(a.CreatedAt.AsTime())
|
||||
})
|
||||
}
|
||||
|
||||
rec := &recs[0]
|
||||
|
||||
url := desktop.BuildURL(fmt.Sprintf("%s/%s/%s", rec.node.Builder, rec.node.Name, rec.Ref))
|
||||
return browser.OpenURL(url)
|
||||
}
|
||||
|
||||
func openCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
|
||||
var options openOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "open [OPTIONS] [REF]",
|
||||
Short: "Open a build in Docker Desktop",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
options.ref = args[0]
|
||||
}
|
||||
options.builder = *rootOpts.Builder
|
||||
return runOpen(cmd.Context(), dockerCli, options)
|
||||
},
|
||||
ValidArgsFunction: completion.Disable,
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
151
commands/history/rm.go
Normal file
151
commands/history/rm.go
Normal file
@ -0,0 +1,151 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/util/cobrautil/completion"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
controlapi "github.com/moby/buildkit/api/services/control"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type rmOptions struct {
|
||||
builder string
|
||||
refs []string
|
||||
all bool
|
||||
}
|
||||
|
||||
func runRm(ctx context.Context, dockerCli command.Cli, opts rmOptions) error {
|
||||
b, err := builder.New(dockerCli, builder.WithName(opts.builder))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodes, err := b.LoadNodes(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node.Err != nil {
|
||||
return node.Err
|
||||
}
|
||||
}
|
||||
|
||||
errs := make([][]error, len(opts.refs))
|
||||
for i := range errs {
|
||||
errs[i] = make([]error, len(nodes))
|
||||
}
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for i, node := range nodes {
|
||||
node := node
|
||||
eg.Go(func() error {
|
||||
if node.Driver == nil {
|
||||
return nil
|
||||
}
|
||||
c, err := node.Driver.Client(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
refs := opts.refs
|
||||
|
||||
if opts.all {
|
||||
serv, err := c.ControlClient().ListenBuildHistory(ctx, &controlapi.BuildHistoryRequest{
|
||||
EarlyExit: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer serv.CloseSend()
|
||||
|
||||
for {
|
||||
resp, err := serv.Recv()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
if resp.Type == controlapi.BuildHistoryEventType_COMPLETE {
|
||||
refs = append(refs, resp.Record.Ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for j, ref := range refs {
|
||||
_, err = c.ControlClient().UpdateBuildHistory(ctx, &controlapi.UpdateBuildHistoryRequest{
|
||||
Ref: ref,
|
||||
Delete: true,
|
||||
})
|
||||
if opts.all {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
errs[j][i] = err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var out []error
|
||||
loop0:
|
||||
for _, nodeErrs := range errs {
|
||||
var nodeErr error
|
||||
for _, err1 := range nodeErrs {
|
||||
if err1 == nil {
|
||||
continue loop0
|
||||
}
|
||||
if nodeErr == nil {
|
||||
nodeErr = err1
|
||||
} else {
|
||||
nodeErr = multierror.Append(nodeErr, err1)
|
||||
}
|
||||
}
|
||||
out = append(out, nodeErr)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(out) == 1 {
|
||||
return out[0]
|
||||
}
|
||||
return multierror.Append(out[0], out[1:]...)
|
||||
}
|
||||
|
||||
func rmCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
|
||||
var options rmOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "rm [OPTIONS] [REF...]",
|
||||
Short: "Remove build records",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 && !options.all {
|
||||
return errors.New("rm requires at least one argument")
|
||||
}
|
||||
if len(args) > 0 && options.all {
|
||||
return errors.New("rm requires either --all or at least one argument")
|
||||
}
|
||||
options.refs = args
|
||||
options.builder = *rootOpts.Builder
|
||||
return runRm(cmd.Context(), dockerCli, options)
|
||||
},
|
||||
ValidArgsFunction: completion.Disable,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&options.all, "all", false, "Remove all build records")
|
||||
|
||||
return cmd
|
||||
}
|
30
commands/history/root.go
Normal file
30
commands/history/root.go
Normal file
@ -0,0 +1,30 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"github.com/docker/buildx/util/cobrautil/completion"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type RootOptions struct {
|
||||
Builder *string
|
||||
}
|
||||
|
||||
func RootCmd(rootcmd *cobra.Command, dockerCli command.Cli, opts RootOptions) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "history",
|
||||
Short: "Commands to work on build records",
|
||||
ValidArgsFunction: completion.Disable,
|
||||
RunE: rootcmd.RunE,
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
lsCmd(dockerCli, opts),
|
||||
rmCmd(dockerCli, opts),
|
||||
logsCmd(dockerCli, opts),
|
||||
inspectCmd(dockerCli, opts),
|
||||
openCmd(dockerCli, opts),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
180
commands/history/utils.go
Normal file
180
commands/history/utils.go
Normal file
@ -0,0 +1,180 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/localstate"
|
||||
controlapi "github.com/moby/buildkit/api/services/control"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func buildName(fattrs map[string]string, ls *localstate.State) string {
|
||||
var res string
|
||||
|
||||
var target, contextPath, dockerfilePath, vcsSource string
|
||||
if v, ok := fattrs["target"]; ok {
|
||||
target = v
|
||||
}
|
||||
if v, ok := fattrs["context"]; ok {
|
||||
contextPath = filepath.ToSlash(v)
|
||||
} else if v, ok := fattrs["vcs:localdir:context"]; ok && v != "." {
|
||||
contextPath = filepath.ToSlash(v)
|
||||
}
|
||||
if v, ok := fattrs["vcs:source"]; ok {
|
||||
vcsSource = v
|
||||
}
|
||||
if v, ok := fattrs["filename"]; ok && v != "Dockerfile" {
|
||||
dockerfilePath = filepath.ToSlash(v)
|
||||
}
|
||||
if v, ok := fattrs["vcs:localdir:dockerfile"]; ok && v != "." {
|
||||
dockerfilePath = filepath.ToSlash(filepath.Join(v, dockerfilePath))
|
||||
}
|
||||
|
||||
var localPath string
|
||||
if ls != nil && !build.IsRemoteURL(ls.LocalPath) {
|
||||
if ls.LocalPath != "" && ls.LocalPath != "-" {
|
||||
localPath = filepath.ToSlash(ls.LocalPath)
|
||||
}
|
||||
if ls.DockerfilePath != "" && ls.DockerfilePath != "-" && ls.DockerfilePath != "Dockerfile" {
|
||||
dockerfilePath = filepath.ToSlash(ls.DockerfilePath)
|
||||
}
|
||||
}
|
||||
|
||||
// remove default dockerfile name
|
||||
const defaultFilename = "/Dockerfile"
|
||||
hasDefaultFileName := strings.HasSuffix(dockerfilePath, defaultFilename) || dockerfilePath == ""
|
||||
dockerfilePath = strings.TrimSuffix(dockerfilePath, defaultFilename)
|
||||
|
||||
// dockerfile is a subpath of context
|
||||
if strings.HasPrefix(dockerfilePath, localPath) && len(dockerfilePath) > len(localPath) {
|
||||
res = dockerfilePath[strings.LastIndex(localPath, "/")+1:]
|
||||
} else {
|
||||
// Otherwise, use basename
|
||||
bpath := localPath
|
||||
if len(dockerfilePath) > 0 {
|
||||
bpath = dockerfilePath
|
||||
}
|
||||
if len(bpath) > 0 {
|
||||
lidx := strings.LastIndex(bpath, "/")
|
||||
res = bpath[lidx+1:]
|
||||
if !hasDefaultFileName {
|
||||
if lidx != -1 {
|
||||
res = filepath.ToSlash(filepath.Join(filepath.Base(bpath[:lidx]), res))
|
||||
} else {
|
||||
res = filepath.ToSlash(filepath.Join(filepath.Base(bpath), res))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(contextPath) > 0 {
|
||||
res = contextPath
|
||||
}
|
||||
if len(target) > 0 {
|
||||
if len(res) > 0 {
|
||||
res = res + " (" + target + ")"
|
||||
} else {
|
||||
res = target
|
||||
}
|
||||
}
|
||||
if res == "" && vcsSource != "" {
|
||||
return vcsSource
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func trimBeginning(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return ".." + s[len(s)-n+2:]
|
||||
}
|
||||
|
||||
type historyRecord struct {
|
||||
*controlapi.BuildHistoryRecord
|
||||
currentTimestamp *time.Time
|
||||
node *builder.Node
|
||||
name string
|
||||
}
|
||||
|
||||
func queryRecords(ctx context.Context, ref string, nodes []builder.Node) ([]historyRecord, error) {
|
||||
var mu sync.Mutex
|
||||
var out []historyRecord
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for _, node := range nodes {
|
||||
node := node
|
||||
eg.Go(func() error {
|
||||
if node.Driver == nil {
|
||||
return nil
|
||||
}
|
||||
var records []historyRecord
|
||||
c, err := node.Driver.Client(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serv, err := c.ControlClient().ListenBuildHistory(ctx, &controlapi.BuildHistoryRequest{
|
||||
EarlyExit: true,
|
||||
Ref: ref,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
md, err := serv.Header()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var ts *time.Time
|
||||
if v, ok := md[headerKeyTimestamp]; ok {
|
||||
t, err := time.Parse(time.RFC3339Nano, v[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ts = &t
|
||||
}
|
||||
defer serv.CloseSend()
|
||||
for {
|
||||
he, err := serv.Recv()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
if he.Type == controlapi.BuildHistoryEventType_DELETED || he.Record == nil {
|
||||
continue
|
||||
}
|
||||
records = append(records, historyRecord{
|
||||
BuildHistoryRecord: he.Record,
|
||||
currentTimestamp: ts,
|
||||
node: &node,
|
||||
})
|
||||
}
|
||||
mu.Lock()
|
||||
out = append(out, records...)
|
||||
mu.Unlock()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func formatDuration(d time.Duration) string {
|
||||
if d < time.Minute {
|
||||
return fmt.Sprintf("%.1fs", d.Seconds())
|
||||
}
|
||||
return fmt.Sprintf("%dm %2ds", int(d.Minutes()), int(d.Seconds())%60)
|
||||
}
|
@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
|
||||
debugcmd "github.com/docker/buildx/commands/debug"
|
||||
historycmd "github.com/docker/buildx/commands/history"
|
||||
imagetoolscmd "github.com/docker/buildx/commands/imagetools"
|
||||
"github.com/docker/buildx/controller/remote"
|
||||
"github.com/docker/buildx/util/cobrautil/completion"
|
||||
@ -106,6 +107,7 @@ func addCommands(cmd *cobra.Command, opts *rootOptions, dockerCli command.Cli) {
|
||||
pruneCmd(dockerCli, opts),
|
||||
duCmd(dockerCli, opts),
|
||||
imagetoolscmd.RootCmd(cmd, dockerCli, imagetoolscmd.RootOptions{Builder: &opts.builder}),
|
||||
historycmd.RootCmd(cmd, dockerCli, historycmd.RootOptions{Builder: &opts.builder}),
|
||||
)
|
||||
if confutil.IsExperimental() {
|
||||
cmd.AddCommand(debugcmd.RootCmd(dockerCli,
|
||||
|
@ -17,6 +17,7 @@ Extended build capabilities with BuildKit
|
||||
| [`debug`](buildx_debug.md) | Start debugger (EXPERIMENTAL) |
|
||||
| [`dial-stdio`](buildx_dial-stdio.md) | Proxy current stdio streams to builder instance |
|
||||
| [`du`](buildx_du.md) | Disk usage |
|
||||
| [`history`](buildx_history.md) | Commands to work on build records |
|
||||
| [`imagetools`](buildx_imagetools.md) | Commands to work on images in registry |
|
||||
| [`inspect`](buildx_inspect.md) | Inspect current builder instance |
|
||||
| [`ls`](buildx_ls.md) | List builder instances |
|
||||
|
26
docs/reference/buildx_history.md
Normal file
26
docs/reference/buildx_history.md
Normal file
@ -0,0 +1,26 @@
|
||||
# docker buildx history
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Commands to work on build records
|
||||
|
||||
### Subcommands
|
||||
|
||||
| Name | Description |
|
||||
|:---------------------------------------|:-------------------------------|
|
||||
| [`inspect`](buildx_history_inspect.md) | Inspect a build |
|
||||
| [`logs`](buildx_history_logs.md) | Print the logs of a build |
|
||||
| [`ls`](buildx_history_ls.md) | List build records |
|
||||
| [`open`](buildx_history_open.md) | Open a build in Docker Desktop |
|
||||
| [`rm`](buildx_history_rm.md) | Remove build records |
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------|:---------|:--------|:-----------------------------------------|
|
||||
| `--builder` | `string` | | Override the configured builder instance |
|
||||
| `-D`, `--debug` | `bool` | | Enable debug logging |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
22
docs/reference/buildx_history_inspect.md
Normal file
22
docs/reference/buildx_history_inspect.md
Normal file
@ -0,0 +1,22 @@
|
||||
# docker buildx history inspect
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Inspect a build
|
||||
|
||||
### Subcommands
|
||||
|
||||
| Name | Description |
|
||||
|:-----------------------------------------------------|:---------------------------|
|
||||
| [`attachment`](buildx_history_inspect_attachment.md) | Inspect a build attachment |
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------|:---------|:--------|:-----------------------------------------|
|
||||
| `--builder` | `string` | | Override the configured builder instance |
|
||||
| `-D`, `--debug` | `bool` | | Enable debug logging |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
17
docs/reference/buildx_history_inspect_attachment.md
Normal file
17
docs/reference/buildx_history_inspect_attachment.md
Normal file
@ -0,0 +1,17 @@
|
||||
# docker buildx history inspect attachment
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Inspect a build attachment
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------|:---------|:--------|:-----------------------------------------|
|
||||
| `--builder` | `string` | | Override the configured builder instance |
|
||||
| `-D`, `--debug` | `bool` | | Enable debug logging |
|
||||
| `--platform` | `string` | | Platform of attachment |
|
||||
| `--type` | `string` | | Type of attachment |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
16
docs/reference/buildx_history_logs.md
Normal file
16
docs/reference/buildx_history_logs.md
Normal file
@ -0,0 +1,16 @@
|
||||
# docker buildx history logs
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Print the logs of a build
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------|:---------|:--------|:--------------------------------------------------|
|
||||
| `--builder` | `string` | | Override the configured builder instance |
|
||||
| `-D`, `--debug` | `bool` | | Enable debug logging |
|
||||
| `--progress` | `string` | `plain` | Set type of progress output (plain, rawjson, tty) |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
17
docs/reference/buildx_history_ls.md
Normal file
17
docs/reference/buildx_history_ls.md
Normal file
@ -0,0 +1,17 @@
|
||||
# docker buildx history ls
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
List build records
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------|:---------|:--------|:-----------------------------------------|
|
||||
| `--builder` | `string` | | Override the configured builder instance |
|
||||
| `-D`, `--debug` | `bool` | | Enable debug logging |
|
||||
| `--format` | `string` | `table` | Format the output |
|
||||
| `--no-trunc` | `bool` | | Don't truncate output |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
15
docs/reference/buildx_history_open.md
Normal file
15
docs/reference/buildx_history_open.md
Normal file
@ -0,0 +1,15 @@
|
||||
# docker buildx history open
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Open a build in Docker Desktop
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------|:---------|:--------|:-----------------------------------------|
|
||||
| `--builder` | `string` | | Override the configured builder instance |
|
||||
| `-D`, `--debug` | `bool` | | Enable debug logging |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
16
docs/reference/buildx_history_rm.md
Normal file
16
docs/reference/buildx_history_rm.md
Normal file
@ -0,0 +1,16 @@
|
||||
# docker buildx history rm
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Remove build records
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------|:---------|:--------|:-----------------------------------------|
|
||||
| `--all` | `bool` | | Remove all build records |
|
||||
| `--builder` | `string` | | Override the configured builder instance |
|
||||
| `-D`, `--debug` | `bool` | | Enable debug logging |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
5
go.mod
5
go.mod
@ -25,6 +25,7 @@ require (
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-cty-funcs v0.0.0-20241120183456-c51673e0b3dd
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/hcl/v2 v2.23.0
|
||||
github.com/in-toto/in-toto-golang v0.5.0
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
@ -35,6 +36,7 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10
|
||||
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b
|
||||
@ -49,6 +51,7 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.31.0
|
||||
go.opentelemetry.io/otel/sdk v1.31.0
|
||||
go.opentelemetry.io/otel/trace v1.31.0
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0
|
||||
golang.org/x/mod v0.21.0
|
||||
golang.org/x/sync v0.10.0
|
||||
golang.org/x/sys v0.28.0
|
||||
@ -114,7 +117,6 @@ require (
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
@ -166,7 +168,6 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
|
2
go.sum
2
go.sum
@ -359,6 +359,8 @@ github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsq
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
|
@ -28,13 +28,14 @@ func BuildBackendEnabled() bool {
|
||||
return bbEnabled
|
||||
}
|
||||
|
||||
func BuildURL(ref string) string {
|
||||
return fmt.Sprintf("docker-desktop://dashboard/build/%s", ref)
|
||||
}
|
||||
|
||||
func BuildDetailsOutput(refs map[string]string, term bool) string {
|
||||
if len(refs) == 0 {
|
||||
return ""
|
||||
}
|
||||
refURL := func(ref string) string {
|
||||
return fmt.Sprintf("docker-desktop://dashboard/build/%s", ref)
|
||||
}
|
||||
var out bytes.Buffer
|
||||
out.WriteString("View build details: ")
|
||||
multiTargets := len(refs) > 1
|
||||
@ -43,9 +44,10 @@ func BuildDetailsOutput(refs map[string]string, term bool) string {
|
||||
out.WriteString(fmt.Sprintf("\n %s: ", target))
|
||||
}
|
||||
if term {
|
||||
out.WriteString(hyperlink(refURL(ref)))
|
||||
url := BuildURL(ref)
|
||||
out.WriteString(ANSIHyperlink(url, url))
|
||||
} else {
|
||||
out.WriteString(refURL(ref))
|
||||
out.WriteString(BuildURL(ref))
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
@ -57,9 +59,9 @@ func PrintBuildDetails(w io.Writer, refs map[string]string, term bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func hyperlink(url string) string {
|
||||
func ANSIHyperlink(url, text string) string {
|
||||
// create an escape sequence using the OSC 8 format: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
||||
return fmt.Sprintf("\033]8;;%s\033\\%s\033]8;;\033\\", url, url)
|
||||
return fmt.Sprintf("\033]8;;%s\033\\%s\033]8;;\033\\", url, text)
|
||||
}
|
||||
|
||||
type ErrorWithBuildRef struct {
|
||||
|
23
vendor/github.com/pkg/browser/LICENSE
generated
vendored
Normal file
23
vendor/github.com/pkg/browser/LICENSE
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
Copyright (c) 2014, Dave Cheney <dave@cheney.net>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
55
vendor/github.com/pkg/browser/README.md
generated
vendored
Normal file
55
vendor/github.com/pkg/browser/README.md
generated
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
|
||||
# browser
|
||||
import "github.com/pkg/browser"
|
||||
|
||||
Package browser provides helpers to open files, readers, and urls in a browser window.
|
||||
|
||||
The choice of which browser is started is entirely client dependant.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Variables
|
||||
``` go
|
||||
var Stderr io.Writer = os.Stderr
|
||||
```
|
||||
Stderr is the io.Writer to which executed commands write standard error.
|
||||
|
||||
``` go
|
||||
var Stdout io.Writer = os.Stdout
|
||||
```
|
||||
Stdout is the io.Writer to which executed commands write standard output.
|
||||
|
||||
|
||||
## func OpenFile
|
||||
``` go
|
||||
func OpenFile(path string) error
|
||||
```
|
||||
OpenFile opens new browser window for the file path.
|
||||
|
||||
|
||||
## func OpenReader
|
||||
``` go
|
||||
func OpenReader(r io.Reader) error
|
||||
```
|
||||
OpenReader consumes the contents of r and presents the
|
||||
results in a new browser window.
|
||||
|
||||
|
||||
## func OpenURL
|
||||
``` go
|
||||
func OpenURL(url string) error
|
||||
```
|
||||
OpenURL opens a new browser window pointing to url.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- - -
|
||||
Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md)
|
57
vendor/github.com/pkg/browser/browser.go
generated
vendored
Normal file
57
vendor/github.com/pkg/browser/browser.go
generated
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
// Package browser provides helpers to open files, readers, and urls in a browser window.
|
||||
//
|
||||
// The choice of which browser is started is entirely client dependant.
|
||||
package browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Stdout is the io.Writer to which executed commands write standard output.
|
||||
var Stdout io.Writer = os.Stdout
|
||||
|
||||
// Stderr is the io.Writer to which executed commands write standard error.
|
||||
var Stderr io.Writer = os.Stderr
|
||||
|
||||
// OpenFile opens new browser window for the file path.
|
||||
func OpenFile(path string) error {
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return OpenURL("file://" + path)
|
||||
}
|
||||
|
||||
// OpenReader consumes the contents of r and presents the
|
||||
// results in a new browser window.
|
||||
func OpenReader(r io.Reader) error {
|
||||
f, err := ioutil.TempFile("", "browser.*.html")
|
||||
if err != nil {
|
||||
return fmt.Errorf("browser: could not create temporary file: %v", err)
|
||||
}
|
||||
if _, err := io.Copy(f, r); err != nil {
|
||||
f.Close()
|
||||
return fmt.Errorf("browser: caching temporary file failed: %v", err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return fmt.Errorf("browser: caching temporary file failed: %v", err)
|
||||
}
|
||||
return OpenFile(f.Name())
|
||||
}
|
||||
|
||||
// OpenURL opens a new browser window pointing to url.
|
||||
func OpenURL(url string) error {
|
||||
return openBrowser(url)
|
||||
}
|
||||
|
||||
func runCmd(prog string, args ...string) error {
|
||||
cmd := exec.Command(prog, args...)
|
||||
cmd.Stdout = Stdout
|
||||
cmd.Stderr = Stderr
|
||||
return cmd.Run()
|
||||
}
|
5
vendor/github.com/pkg/browser/browser_darwin.go
generated
vendored
Normal file
5
vendor/github.com/pkg/browser/browser_darwin.go
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
package browser
|
||||
|
||||
func openBrowser(url string) error {
|
||||
return runCmd("open", url)
|
||||
}
|
14
vendor/github.com/pkg/browser/browser_freebsd.go
generated
vendored
Normal file
14
vendor/github.com/pkg/browser/browser_freebsd.go
generated
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func openBrowser(url string) error {
|
||||
err := runCmd("xdg-open", url)
|
||||
if e, ok := err.(*exec.Error); ok && e.Err == exec.ErrNotFound {
|
||||
return errors.New("xdg-open: command not found - install xdg-utils from ports(8)")
|
||||
}
|
||||
return err
|
||||
}
|
21
vendor/github.com/pkg/browser/browser_linux.go
generated
vendored
Normal file
21
vendor/github.com/pkg/browser/browser_linux.go
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func openBrowser(url string) error {
|
||||
providers := []string{"xdg-open", "x-www-browser", "www-browser"}
|
||||
|
||||
// There are multiple possible providers to open a browser on linux
|
||||
// One of them is xdg-open, another is x-www-browser, then there's www-browser, etc.
|
||||
// Look for one that exists and run it
|
||||
for _, provider := range providers {
|
||||
if _, err := exec.LookPath(provider); err == nil {
|
||||
return runCmd(provider, url)
|
||||
}
|
||||
}
|
||||
|
||||
return &exec.Error{Name: strings.Join(providers, ","), Err: exec.ErrNotFound}
|
||||
}
|
14
vendor/github.com/pkg/browser/browser_netbsd.go
generated
vendored
Normal file
14
vendor/github.com/pkg/browser/browser_netbsd.go
generated
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func openBrowser(url string) error {
|
||||
err := runCmd("xdg-open", url)
|
||||
if e, ok := err.(*exec.Error); ok && e.Err == exec.ErrNotFound {
|
||||
return errors.New("xdg-open: command not found - install xdg-utils from pkgsrc(7)")
|
||||
}
|
||||
return err
|
||||
}
|
14
vendor/github.com/pkg/browser/browser_openbsd.go
generated
vendored
Normal file
14
vendor/github.com/pkg/browser/browser_openbsd.go
generated
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func openBrowser(url string) error {
|
||||
err := runCmd("xdg-open", url)
|
||||
if e, ok := err.(*exec.Error); ok && e.Err == exec.ErrNotFound {
|
||||
return errors.New("xdg-open: command not found - install xdg-utils from ports(8)")
|
||||
}
|
||||
return err
|
||||
}
|
12
vendor/github.com/pkg/browser/browser_unsupported.go
generated
vendored
Normal file
12
vendor/github.com/pkg/browser/browser_unsupported.go
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
// +build !linux,!windows,!darwin,!openbsd,!freebsd,!netbsd
|
||||
|
||||
package browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func openBrowser(url string) error {
|
||||
return fmt.Errorf("openBrowser: unsupported operating system: %v", runtime.GOOS)
|
||||
}
|
7
vendor/github.com/pkg/browser/browser_windows.go
generated
vendored
Normal file
7
vendor/github.com/pkg/browser/browser_windows.go
generated
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
package browser
|
||||
|
||||
import "golang.org/x/sys/windows"
|
||||
|
||||
func openBrowser(url string) error {
|
||||
return windows.ShellExecute(0, nil, windows.StringToUTF16Ptr(url), nil, nil, windows.SW_SHOWNORMAL)
|
||||
}
|
3
vendor/modules.txt
vendored
3
vendor/modules.txt
vendored
@ -636,6 +636,9 @@ github.com/opencontainers/image-spec/specs-go/v1
|
||||
# github.com/pelletier/go-toml v1.9.5
|
||||
## explicit; go 1.12
|
||||
github.com/pelletier/go-toml
|
||||
# github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||
## explicit; go 1.14
|
||||
github.com/pkg/browser
|
||||
# github.com/pkg/errors v0.9.1
|
||||
## explicit
|
||||
github.com/pkg/errors
|
||||
|
Loading…
x
Reference in New Issue
Block a user