mirror of https://github.com/knative/func.git
Add --platform flag for build/deploy sub-cmd (#1076)
Signed-off-by: Matej Vasek <mvasek@redhat.com>
This commit is contained in:
parent
c550ac1e53
commit
f066218042
12
cmd/build.go
12
cmd/build.go
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
1
go.mod
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue