feat(sg): add images [list|build] command (#62030)

This commit is contained in:
Jean-Hadrien Chabran 2024-04-19 13:41:41 +02:00 committed by GitHub
parent 1f09814d6f
commit da08670715
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 197 additions and 0 deletions

View File

@ -21,6 +21,7 @@ go_library(
"sg_embeddings_qa.go",
"sg_generate.go",
"sg_help.go",
"sg_images.go",
"sg_insights.go",
"sg_install.go",
"sg_lint.go",
@ -141,6 +142,7 @@ go_test(
timeout = "short",
srcs = [
"main_test.go",
"sg_images_test.go",
"sg_start_test.go",
],
# Required by func findRoot() to check if sg is running in sourcegraph/sourcegraph

View File

@ -283,6 +283,7 @@ var sg = &cli.App{
setupCommand,
srcCommand,
srcInstanceCommand,
imagesCommand,
// Company
teammateCommand,

162
dev/sg/sg_images.go Normal file
View File

@ -0,0 +1,162 @@
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/grafana/regexp"
"github.com/urfave/cli/v2"
"github.com/sourcegraph/run"
"github.com/sourcegraph/sourcegraph/dev/sg/internal/category"
"github.com/sourcegraph/sourcegraph/dev/sg/internal/std"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/lib/output"
)
var imagesCommand = &cli.Command{
Name: "images",
Usage: "Commands for interacting with containers",
Category: category.Dev,
Subcommands: []*cli.Command{
{
Name: "list",
Usage: "List container images available to build",
Action: func(cctx *cli.Context) error {
lines, err := listBazelOCITarballs(cctx.Context)
if err != nil {
return err
}
std.Out.WriteLine(output.Styledf(output.StylePending, "Found %d targets", len(lines)))
std.Out.WriteLine(output.Styledf(output.StyleReset, strings.Join(lines, "\n")))
std.Out.WriteLine(output.Styledf(output.StylePending, "💡 You can build and load the above targets using glob patterns with 'sg images build [pattern]..."))
std.Out.WriteLine(output.Styledf(output.StylePending, "💡 Examples:"))
std.Out.WriteLine(output.Styledf(output.StyleReset, " 'worker' to match //cmd/worker:image_tarball"))
std.Out.WriteLine(output.Styledf(output.StyleReset, " 'cmd/*' to match all containers under //cmd/"))
std.Out.WriteLine(output.Styledf(output.StyleReset, " 'postgres*' to match"))
std.Out.WriteLine(output.Styledf(output.StyleReset, " //docker-images/postgres-12-alpine:base_tarball//docker-images/postgres-12-alpine:image_tarball"))
std.Out.WriteLine(output.Styledf(output.StyleReset, " //docker-images/postgres-12-alpine:legacy_tarball"))
std.Out.WriteLine(output.Styledf(output.StyleReset, " //docker-images/postgres_exporter:base_tarball"))
std.Out.WriteLine(output.Styledf(output.StyleReset, " //docker-images/postgres_exporter:image_tarball"))
return nil
},
},
{
Name: "build",
Usage: "builds a container image by matching [pattern] using glob syntax to the target.\nExamples:\n- sg images build worker\n- sg images build cmd/*\n- sg images build postgres*",
UsageText: "build [pattern1] ([pattern2] ...)",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "load",
Usage: "Load the image into the local Docker daemon",
Value: true,
},
},
Action: func(cctx *cli.Context) error {
allTargets, err := listBazelOCITarballs(cctx.Context)
if err != nil {
return err
}
patterns := cctx.Args().Slice()
selectedTargets := []string{}
for _, name := range patterns {
if strings.HasPrefix(name, "//") {
std.Out.WriteLine(output.Styledf(output.StyleYellow, "Detected a Bazel target path (%q) aborting.", name))
std.Out.WriteLine(output.Styledf(output.StyleBold, "Run the following command instead:"))
std.Out.WriteLine(output.Styledf(output.StyleReset, "Building the image without loading it:\n sg bazel build %s", name))
std.Out.WriteLine(output.Styledf(output.StyleReset, "Building the image and loading it:\n sg bazel run %s", name))
return nil
}
}
for _, target := range allTargets {
for _, name := range patterns {
var ok bool
var err error
if strings.Contains(name, "/") {
ok, err = filepath.Match(name, trimImageTarballTarget(target))
} else {
ok, err = filepath.Match(fmt.Sprintf("*/%s", name), trimImageTarballTarget(target))
}
if err != nil {
return err
}
if ok {
selectedTargets = append(selectedTargets, target)
}
}
}
std.Out.WriteLine(output.Styledf(output.StylePending, "Found Bazel targets: \n%s", strings.Join(selectedTargets, "\n")))
commandText := fmt.Sprintf("sg bazel build %s", strings.Join(selectedTargets, " "))
std.Out.WriteLine(output.Styledf(output.StyleBold, "Running Bazel 'build' command for you"))
std.Out.WriteLine(output.Styledf(output.StyleYellow, " "+commandText))
std.Out.WriteLine(output.Styledf(output.StyleReset, "--- 👇 Bazel output ---"))
// Please note the added --color flag here, to ensure we keep the colors when streaming back the output.
// And we run `bazel` directly, because we know that for build/run you don't need `sg bazel` but our users
// don't have to know that.
cmd := run.Bash(cctx.Context, fmt.Sprintf("bazel build --color=yes %s", strings.Join(selectedTargets, " ")))
if err := cmd.Run().Stream(os.Stdout); err != nil {
return err
}
std.Out.WriteLine(output.Styledf(output.StyleReset, "----------------------"))
if cctx.Bool("load") {
for _, target := range selectedTargets {
commandText := fmt.Sprintf("bazel run %s", target)
std.Out.WriteLine(output.Styledf(output.StyleBold, "Running Bazel command for you"))
std.Out.WriteLine(output.Styledf(output.StyleYellow, " "+commandText))
std.Out.WriteLine(output.Styledf(output.StyleReset, "--- 👇 Bazel output ---"))
// Please note the added --color flag here, to ensure we keep the colors when streaming back the output.
cmd := run.Bash(cctx.Context, fmt.Sprintf("sg bazel run --color=yes %s", target))
if err := cmd.Run().Stream(os.Stdout); err != nil {
return err
}
std.Out.WriteLine(output.Styledf(output.StyleReset, "----------------------"))
}
}
return nil
},
},
},
}
func listBazelOCITarballs(ctx context.Context) ([]string, error) {
cmd := run.Cmd(ctx, "bazel", "query", "kind('oci_tarball', //...)")
lines, err := cmd.Run().Lines()
if err != nil {
return nil, errors.Wrap(err, "failed to list bazel images")
}
if len(lines) > 0 {
if strings.HasPrefix(lines[0], "Loading") {
// Trim the first line, which is just "Loading: (...)"
lines = lines[1:]
}
}
return lines, nil
}
// trimImageTarballTarget simplifies the Bazel target path for easier matching with glob syntax.
// For example, the target '//cmd/worker:image_tarball' becomes 'worker'.
func trimImageTarballTarget(target string) string {
// Remove the leading '//'
target = strings.TrimPrefix(target, "//")
// Remove the trailing ':.*'
reg := regexp.MustCompile(":.*")
target = reg.ReplaceAllString(target, "")
return target
}

32
dev/sg/sg_images_test.go Normal file
View File

@ -0,0 +1,32 @@
package main
import (
"fmt"
"testing"
)
func Test_trimImageTarballTarget(t *testing.T) {
tests := []struct {
target string
want string
}{
{
target: "//cmd/worker:image_tarball",
want: "cmd/worker",
},
{
target: "//docker-images/caddy:image_tarball",
want: "docker-images/caddy",
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%q to %q", tt.target, tt.want), func(t *testing.T) {
got := trimImageTarballTarget(tt.target)
if got != tt.want {
t.Logf("got %q but wanted %q", got, tt.want)
t.Fail()
}
})
}
}