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 ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal" "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 {{.Name}} build --builder=pack --builder-image cnbs/sample-builder:bionic
`, `,
SuggestFor: []string{"biuld", "buidl", "built"}, 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'.") 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("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().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().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) setPathFlag(cmd)
if err := cmd.RegisterFlagCompletionFunc("builder", CompleteBuildStrategyList); err != nil { 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 // Choose a builder based on the value of the --builder flag
var builder fn.Builder var builder fn.Builder
if config.Builder == "pack" { 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)) builder = buildpacks.NewBuilder(buildpacks.WithVerbose(config.Verbose))
} else if config.Builder == "s2i" { } else if config.Builder == "s2i" {
builder = s2i.NewBuilder(s2i.WithVerbose(config.Verbose)) builder = s2i.NewBuilder(s2i.WithVerbose(config.Verbose), s2i.WithPlatform(config.Platform))
} else { } else {
err = errors.New("unrecognized builder: valid values are: s2i, pack") err = errors.New("unrecognized builder: valid values are: s2i, pack")
return return
@ -216,6 +221,8 @@ type buildConfig struct {
// BuilderImage is the image (name or mapping) to use for building. Usually // BuilderImage is the image (name or mapping) to use for building. Usually
// set automatically. // set automatically.
BuilderImage string BuilderImage string
Platform string
} }
func newBuildConfig() buildConfig { func newBuildConfig() buildConfig {
@ -228,6 +235,7 @@ func newBuildConfig() buildConfig {
Builder: viper.GetString("builder"), Builder: viper.GetString("builder"),
BuilderImage: viper.GetString("builder-image"), BuilderImage: viper.GetString("builder-image"),
Push: viper.GetBool("push"), 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 {{.Name}} deploy --image quay.io/myuser/myfunc -n myns
`, `,
SuggestFor: []string{"delpoy", "deplyo"}, 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)") 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("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().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().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) setPathFlag(cmd)
if err := cmd.RegisterFlagCompletionFunc("build", CompleteDeployBuildType); err != nil { 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 // Choose a builder based on the value of the --builder flag
var builder fn.Builder var builder fn.Builder
if config.Builder == "pack" { 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)) builder = buildpacks.NewBuilder(buildpacks.WithVerbose(config.Verbose))
} else if config.Builder == "s2i" { } else if config.Builder == "s2i" {
builder = s2i.NewBuilder(s2i.WithVerbose(config.Verbose)) builder = s2i.NewBuilder(s2i.WithVerbose(config.Verbose), s2i.WithPlatform(config.Platform))
} else { } else {
err = errors.New("unrecognized builder: valid values are: s2i, pack") err = errors.New("unrecognized builder: valid values are: s2i, pack")
return 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/alecthomas/jsonschema v0.0.0-20210526225647-edb03dcab7bc
github.com/buildpacks/pack v0.24.0 github.com/buildpacks/pack v0.24.0
github.com/cloudevents/sdk-go/v2 v2.8.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/containers/image/v5 v5.19.1
github.com/coreos/go-semver v0.3.0 github.com/coreos/go-semver v0.3.0
github.com/docker/cli v20.10.12+incompatible github.com/docker/cli v20.10.12+incompatible

View File

@ -56,6 +56,7 @@ type Builder struct {
verbose bool verbose bool
impl build.Builder // S2I builder implementation (aka "Strategy") impl build.Builder // S2I builder implementation (aka "Strategy")
cli DockerClient cli DockerClient
platform string
} }
type Option func(*Builder) 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. // NewBuilder creates a new instance of a Builder with static defaults.
func NewBuilder(options ...Option) *Builder { func NewBuilder(options ...Option) *Builder {
b := &Builder{} b := &Builder{}
@ -100,6 +107,13 @@ func (b *Builder) Build(ctx context.Context, f fn.Function) (err error) {
return 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 // Build Config
cfg := &api.Config{} cfg := &api.Config{}
cfg.Quiet = !b.verbose 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 v1.0.3
github.com/containerd/cgroups/stats/v1 github.com/containerd/cgroups/stats/v1
# github.com/containerd/containerd v1.6.0 # github.com/containerd/containerd v1.6.0
## explicit
github.com/containerd/containerd/errdefs github.com/containerd/containerd/errdefs
github.com/containerd/containerd/log github.com/containerd/containerd/log
github.com/containerd/containerd/pkg/userns github.com/containerd/containerd/pkg/userns