Add --platform flag for build/deploy sub-cmd (#1076)

Signed-off-by: Matej Vasek <mvasek@redhat.com>
This commit is contained in:
Matej Vasek 2022-06-22 18:40:23 +02:00 committed by GitHub
parent c550ac1e53
commit f066218042
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 233 additions and 7 deletions

View File

@ -3,6 +3,7 @@ package cmd
import (
"errors"
"fmt"
"os"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
@ -46,7 +47,7 @@ and the image name is stored in the configuration file.
{{.Name}} build --builder=pack --builder-image cnbs/sample-builder:bionic
`,
SuggestFor: []string{"biuld", "buidl", "built"},
PreRunE: bindEnv("image", "path", "builder", "registry", "confirm", "push", "builder-image"),
PreRunE: bindEnv("image", "path", "builder", "registry", "confirm", "push", "builder-image", "platform"),
}
cmd.Flags().StringP("builder", "b", "pack", "build strategy to use when creating the underlying image. Currently supported build strategies are 'pack' and 's2i'.")
@ -55,6 +56,7 @@ and the image name is stored in the configuration file.
cmd.Flags().StringP("image", "i", "", "Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry (Env: $FUNC_IMAGE)")
cmd.Flags().StringP("registry", "r", GetDefaultRegistry(), "Registry + namespace part of the image to build, ex 'quay.io/myuser'. The full image name is automatically determined based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)")
cmd.Flags().BoolP("push", "u", false, "Attempt to push the function image after being successfully built")
cmd.Flags().StringP("platform", "", "", "Target platform to build (e.g. linux/amd64).")
setPathFlag(cmd)
if err := cmd.RegisterFlagCompletionFunc("builder", CompleteBuildStrategyList); err != nil {
@ -158,9 +160,12 @@ func runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err erro
// Choose a builder based on the value of the --builder flag
var builder fn.Builder
if config.Builder == "pack" {
if config.Platform != "" {
fmt.Fprintln(os.Stderr, "the --platform flag works only with s2i build")
}
builder = buildpacks.NewBuilder(buildpacks.WithVerbose(config.Verbose))
} else if config.Builder == "s2i" {
builder = s2i.NewBuilder(s2i.WithVerbose(config.Verbose))
builder = s2i.NewBuilder(s2i.WithVerbose(config.Verbose), s2i.WithPlatform(config.Platform))
} else {
err = errors.New("unrecognized builder: valid values are: s2i, pack")
return
@ -216,6 +221,8 @@ type buildConfig struct {
// BuilderImage is the image (name or mapping) to use for building. Usually
// set automatically.
BuilderImage string
Platform string
}
func newBuildConfig() buildConfig {
@ -228,6 +235,7 @@ func newBuildConfig() buildConfig {
Builder: viper.GetString("builder"),
BuilderImage: viper.GetString("builder-image"),
Push: viper.GetBool("push"),
Platform: viper.GetString("platform"),
}
}

View File

@ -49,7 +49,7 @@ that is pushed to an image registry, and finally the function's Knative service
{{.Name}} deploy --image quay.io/myuser/myfunc -n myns
`,
SuggestFor: []string{"delpoy", "deplyo"},
PreRunE: bindEnv("image", "path", "registry", "confirm", "build", "push", "git-url", "git-branch", "git-dir", "builder", "builder-image"),
PreRunE: bindEnv("image", "path", "registry", "confirm", "build", "push", "git-url", "git-branch", "git-dir", "builder", "builder-image", "platform"),
}
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
@ -66,6 +66,7 @@ that is pushed to an image registry, and finally the function's Knative service
cmd.Flags().StringP("image", "i", "", "Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry (Env: $FUNC_IMAGE)")
cmd.Flags().StringP("registry", "r", GetDefaultRegistry(), "Registry + namespace part of the image to build, ex 'quay.io/myuser'. The full image name is automatically determined based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)")
cmd.Flags().BoolP("push", "u", true, "Attempt to push the function image to registry before deploying (Env: $FUNC_PUSH)")
cmd.Flags().StringP("platform", "", "", "Target platform to build (e.g. linux/amd64).")
setPathFlag(cmd)
if err := cmd.RegisterFlagCompletionFunc("build", CompleteDeployBuildType); err != nil {
@ -174,9 +175,12 @@ func runDeploy(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err
// Choose a builder based on the value of the --builder flag
var builder fn.Builder
if config.Builder == "pack" {
if config.Platform != "" {
fmt.Fprintln(os.Stderr, "the --platform flag works only with s2i build")
}
builder = buildpacks.NewBuilder(buildpacks.WithVerbose(config.Verbose))
} else if config.Builder == "s2i" {
builder = s2i.NewBuilder(s2i.WithVerbose(config.Verbose))
builder = s2i.NewBuilder(s2i.WithVerbose(config.Verbose), s2i.WithPlatform(config.Platform))
} else {
err = errors.New("unrecognized builder: valid values are: s2i, pack")
return

71
docker/platform.go Normal file
View File

@ -0,0 +1,71 @@
package docker
import (
"fmt"
"github.com/containerd/containerd/platforms"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
gcrTypes "github.com/google/go-containerregistry/pkg/v1/types"
)
// GetPlatformImage returns image reference for specific platform.
// If the image is not multi-arch it returns ref argument directly (provided platform matches).
// If the image is multi-arch it returns digest based reference (provided the platform is part of the multi-arch image).
func GetPlatformImage(ref, platform string) (string, error) {
plat, err := platforms.Parse(platform)
if err != nil {
return "", fmt.Errorf("cannot parse platform: %w", err)
}
r, err := name.ParseReference(ref)
if err != nil {
return "", fmt.Errorf("cannot parse reference: %w", err)
}
desc, err := remote.Get(r)
if err != nil {
return "", fmt.Errorf("cannot get remote image: %w", err)
}
if desc.MediaType != gcrTypes.OCIImageIndex && desc.MediaType != gcrTypes.DockerManifestList {
// it's non-multi-arch image
var img v1.Image
var cfg *v1.ConfigFile
img, err = desc.Image()
if err != nil {
return "", fmt.Errorf("cannot get image from the descriptor: %w", err)
}
cfg, err = img.ConfigFile()
if err != nil {
return "", fmt.Errorf("cannot get config file for the image: %w", err)
}
if plat.OS == cfg.OS &&
plat.Architecture == cfg.Architecture {
return ref, nil
}
return "", fmt.Errorf("the %q platform is not supported by the %q image", platform, ref)
}
idx, err := desc.ImageIndex()
if err != nil {
return "", fmt.Errorf("cannot get image index: %w", err)
}
idxMft, err := idx.IndexManifest()
if err != nil {
return "", fmt.Errorf("cannot get index manifest: %w", err)
}
for _, manifest := range idxMft.Manifests {
if plat.OS == manifest.Platform.OS &&
plat.Architecture == manifest.Platform.Architecture {
return r.Context().Name() + "@" + manifest.Digest.String(), nil
}
}
return "", fmt.Errorf("the %q platform is not supported by the %q image", platform, ref)
}

127
docker/platform_test.go Normal file
View File

@ -0,0 +1,127 @@
package docker_test
import (
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"testing"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/registry"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/remote"
gcrTypes "github.com/google/go-containerregistry/pkg/v1/types"
"knative.dev/kn-plugin-func/docker"
)
func TestPlatform(t *testing.T) {
testRegistry := startRegistry(t)
nonMultiArchBuilder := testRegistry + "/default/builder:nonmultiarch"
multiArchBuilder := testRegistry + "/default/builder:multiarch"
// begin push testing builders to registry
tag, err := name.NewTag(nonMultiArchBuilder)
if err != nil {
t.Fatal(err)
}
var img v1.Image
img, err = mutate.ConfigFile(empty.Image, &v1.ConfigFile{
Architecture: "ppc64le",
OS: "linux",
})
if err != nil {
t.Fatal(err)
}
err = remote.Write(&tag, img)
if err != nil {
t.Fatal(err)
}
tag, err = name.NewTag(multiArchBuilder)
if err != nil {
t.Fatal(err)
}
zeroHash := v1.Hash{
Algorithm: "sha256",
Hex: "0000000000000000000000000000000000000000000000000000000000000000",
}
var imgIdx = mutate.AppendManifests(empty.Index, mutate.IndexAddendum{
Add: empty.Index,
Descriptor: v1.Descriptor{
MediaType: gcrTypes.DockerManifestList,
Digest: zeroHash,
Platform: &v1.Platform{
Architecture: "ppc64le",
OS: "linux",
},
},
})
err = remote.WriteIndex(tag, imgIdx)
if err != nil {
t.Fatal(err)
}
// end push testing builders to registry
_, err = docker.GetPlatformImage(nonMultiArchBuilder, "windows/amd64")
if err == nil {
t.Error("expected error but got nil")
}
_, err = docker.GetPlatformImage(multiArchBuilder, "windows/amd64")
if err == nil {
t.Error("expected error but got nil")
}
var ref string
ref, err = docker.GetPlatformImage(nonMultiArchBuilder, "linux/ppc64le")
if err != nil {
t.Errorf("unexpeced error: %v", err)
}
if ref != nonMultiArchBuilder {
t.Error("incorrect reference")
}
ref, err = docker.GetPlatformImage(multiArchBuilder, "linux/ppc64le")
if err != nil {
t.Errorf("unexpeced error: %v", err)
}
if ref != testRegistry+"/default/builder@sha256:0000000000000000000000000000000000000000000000000000000000000000" {
t.Error("incorrect reference")
}
}
func startRegistry(t *testing.T) (addr string) {
s := http.Server{
Handler: registry.New(registry.Logger(log.New(io.Discard, "", 0))),
}
t.Cleanup(func() { s.Close() })
l, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
addr = l.Addr().String()
go func() {
err = s.Serve(l)
if err != nil && !errors.Is(err, net.ErrClosed) {
fmt.Fprintln(os.Stderr, "ERROR: ", err)
}
}()
return addr
}

1
go.mod
View File

@ -9,6 +9,7 @@ require (
github.com/alecthomas/jsonschema v0.0.0-20210526225647-edb03dcab7bc
github.com/buildpacks/pack v0.24.0
github.com/cloudevents/sdk-go/v2 v2.8.0
github.com/containerd/containerd v1.6.0
github.com/containers/image/v5 v5.19.1
github.com/coreos/go-semver v0.3.0
github.com/docker/cli v20.10.12+incompatible

View File

@ -53,9 +53,10 @@ type DockerClient interface {
// Builder of Functions using the s2i subsystem.
type Builder struct {
verbose bool
impl build.Builder // S2I builder implementation (aka "Strategy")
cli DockerClient
verbose bool
impl build.Builder // S2I builder implementation (aka "Strategy")
cli DockerClient
platform string
}
type Option func(*Builder)
@ -82,6 +83,12 @@ func WithDockerClient(cli DockerClient) Option {
}
}
func WithPlatform(platform string) Option {
return func(b *Builder) {
b.platform = platform
}
}
// NewBuilder creates a new instance of a Builder with static defaults.
func NewBuilder(options ...Option) *Builder {
b := &Builder{}
@ -100,6 +107,13 @@ func (b *Builder) Build(ctx context.Context, f fn.Function) (err error) {
return
}
if b.platform != "" {
builderImage, err = docker.GetPlatformImage(builderImage, b.platform)
if err != nil {
return fmt.Errorf("cannot get platform specific image reference: %w", err)
}
}
// Build Config
cfg := &api.Config{}
cfg.Quiet = !b.verbose

1
vendor/modules.txt vendored
View File

@ -178,6 +178,7 @@ github.com/cloudevents/sdk-go/v2/types
# github.com/containerd/cgroups v1.0.3
github.com/containerd/cgroups/stats/v1
# github.com/containerd/containerd v1.6.0
## explicit
github.com/containerd/containerd/errdefs
github.com/containerd/containerd/log
github.com/containerd/containerd/pkg/userns