Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow comma-separated list of platforms #259

Merged
merged 8 commits into from
Dec 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,9 @@ script:
- go install -mod=vendor ./cmd/ko
# Try with all, and GOOS/GOARCH set.
- |
GOOS=${GOOS} GOARCH=${GOARCH} KO_DOCKER_REPO=ko.local ko publish --platform=all -B ./cmd/ko/test
OUTPUT=$(docker run -i ko.local/test -wait=false 2>&1)
if [[ ! "${OUTPUT}" =~ "$(cat ./cmd/ko/test/kodata/kenobi)" ]]; then
echo Mismatched output: ${OUTPUT}, wanted: $(cat ./cmd/ko/test/kodata/kenobi)
exit 1
fi
if [[ ! "${OUTPUT}" =~ "$(cat ./cmd/ko/test/kodata/HEAD)" ]]; then
echo Mismatched output: ${OUTPUT}, wanted: $(cat ./cmd/ko/test/kodata/HEAD)
OUTPUT=$(GOOS=${GOOS} GOARCH=${GOARCH} KO_DOCKER_REPO=ko.local ko publish --platform=all -B ./cmd/ko/test 2>&1)
if [[ ! "${OUTPUT}" =~ "cannot use --platform with GOOS=\"${GOOS}\"" ]]; then
echo Mismatched output: ${OUTPUT}, wanted: "cannot use --platform with GOOS=\"${GOOS}\""
exit 1
fi

Expand Down
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,13 +454,28 @@ a multi-architecture
(aka "manifest list"), with support for each OS and architecture pair supported
by the base image.

