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

feat: wasi platform build #738

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
53 changes: 53 additions & 0 deletions .github/workflows/wasi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Integration test for wasi

on:
pull_request:
branches: ["main"]

jobs:
wasi:
strategy:
fail-fast: false
matrix:
platform:
- ubuntu-latest
name: wasi
runs-on: ${{ matrix.platform }}

steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.17
check-latest: true
- name: setup tinygo
shell: bash
env:
TINYGO_VERSION: 0.23.0
run: |
wget https://github.com/tinygo-org/tinygo/releases/download/v"${TINYGO_VERSION}"/tinygo_${TINYGO_VERSION}_amd64.deb
sudo dpkg -i tinygo_${TINYGO_VERSION}_amd64.deb
- name: create crun wasmedge binary
shell: bash
run: ./test/wasi/crun-wasmedge.sh

- name: Build and run ko container
env:
KO_DOCKER_REPO: ko.local
shell: bash
run: |
set -euxo pipefail

# Build and run the test/wasi binary, which should print "hello from wasi"
testimg=$(go run ./ build ./test/wasi/ --platform="wasm/wasi")

# export built image from docker into tar to load into podman
docker save $testimg -o testimg.tar

# run a nested podman container to use annotations (could be removed once docker is able to specify annotations on containers)
docker run --priviliged \
-v $(pwd)/testimg.tar:/tmp/testimg.tar \
-v /tmp/crun/crun:/usr/bin/crun \
quay.io/containers/podman:v4 \
sh -c "podman load -i /tmp/testimg.tar && podman run --runtime=/usr/bin/crun --annotations=module.wasm.image/variant=compat $testimg" \
| grep "hello from wasi"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
.idea/

ko
.vscode/
23 changes: 23 additions & 0 deletions hack/install-crun-wasmedge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

echo -e "Installing WasmEdge"
if [ -f install.sh ]; then
rm -rf install.sh
fi
curl -L -o install.sh -q https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh
sudo chmod a+x install.sh
# use 0.9.1 because 0.10.0 had a breaking change which is not yet fixed in crun.
# See https://github.com/containers/crun/pull/933
sudo bash ./install.sh --path="/usr/local" -v 0.9.1
rm -rf install.sh

echo -e "Building and installing crun"
sudo apt install -y make git gcc build-essential pkgconf libtool libsystemd-dev \
libprotobuf-c-dev libcap-dev libseccomp-dev libyajl-dev \
go-md2man libtool autoconf python3 automake

git clone https://github.com/containers/crun /tmp/crun || true
cd /tmp/crun
./autogen.sh
./configure --with-wasmedge
make
118 changes: 78 additions & 40 deletions pkg/build/gobuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,15 +241,6 @@ func getGoarm(platform v1.Platform) (string, error) {
}

