diff --git a/src/cmd/linuxkit/pkg.go b/src/cmd/linuxkit/pkg.go index 4b9e7ab63..cacf2c3e3 100644 --- a/src/cmd/linuxkit/pkg.go +++ b/src/cmd/linuxkit/pkg.go @@ -12,6 +12,7 @@ func pkgUsage() { fmt.Printf("'subcommand' is one of:\n") fmt.Printf(" build\n") + fmt.Printf(" builder\n") fmt.Printf(" push\n") fmt.Printf(" show-tag\n") fmt.Printf("\n") @@ -28,6 +29,8 @@ func pkg(args []string) { switch args[0] { case "build": pkgBuild(args[1:]) + case "builder": + pkgBuilder(args[1:]) case "push": pkgPush(args[1:]) case "show-tag": diff --git a/src/cmd/linuxkit/pkg_builder.go b/src/cmd/linuxkit/pkg_builder.go new file mode 100644 index 000000000..6abb6db23 --- /dev/null +++ b/src/cmd/linuxkit/pkg_builder.go @@ -0,0 +1,84 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/linuxkit/linuxkit/src/cmd/linuxkit/pkglib" + log "github.com/sirupsen/logrus" +) + +func pkgBuilderUsage() { + invoked := filepath.Base(os.Args[0]) + fmt.Printf("USAGE: %s builder command [options]\n\n", invoked) + fmt.Printf("Supported commands are\n") + // Please keep these in alphabetical order + fmt.Printf(" du\n") + fmt.Printf(" prune\n") + fmt.Printf("\n") + fmt.Printf("'options' are the backend specific options.\n") + fmt.Printf("See '%s builder [command] --help' for details.\n\n", invoked) +} + +// Process the builder +func pkgBuilder(args []string) { + if len(args) < 1 { + pkgBuilderUsage() + os.Exit(1) + } + switch args[0] { + // Please keep cases in alphabetical order + case "du": + pkgBuilderCommands(args[0], args[1:]) + case "prune": + pkgBuilderCommands(args[0], args[1:]) + case "help", "-h", "-help", "--help": + pkgBuilderUsage() + os.Exit(0) + default: + log.Errorf("No 'builder' command specified.") + } +} + +func pkgBuilderCommands(command string, args []string) { + flags := flag.NewFlagSet(command, flag.ExitOnError) + builders := flags.String("builders", "", "Which builders to use for which platforms, e.g. linux/arm64=docker-context-arm64, overrides defaults and environment variables, see https://github.com/linuxkit/linuxkit/blob/master/docs/packages.md#Providing-native-builder-nodes") + platforms := flags.String("platforms", fmt.Sprintf("linux/%s", runtime.GOARCH), "Which platforms we built images for") + builderImage := flags.String("builder-image", defaultBuilderImage, "buildkit builder container image to use") + verbose := flags.Bool("v", false, "Verbose output") + if err := flags.Parse(args); err != nil { + log.Fatal("Unable to parse args") + } + // build the builders map + buildersMap := make(map[string]string) + // look for builders env var + buildersMap, err := buildPlatformBuildersMap(os.Getenv(buildersEnvVar), buildersMap) + if err != nil { + log.Fatalf("%s in environment variable %s\n", err.Error(), buildersEnvVar) + } + // any CLI options override env var + buildersMap, err = buildPlatformBuildersMap(*builders, buildersMap) + if err != nil { + log.Fatalf("%s in --builders flag\n", err.Error()) + } + + platformsToClean := strings.Split(*platforms, ",") + switch command { + case "du": + if err := pkglib.DiskUsage(buildersMap, *builderImage, platformsToClean, *verbose); err != nil { + log.Fatalf("Unable to print disk usage of builder: %v", err) + } + case "prune": + if err := pkglib.PruneBuilder(buildersMap, *builderImage, platformsToClean, *verbose); err != nil { + log.Fatalf("Unable to prune builder: %v", err) + } + default: + log.Errorf("unexpected command %s", command) + pkgBuilderUsage() + os.Exit(1) + } +} diff --git a/src/cmd/linuxkit/pkglib/build_test.go b/src/cmd/linuxkit/pkglib/build_test.go index 7f50ff185..a4bbc2b8c 100644 --- a/src/cmd/linuxkit/pkglib/build_test.go +++ b/src/cmd/linuxkit/pkglib/build_test.go @@ -19,6 +19,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/types" "github.com/linuxkit/linuxkit/src/cmd/linuxkit/spec" lktspec "github.com/linuxkit/linuxkit/src/cmd/linuxkit/spec" + buildkitClient "github.com/moby/buildkit/client" imagespec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -52,7 +53,9 @@ func (d *dockerMocker) contextSupportCheck() error { } return errors.New("contexts not supported") } - +func (d *dockerMocker) builder(_ context.Context, _, _, _ string, _ bool) (*buildkitClient.Client, error) { + return nil, fmt.Errorf("not implemented") +} func (d *dockerMocker) build(ctx context.Context, tag, pkg, dockerContext, builderImage, platform string, builderRestart bool, c spec.CacheProvider, r io.Reader, stdout io.Writer, imageBuildOpts dockertypes.ImageBuildOptions) error { if !d.enableBuild { return errors.New("build disabled") diff --git a/src/cmd/linuxkit/pkglib/docker.go b/src/cmd/linuxkit/pkglib/docker.go index a3a11131d..55450298b 100644 --- a/src/cmd/linuxkit/pkglib/docker.go +++ b/src/cmd/linuxkit/pkglib/docker.go @@ -57,6 +57,7 @@ type dockerRunner interface { load(src io.Reader) error pull(img string) (bool, error) contextSupportCheck() error + builder(ctx context.Context, dockerContext, builderImage, platform string, restart bool) (*buildkitClient.Client, error) } type dockerRunnerImpl struct { diff --git a/src/cmd/linuxkit/pkglib/utils.go b/src/cmd/linuxkit/pkglib/utils.go new file mode 100644 index 000000000..c9c13e267 --- /dev/null +++ b/src/cmd/linuxkit/pkglib/utils.go @@ -0,0 +1,183 @@ +package pkglib + +import ( + "context" + "fmt" + "io" + "os" + "strings" + "text/tabwriter" + "time" + + "github.com/containerd/containerd/platforms" + "github.com/docker/go-units" + buildkitClient "github.com/moby/buildkit/client" +) + +func printTableHeader(tw *tabwriter.Writer) { + fmt.Fprintln(tw, "ID\tRECLAIMABLE\tSIZE\tLAST ACCESSED") +} + +func printTableRow(tw *tabwriter.Writer, di *buildkitClient.UsageInfo) { + id := di.ID + if di.Mutable { + id += "*" + } + size := units.HumanSize(float64(di.Size)) + if di.Shared { + size += "*" + } + lastAccessed := "" + if di.LastUsedAt != nil { + lastAccessed = units.HumanDuration(time.Since(*di.LastUsedAt)) + " ago" + } + fmt.Fprintf(tw, "%-40s\t%-5v\t%-10s\t%s\n", id, !di.InUse, size, lastAccessed) +} + +func printSummary(tw *tabwriter.Writer, du []*buildkitClient.UsageInfo) { + total := int64(0) + reclaimable := int64(0) + shared := int64(0) + + for _, di := range du { + if di.Size > 0 { + total += di.Size + if !di.InUse { + reclaimable += di.Size + } + } + if di.Shared { + shared += di.Size + } + } + + if shared > 0 { + fmt.Fprintf(tw, "Shared:\t%s\n", units.HumanSize(float64(shared))) + fmt.Fprintf(tw, "Private:\t%s\n", units.HumanSize(float64(total-shared))) + } + + fmt.Fprintf(tw, "Reclaimable:\t%s\n", units.HumanSize(float64(reclaimable))) + fmt.Fprintf(tw, "Total:\t%s\n", units.HumanSize(float64(total))) + tw.Flush() +} + +func printKV(w io.Writer, k string, v interface{}) { + fmt.Fprintf(w, "%s:\t%v\n", k, v) +} + +func printVerbose(tw *tabwriter.Writer, du []*buildkitClient.UsageInfo) { + for _, di := range du { + printKV(tw, "ID", di.ID) + if len(di.Parents) != 0 { + printKV(tw, "Parent", strings.Join(di.Parents, ",")) + } + printKV(tw, "Created at", di.CreatedAt) + printKV(tw, "Mutable", di.Mutable) + printKV(tw, "Reclaimable", !di.InUse) + printKV(tw, "Shared", di.Shared) + printKV(tw, "Size", units.HumanSize(float64(di.Size))) + if di.Description != "" { + printKV(tw, "Description", di.Description) + } + printKV(tw, "Usage count", di.UsageCount) + if di.LastUsedAt != nil { + printKV(tw, "Last used", units.HumanDuration(time.Since(*di.LastUsedAt))+" ago") + } + if di.RecordType != "" { + printKV(tw, "Type", di.RecordType) + } + + fmt.Fprintf(tw, "\n") + } + + tw.Flush() +} + +func getClientForPlatform(ctx context.Context, buildersMap map[string]string, builderImage, platform string) (*buildkitClient.Client, error) { + p, err := platforms.Parse(platform) + if err != nil { + return nil, fmt.Errorf("failed to parse platform: %s", err) + } + dr := newDockerRunner(false) + builderName := getBuilderForPlatform(p.Architecture, buildersMap) + client, err := dr.builder(ctx, builderName, builderImage, platform, false) + if err != nil { + return nil, fmt.Errorf("unable to ensure builder container: %v", err) + } + return client, nil +} + +// DiskUsage of builder +func DiskUsage(buildersMap map[string]string, builderImage string, platformsToClean []string, verbose bool) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + for _, platform := range platformsToClean { + client, err := getClientForPlatform(ctx, buildersMap, builderImage, platform) + if err != nil { + return fmt.Errorf("cannot get client: %s", err) + } + + du, err := client.DiskUsage(ctx) + if err != nil { + _ = client.Close() + return err + } + err = client.Close() + if err != nil { + return fmt.Errorf("cannot close client: %s", err) + } + tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0) + if len(du) > 0 { + if verbose { + printVerbose(tw, du) + } else { + printTableHeader(tw) + for _, di := range du { + printTableRow(tw, di) + } + } + } + printSummary(tw, du) + } + return nil +} + +// PruneBuilder clean build cache of builder +func PruneBuilder(buildersMap map[string]string, builderImage string, platformsToClean []string, verbose bool) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + total := int64(0) + for _, platform := range platformsToClean { + client, err := getClientForPlatform(ctx, buildersMap, builderImage, platform) + if err != nil { + return fmt.Errorf("cannot get client: %s", err) + } + + ch := make(chan buildkitClient.UsageInfo) + processed := make(chan struct{}) + + go func() { + defer close(processed) + for du := range ch { + if verbose { + fmt.Printf("%s\t%s\tremoved\n", du.ID, units.HumanSize(float64(du.Size))) + } + total += du.Size + } + }() + err = client.Prune(ctx, ch) + if err != nil { + _ = client.Close() + close(ch) + return err + } + err = client.Close() + if err != nil { + return fmt.Errorf("cannot close client: %s", err) + } + close(ch) + <-processed + } + fmt.Printf("Reclaimed:\t%s\n", units.BytesSize(float64(total))) + return nil +}