If `ko` is invoked with `--platform=<some-OS>/<some-platform>` (e.g.,
If `ko` is invoked with `--platform=<some-OS>/<some-arch>` (e.g.,
`--platform=linux/amd64` or `--platform=linux/arm64`), then it will attempt to
build an image for that OS and architecture only, assuming the base image
supports it.

When `--platform` is not provided, `ko` builds an image with the OS and
architecture based on the build environment's `GOOS` and `GOARCH`.
If the base image is a manifest list with more platforms than you want to build,
invoking `ko` with comma-separated list of platforms (e.g.
`--platform=linux/amd64,linux/arm/v6`) will produce a manifest list
containing only the provided platforms. Note that if the base image does not
contain platforms that are provided by this flag, `ko` will be unable to build
a corresponding image, and this is not an error. The resulting artifact will be
a multi-platform image containing the intersection of platforms from the base
image and the `--platform` flag. This is especially relevant for projects that
use multiple base images, as you must ensure that every base image contains all
the platforms that you'd like to build.

When `--platform` is not provided, if both `GOOS` and `GOARCH` environment
variables are set, `ko` will build an image for `${GOOS}/${GOARCH}[/v${GOARM}]`,
otherwise `ko` will build a `linux/amd64` image.

Note that using both `--platform` and GOOS/GOARCH will return an error, as it's
unclear what platform should be used.

## Enable Autocompletion

Expand Down
71 changes: 71 additions & 0 deletions pkg/build/gobuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,19 @@ type buildContext interface {
Import(path string, srcDir string, mode gb.ImportMode) (*gb.Package, error)
}

type platformMatcher struct {
spec string
platforms []v1.Platform
}

type gobuild struct {
getBase GetBase
creationTime v1.Time
build builder
disableOptimizations bool
mod *modules
buildContext buildContext
platformMatcher *platformMatcher
}

// Option is a functional option for NewGo.
Expand All @@ -73,19 +79,25 @@ type gobuildOpener struct {
disableOptimizations bool
mod *modules
buildContext buildContext
platform string
}

func (gbo *gobuildOpener) Open() (Interface, error) {
if gbo.getBase == nil {
return nil, errors.New("a way of providing base images must be specified, see build.WithBaseImages")
}
matcher, err := parseSpec(gbo.platform)
if err != nil {
return nil, err
}
return &gobuild{
getBase: gbo.getBase,
creationTime: gbo.creationTime,
build: gbo.build,
disableOptimizations: gbo.disableOptimizations,
mod: gbo.mod,
buildContext: gbo.buildContext,
platformMatcher: matcher,
}, nil
}

Expand Down Expand Up @@ -623,6 +635,10 @@ func (g *gobuild) buildAll(ctx context.Context, s string, base v1.ImageIndex) (v
return nil, fmt.Errorf("%q has unexpected mediaType %q in base for %q", desc.Digest, desc.MediaType, s)
}

if !g.platformMatcher.matches(desc.Platform) {
continue
}

base, err := base.Image(desc.Digest)
if err != nil {
return nil, err
Expand All @@ -649,3 +665,58 @@ func (g *gobuild) buildAll(ctx context.Context, s string, base v1.ImageIndex) (v

return mutate.IndexMediaType(mutate.AppendManifests(empty.Index, adds...), baseType), nil
}

func parseSpec(spec string) (*platformMatcher, error) {
// Don't bother parsing "all".
// "" should never happen because we default to linux/amd64.
platforms := []v1.Platform{}
if spec == "all" || spec == "" {
return &platformMatcher{spec: spec}, nil
}

for _, platform := range strings.Split(spec, ",") {
var p v1.Platform
parts := strings.Split(strings.TrimSpace(platform), "/")
if len(parts) > 0 {
p.OS = parts[0]
}
if len(parts) > 1 {
p.Architecture = parts[1]
}
if len(parts) > 2 {
p.Variant = parts[2]
}
if len(parts) > 3 {
return nil, fmt.Errorf("too many slashes in platform spec: %s", platform)
}
platforms = append(platforms, p)
}
return &platformMatcher{spec: spec, platforms: platforms}, nil
}

func (pm *platformMatcher) matches(base *v1.Platform) bool {
if pm.spec == "all" {
return true
}

// Don't build anything without a platform field unless "all". Unclear what we should do here.
if base == nil {
return false
}

for _, p := range pm.platforms {
if p.OS != "" && base.OS != p.OS {
continue
}
if p.Architecture != "" && base.Architecture != p.Architecture {
continue
}
if p.Variant != "" && base.Variant != p.Variant {
continue
}

return true
}

return false
}
78 changes: 78 additions & 0 deletions pkg/build/gobuild_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ func TestGoBuildIndex(t *testing.T) {
context.Background(),
WithCreationTime(creationTime),
WithBaseImages(func(context.Context, string) (Result, error) { return base, nil }),
WithPlatforms("all"),
withBuilder(writeTempFile),
)
if err != nil {
Expand Down Expand Up @@ -611,3 +612,80 @@ func TestGoarm(t *testing.T) {
}
}
}

func TestMatchesPlatformSpec(t *testing.T) {
for _, tc := range []struct {
platform *v1.Platform
spec string
result bool
err bool
}{{
platform: nil,
spec: "all",
result: true,
}, {
platform: nil,
spec: "linux/amd64",
result: false,
}, {
platform: &v1.Platform{
Architecture: "amd64",
OS: "linux",
},
spec: "all",
result: true,
}, {
platform: &v1.Platform{
Architecture: "amd64",
OS: "windows",
},
spec: "linux",
result: false,
}, {
platform: &v1.Platform{
Architecture: "arm64",
OS: "linux",
Variant: "v3",
},
spec: "linux/amd64,linux/arm64",
result: true,
}, {
platform: &v1.Platform{
Architecture: "arm64",
OS: "linux",
Variant: "v3",
},
spec: "linux/amd64,linux/arm64/v4",
result: false,
}, {
platform: &v1.Platform{
Architecture: "arm64",
OS: "linux",
Variant: "v3",
},
spec: "linux/amd64,linux/arm64/v3/z5",
err: true,
}, {
spec: "",
platform: &v1.Platform{
Architecture: "amd64",
OS: "linux",
},
result: false,
}} {
pm, err := parseSpec(tc.spec)
if tc.err {
if err == nil {
t.Errorf("parseSpec(%v, %q) expected err", tc.platform, tc.spec)
}
continue
}
if err != nil {
t.Fatalf("parseSpec failed for %v %q: %v", tc.platform, tc.spec, err)
}
matches := pm.matches(tc.platform)
if got, want := matches, tc.result; got != want {
t.Errorf("wrong result for %v %q: want %t got %t", tc.platform, tc.spec, want, got)
}
}
}
13 changes: 13 additions & 0 deletions pkg/build/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ func WithDisabledOptimizations() Option {
}
}