func build(ctx context.Context, ip string, dir string, platform v1.Platform, config Config) (string, error) {
buildArgs, err := createBuildArgs(config)
if err != nil {
return "", err
}

args := make([]string, 0, 4+len(buildArgs))
args = append(args, "build")
args = append(args, buildArgs...)

env, err := buildEnv(platform, os.Environ(), config.Env)
if err != nil {
return "", fmt.Errorf("could not create env for %s: %w", ip, err)
Expand Down Expand Up @@ -279,27 +270,52 @@ func build(ctx context.Context, ip string, dir string, platform v1.Platform, con

file := filepath.Join(tmpDir, "out")

args = append(args, "-o", file)
args = append(args, ip)
cmd := exec.CommandContext(ctx, "go", args...)
cmd.Dir = dir
cmd.Env = env

goBinary := "go"
if platform.String() == "wasm/wasi" {
goBinary = "tinygo"
}
cmd, err := gobuildCommand(ctx, config, goBinary, ip, file, dir, env)
if err != nil {
return "", fmt.Errorf("creating %v build exec.Cmd: %w", goBinary, err)
}
var output bytes.Buffer
cmd.Stderr = &output
cmd.Stdout = &output

log.Printf("Building %s for %s", ip, platform)
log.Printf("Building %s for %v with %v", ip, platform, goBinary)
hown3d marked this conversation as resolved.
Show resolved Hide resolved
if err := cmd.Run(); err != nil {
if os.Getenv("KOCACHE") == "" {
os.RemoveAll(tmpDir)
}
log.Printf("Unexpected error running \"go build\": %v\n%v", err, output.String())
log.Printf("Unexpected error running \"%v build\": %v\n%v", goBinary, err, output.String())
hown3d marked this conversation as resolved.
Show resolved Hide resolved
return "", err
}
return file, nil
}

func gobuildCommand(ctx context.Context, config Config, goBinary string, ip string, outDir string, dir string, env []string) (*exec.Cmd, error) {
buildArgs, err := createBuildArgs(config)
if err != nil {
return nil, err
}

args := make([]string, 0, 4+len(buildArgs))
args = append(args, "build")
args = append(args, buildArgs...)
// if we are using tinygo, add wasi target
if goBinary == "tinygo" {
args = append(args, "-target", "wasi")
}

args = append(args, "-o", outDir)
args = append(args, ip)
cmd := exec.CommandContext(ctx, goBinary, args...)
cmd.Dir = dir
cmd.Env = env

return cmd, nil
}

func goversionm(ctx context.Context, file string, appPath string, _ v1.Image) ([]byte, types.MediaType, error) {
sbom := bytes.NewBuffer(nil)
cmd := exec.CommandContext(ctx, "go", "version", "-m", file)
Expand Down Expand Up @@ -689,6 +705,8 @@ func (g *gobuild) configForImportPath(ip string) Config {
return config
}

const wasmLayerAnnotationKey = "module.wasm.image/variant"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this annotation documented anywhere? What expects to read it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specification of this annotation are here: https://github.com/solo-io/wasm/blob/master/spec/spec-compat.md

crun also has an experimental annotation run.oci.handler which can be used to specifiy the handler for the container (e.g. wasm, wasm-smart). More in https://man.archlinux.org/man/crun.1.en


func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, platform *v1.Platform) (oci.SignedImage, error) {
if err := g.semaphore.Acquire(ctx, 1); err != nil {
return nil, err
Expand All @@ -709,6 +727,10 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl
}
}

// disable trimpath flag because it doesn't exists in tinygo
if platform.String() == "wasm/wasi" {
g.trimpath = false
}
// Do the build into a temporary file.
file, err := g.build(ctx, ref.Path(), g.dir, *platform, g.configForImportPath(ref.Path()))
if err != nil {
Expand All @@ -720,30 +742,41 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl

var layers []mutate.Addendum

// Create a layer from the kodata directory under this import path.
dataLayerBuf, err := g.tarKoData(ref, platform)
if err != nil {
return nil, err
}
dataLayerBytes := dataLayerBuf.Bytes()
dataLayer, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewBuffer(dataLayerBytes)), nil
}, tarball.WithCompressedCaching)
if err != nil {
return nil, err
// don't add kodata layer into wasi image
if platform.String() != "wasm/wasi" {
// Create a layer from the kodata directory under this import path.
dataLayerBuf, err := g.tarKoData(ref, platform)
if err != nil {
return nil, err
}
dataLayerBytes := dataLayerBuf.Bytes()
dataLayer, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewBuffer(dataLayerBytes)), nil
}, tarball.WithCompressedCaching)
if err != nil {
return nil, err
}
layers = append(layers, mutate.Addendum{
Layer: dataLayer,
History: v1.History{
Author: "ko",
CreatedBy: "ko build " + ref.String(),
Created: g.kodataCreationTime,
Comment: "kodata contents, at $KO_DATA_PATH",
},
})
}
layers = append(layers, mutate.Addendum{
Layer: dataLayer,
History: v1.History{
Author: "ko",
CreatedBy: "ko build " + ref.String(),
Created: g.kodataCreationTime,
Comment: "kodata contents, at $KO_DATA_PATH",
},
})

appDir := "/ko-app"
appPath := path.Join(appDir, appFilename(ref.Path()))
var filename string
if platform.String() == "wasm/wasi" {
// module.wasm.image/variant=compat-smart expects the entrypoint binary to be of form .wasm
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you link to where this expectation is documented? I'd also just love to learn more about runtimes that run these things.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm currently focusing on the implementation in crun: https://github.com/containers/crun/blob/main/src/libcrun/handlers/handler-utils.c

https://github.com/containers/crun/blob/65028ce3057180395a1826d8c0906f9ff299030a/src/libcrun/custom-handler.c#L208 will check all the registered handlers by calling wasm_can_handle_container function from handler-utils.c.

filename = appFilename(fmt.Sprintf("%v.wasm", ref.Path()))
} else {
filename = appFilename(ref.Path())
}
hown3d marked this conversation as resolved.
Show resolved Hide resolved

appPath := path.Join(appDir, filename)

miss := func() (v1.Layer, error) {
return buildLayer(appPath, file, platform)
Expand Down Expand Up @@ -784,7 +817,7 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl
cfg.Config.Entrypoint = []string{`C:\ko-app\` + appFilename(ref.Path())}
updatePath(cfg, `C:\ko-app`)
cfg.Config.Env = append(cfg.Config.Env, `KO_DATA_PATH=C:\var\run\ko`)
} else {
} else if platform.String() != "wasm/wasi" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we keep doing this comparison, I wonder if it warrants having a helper method isWasi(platform) (and isWindows(platform)) to make this easier to read.

Then we can also avoid the string comparison without bloating the callsite:

func isWasi(p v1.Platform) bool {
  return p.OS == "wasm" && p.Architecture == "wasi"
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is a good idea. The constant comparison with platform string was kinda hacky anyway.

updatePath(cfg, appDir)
cfg.Config.Env = append(cfg.Config.Env, "KO_DATA_PATH="+kodataRoot)
}
Expand Down Expand Up @@ -909,7 +942,12 @@ func (g *gobuild) Build(ctx context.Context, s string) (Result, error) {
if !ok {
return nil, fmt.Errorf("failed to interpret base as image: %v", base)
}
res, err = g.buildOne(ctx, s, baseImage, nil)
// user specified a platform, so force this platform, even if it fails
var platform *v1.Platform
if len(g.platformMatcher.platforms) == 1 {
platform = &g.platformMatcher.platforms[0]
}
res, err = g.buildOne(ctx, s, baseImage, platform)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why's this necessary?

Copy link
Author

@hown3d hown3d Jun 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

platform had been set to nil otherwise, which would remove the possibility to check for the platform string in the build function: https://github.com/hown3d/ko/blob/ceac5e5fcea61bea80231a689bb990cce3198827/pkg/build/gobuild.go#L274

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay. In that case, I think we can remove this check here: https://github.com/hown3d/ko/blob/ceac5e5fcea61bea80231a689bb990cce3198827/pkg/build/gobuild.go#L722

This does make me wonder how ko will behave (before and after this change) when asked to build on top of a single-platform image that isn't linux/amd64, with --platform unset... 🤔

Copy link
Author

@hown3d hown3d Jun 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a single platform image (no OS or Architecture set) here: https://quay.io/repository/hown3d/ko-wasm-tests/manifest/sha256:2374307703312554852605270f7979f1057e4d6b98bcb9f5f7cf4cabf9ca8b4f and used it as as the base image. The result was that, the new image didn't have architecture and os set aswell.

default:
return nil, fmt.Errorf("base image media type: %s", mt)
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/commands/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/daemon"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/google"
"github.com/google/go-containerregistry/pkg/v1/remote"

Expand Down Expand Up @@ -98,6 +99,15 @@ func getBaseImage(bo *options.BuildOptions) build.GetBase {
if bo.InsecureRegistry {
nameOpts = append(nameOpts, name.Insecure)
}

// look for wasm/wasi platform
for _, p := range bo.Platforms {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that if a user passes --platform=linux/amd64,wasm/wasi, that they'll only get the wasi image?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good argument, haven't thought about providing multiple architectures with wasi. I'll figure out, if there's a good way when using wasi in multiple platforms.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've filed a request for clarification here: solo-io/wasm#291

// use scratch image because wasm/wasi is not an official platform
if p == "wasm/wasi" {
// FIXME: not sure if returning a nil reference is a good idea
return nil, empty.Image, nil
}
}
ref, err := name.ParseReference(baseImage, nameOpts...)
if err != nil {
return nil, nil, fmt.Errorf("parsing base image (%q): %w", baseImage, err)
Expand Down
Empty file added podman.sock
Empty file.
21 changes: 21 additions & 0 deletions test/wasi/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2021 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import "fmt"

func main() {
fmt.Println("hello from wasi")
}