// WithPlatforms is a functional option for building certain platforms for
// multi-platform base images. To build everything from the base, use "all",
// otherwise use a comma-separated list of platform specs, i.e.:
//
// platform = <os>[/<arch>[/<variant>]]
// allowed = all | platform[,platform]*
func WithPlatforms(platforms string) Option {
return func(gbo *gobuildOpener) error {
gbo.platform = platforms
return nil
}
}

// withBuilder is a functional option for overriding the way go binaries
// are built.
func withBuilder(b builder) Option {
Expand Down
19 changes: 6 additions & 13 deletions pkg/commands/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"log"
"os"
"os/signal"
"path"
"strconv"
"strings"
"syscall"
Expand All @@ -41,16 +40,6 @@ var (
)

func getBaseImage(platform string) build.GetBase {
// Default to linux/amd64 unless GOOS and GOARCH are set.
if platform == "" {
platform = "linux/amd64"

goos, goarch := os.Getenv("GOOS"), os.Getenv("GOARCH")
if goos != "" && goarch != "" {
platform = path.Join(goos, goarch)
}
}

return func(ctx context.Context, s string) (build.Result, error) {
s = strings.TrimPrefix(s, build.StrictScheme)
// Viper configuration file keys are case insensitive, and are
Expand All @@ -71,8 +60,12 @@ func getBaseImage(platform string) build.GetBase {

// Using --platform=all will use an image index for the base,
// otherwise we'll resolve it to the appropriate platform.
//
// Platforms can be comma-separated if we only want a subset of the base
// image.
multiplatform := platform == "all" || strings.Contains(platform, ",")
var p v1.Platform
if platform != "" && platform != "all" {
if platform != "" && !multiplatform {
parts := strings.Split(platform, "/")
if len(parts) > 0 {
p.OS = parts[0]
Expand All @@ -96,7 +89,7 @@ func getBaseImage(platform string) build.GetBase {
}
switch desc.MediaType {
case types.OCIImageIndex, types.DockerManifestList:
if platform == "all" {
if multiplatform {
return desc.ImageIndex()
}
return desc.Image()
Expand Down
3 changes: 1 addition & 2 deletions pkg/commands/options/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,5 @@ func AddBuildOptions(cmd *cobra.Command, bo *BuildOptions) {
cmd.Flags().BoolVar(&bo.DisableOptimizations, "disable-optimizations", bo.DisableOptimizations,
"Disable optimizations when building Go code. Useful when you want to interactively debug the created container.")
cmd.Flags().StringVar(&bo.Platform, "platform", "",
"Which platform to use when pulling a multi-platform base. Format: all | <os>[/<arch>[/<variant>]]")

"Which platform to use when pulling a multi-platform base. Format: all | <os>[/<arch>[/<variant>]][,platform]*")
}
29 changes: 28 additions & 1 deletion pkg/commands/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"log"
"net/http"
"os"
"path"
"strings"
"sync"

Expand Down Expand Up @@ -69,8 +70,34 @@ func gobuildOptions(bo *options.BuildOptions) ([]build.Option, error) {
if err != nil {
return nil, err
}

platform := bo.Platform
if platform == "" {
platform = "linux/amd64"

goos, goarch, goarm := os.Getenv("GOOS"), os.Getenv("GOARCH"), os.Getenv("GOARM")

// Default to linux/amd64 unless GOOS and GOARCH are set.
if goos != "" && goarch != "" {
platform = path.Join(goos, goarch)
}

// Use GOARM for variant if it's set and GOARCH is arm.
if strings.Contains(goarch, "arm") && goarm != "" {
platform = path.Join(platform, "v"+goarm)
}
} else {
// Make sure these are all unset
for _, env := range []string{"GOOS", "GOARCH", "GOARM"} {
if s, ok := os.LookupEnv(env); ok {
return nil, fmt.Errorf("cannot use --platform with %s=%q", env, s)
}
}
}

opts := []build.Option{
build.WithBaseImages(getBaseImage(bo.Platform)),
build.WithBaseImages(getBaseImage(platform)),
build.WithPlatforms(platform),
}
if creationTime != nil {
opts = append(opts, build.WithCreationTime(*creationTime))
Expand Down