Skip to content

Commit

Permalink
feat: support build and push with soci
Browse files Browse the repository at this point in the history
Signed-off-by: Ziwen Ning <ningziwe@amazon.com>
  • Loading branch information
ningziwen committed Sep 15, 2023
1 parent c33b6d3 commit d4fd880
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 0 deletions.
10 changes: 10 additions & 0 deletions cmd/nerdctl/flagutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ func processImageVerifyOptions(cmd *cobra.Command) (opt types.ImageVerifyOptions
return
}

func processSociOptions(cmd *cobra.Command) (opt types.SociOptions, err error) {
if opt.SpanSize, err = cmd.Flags().GetInt64("soci-span-size"); err != nil {
return
}
if opt.MinLayerSize, err = cmd.Flags().GetInt64("soci-min-layer-size"); err != nil {
return
}
return
}

func processRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error) {
debug, err := cmd.Flags().GetBool("debug")
if err != nil {
Expand Down
10 changes: 10 additions & 0 deletions cmd/nerdctl/image_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ func newPushCommand() *cobra.Command {
pushCommand.Flags().String("notation-key-name", "", "Signing key name for a key previously added to notation's key list for --sign=notation")
// #endregion

// #region soci flags
pushCommand.Flags().Int64("soci-span-size", -1, "Span size that soci index uses to segment layer data. Default is 4 MiB.")
pushCommand.Flags().Int64("soci-min-layer-size", -1, "Minimum layer size to build zTOC for. Smaller layers won't have zTOC and not lazy pulled. Default is 10 MiB.")
// #endregion

pushCommand.Flags().BoolP("quiet", "q", false, "Suppress verbose output")

pushCommand.Flags().Bool(allowNonDistFlag, false, "Allow pushing images with non-distributable blobs")
Expand Down Expand Up @@ -101,9 +106,14 @@ func processImagePushOptions(cmd *cobra.Command) (types.ImagePushOptions, error)
if err != nil {
return types.ImagePushOptions{}, err
}
sociOptions, err := processSociOptions(cmd)
if err != nil {
return types.ImagePushOptions{}, err
}
return types.ImagePushOptions{
GOptions: globalOptions,
SignOptions: signOptions,
SociOptions: sociOptions,
Platforms: platform,
AllPlatforms: allPlatforms,
Estargz: estargz,
Expand Down
68 changes: 68 additions & 0 deletions cmd/nerdctl/image_push_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package main
import (
"fmt"
"net/http"
"os"
"os/exec"
"strings"
"testing"

Expand Down Expand Up @@ -174,3 +176,69 @@ func TestPushNonDistributableArtifacts(t *testing.T) {
}
assert.Equal(t, resp.StatusCode, http.StatusOK, "non-distributable blob should be available")
}

func TestPushSoci(t *testing.T) {
testutil.DockerIncompatible(t)
base := testutil.NewBase(t)
reg := testregistry.NewPlainHTTP(base, 5000)
defer reg.Cleanup()
localhostIP := "127.0.0.1"
t.Logf("localhost IP=%q", localhostIP)
sociExecutable, err := exec.LookPath("soci")
if err != nil {
t.Fatalf("SOCI is not installed.")
}

base.Cmd("pull", testutil.UbuntuImage).AssertOK()
testImageRef := fmt.Sprintf("%s:%d/%s:%s",
localhostIP, reg.ListenPort, testutil.Identifier(t), strings.Split(testutil.UbuntuImage, ":")[1])
t.Logf("testImageRef=%q", testImageRef)
base.Cmd("tag", testutil.UbuntuImage, testImageRef).AssertOK()

base.Cmd("--snapshotter=soci", "--debug", "push", testImageRef).AssertOK()

//base.Cmd("rmi", testImageRef).AssertOK()

// pull the pushed image with soci and verify mounts
initialMounts, err := exec.Command("mount").Output()
if err != nil {
t.Fatal(err)
}

remoteSnapshotsInitialCount := strings.Count(string(initialMounts), "fuse.rawBridge")

pullOutput := base.Cmd("--snapshotter=soci", "pull", testImageRef).Out()
base.T.Logf("pull output: %s", pullOutput)

actualMounts, err := exec.Command("mount").Output()
if err != nil {
t.Fatal(err)
}
remoteSnapshotsActualCount := strings.Count(string(actualMounts), "fuse.rawBridge")
base.T.Logf("number of actual mounts: %v", remoteSnapshotsActualCount)

rmiOutput := base.Cmd("rmi", testImageRef).Out()
base.T.Logf("rmi output: %s", rmiOutput)

rpullCmd := exec.Command(sociExecutable, []string{"image", "rpull", testImageRef}...)

rpullCmd.Env = os.Environ()

err = rpullCmd.Run()
if err != nil {
t.Fatal(err)
}

expectedMounts, err := exec.Command("mount").Output()
if err != nil {
t.Fatal(err)
}

remoteSnapshotsExpectedCount := strings.Count(string(expectedMounts), "fuse.rawBridge")
base.T.Logf("number of expected mounts: %v", remoteSnapshotsExpectedCount)

if remoteSnapshotsExpectedCount != (remoteSnapshotsActualCount - remoteSnapshotsInitialCount) {
t.Fatalf("incorrect number of remote snapshots; expected=%d, actual=%d",
remoteSnapshotsExpectedCount, remoteSnapshotsActualCount)
}
}
2 changes: 2 additions & 0 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,8 @@ Flags:
- :nerd_face: `--allow-nondistributable-artifacts`: Allow pushing images with non-distributable blobs
- :nerd_face: `--ipfs-address`: Multiaddr of IPFS API (default uses `$IPFS_PATH` env variable if defined or local directory `~/.ipfs`)
- :whale: `-q, --quiet`: Suppress verbose output
- :nerd_face: `--soci-span-size`: Span size that soci index uses to segment layer data. Default is 4 MiB.
- :nerd_face: `--soci-min-layer-size`: Minimum layer size to build zTOC for. Smaller layers won't have zTOC and not lazy pulled. Default is 10 MiB.

Unimplemented `docker push` flags: `--all-tags`, `--disable-content-trust` (default true)

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ require (
gotest.tools/v3 v3.5.0
)

require oras.land/oras-go/v2 v2.2.1 // indirect

require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -464,3 +464,5 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
oras.land/oras-go/v2 v2.2.1 h1:3VJTYqy5KfelEF9c2jo1MLSpr+TM3mX8K42wzZcd6qE=
oras.land/oras-go/v2 v2.2.1/go.mod h1:GeAwLuC4G/JpNwkd+bSZ6SkDMGaaYglt6YK2WvZP7uQ=
9 changes: 9 additions & 0 deletions pkg/api/types/image_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ type ImagePushOptions struct {
Stdout io.Writer
GOptions GlobalCommandOptions
SignOptions ImageSignOptions
SociOptions SociOptions
// Platforms convert content for a specific platform
Platforms []string
// AllPlatforms convert content for all platforms
Expand Down Expand Up @@ -256,3 +257,11 @@ type ImageVerifyOptions struct {
// CosignCertificateOidcIssuerRegexp A regular expression alternative to --certificate-oidc-issuer for --verify=cosign. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --cosign-certificate-oidc-issuer or --cosign-certificate-oidc-issuer-regexp must be set for keyless flows
CosignCertificateOidcIssuerRegexp string
}

// SociOptions contains options for SOCI.
type SociOptions struct {
// Span size that soci index uses to segment layer data. Default is 4 MiB.
SpanSize int64
// Minimum layer size to build zTOC for. Smaller layers won't have zTOC and not lazy pulled. Default is 10 MiB.
MinLayerSize int64
}
9 changes: 9 additions & 0 deletions pkg/cmd/image/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"github.com/containerd/nerdctl/pkg/platformutil"
"github.com/containerd/nerdctl/pkg/referenceutil"
"github.com/containerd/nerdctl/pkg/signutil"
"github.com/containerd/nerdctl/pkg/snapshotterutil"
"github.com/containerd/stargz-snapshotter/estargz"
"github.com/containerd/stargz-snapshotter/estargz/zstdchunked"
estargzconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz"
Expand Down Expand Up @@ -181,6 +182,14 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, options
options.SignOptions); err != nil {
return err
}
if options.GOptions.Snapshotter == "soci" {
if err = snapshotterutil.CreateSoci(ref, options.GOptions, options.AllPlatforms, options.Platforms, options.SociOptions); err != nil {
return err
}
if err = snapshotterutil.PushSoci(ref, options.GOptions, options.AllPlatforms, options.Platforms); err != nil {
return err
}
}
if options.Quiet {
fmt.Fprintln(options.Stdout, ref)
}
Expand Down
161 changes: 161 additions & 0 deletions pkg/snapshotterutil/sociutil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
Copyright The containerd Authors.
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 snapshotterutil

import (
"bufio"
"os"
"os/exec"
"strconv"

"github.com/containerd/nerdctl/pkg/api/types"
"github.com/sirupsen/logrus"
)

// CreateSoci creates a SOCI index(`rawRef`)
func CreateSoci(rawRef string, gOpts types.GlobalCommandOptions, allPlatform bool, platforms []string, sOpts types.SociOptions) error {
sociExecutable, err := exec.LookPath("soci")
if err != nil {
logrus.WithError(err).Error("soci executable not found in path $PATH")
logrus.Info("you might consider installing soci from: https://github.com/awslabs/soci-snapshotter/blob/main/docs/install.md")
return err
}

sociCmd := exec.Command(sociExecutable)
sociCmd.Env = os.Environ()

// #region for global flags.
if gOpts.Address != "" {
sociCmd.Args = append(sociCmd.Args, "--address", gOpts.Address)
}
if gOpts.Namespace != "" {
sociCmd.Args = append(sociCmd.Args, "--namespace", gOpts.Namespace)
}
// #endregion

// Global flags have to be put before subcommand before soci upgrades to urfave v3.
// https://github.com/urfave/cli/issues/1113
sociCmd.Args = append(sociCmd.Args, "create")

if allPlatform {
sociCmd.Args = append(sociCmd.Args, "--all-platforms", strconv.FormatBool(allPlatform))
}
if len(platforms) > 0 {
sociCmd.Args = append(sociCmd.Args, "--platform")
sociCmd.Args = append(sociCmd.Args, platforms...)
}
if gOpts.Namespace != "" {
sociCmd.Args = append(sociCmd.Args, "--namespace", gOpts.Namespace)
}
if sOpts.SpanSize != -1 {
sociCmd.Args = append(sociCmd.Args, "--span-size", strconv.FormatInt(sOpts.SpanSize, 10))
}
if sOpts.MinLayerSize != -1 {
sociCmd.Args = append(sociCmd.Args, "--min-layer-size", strconv.FormatInt(sOpts.MinLayerSize, 10))
}
// --timeout, --debug, --content-store
sociCmd.Args = append(sociCmd.Args, rawRef)

logrus.Debugf("running %s %v", sociExecutable, sociCmd.Args)

err = processSociIO(sociCmd)
if err != nil {
return err
}

return sociCmd.Wait()
}

// PushSoci pushes a SOCI index(`rawRef`)
// `hostsDirs` are used to resolve image `rawRef`
func PushSoci(rawRef string, gOpts types.GlobalCommandOptions, allPlatform bool, platforms []string) error {
logrus.Debugf("pushing SOCI index: %s", rawRef)

sociExecutable, err := exec.LookPath("soci")
if err != nil {
logrus.WithError(err).Error("soci executable not found in path $PATH")
logrus.Info("you might consider installing soci from: https://github.com/awslabs/soci-snapshotter/blob/main/docs/install.md")
return err
}

sociCmd := exec.Command(sociExecutable)
sociCmd.Env = os.Environ()

// #region for global flags.
if gOpts.Address != "" {
sociCmd.Args = append(sociCmd.Args, "--address", gOpts.Address)
}
if gOpts.Namespace != "" {
sociCmd.Args = append(sociCmd.Args, "--namespace", gOpts.Namespace)
}
// #endregion

// Global flags have to be put before subcommand before soci upgrades to urfave v3.
// https://github.com/urfave/cli/issues/1113
sociCmd.Args = append(sociCmd.Args, "push")

if gOpts.InsecureRegistry {
sociCmd.Args = append(sociCmd.Args, "--skip-verify")
sociCmd.Args = append(sociCmd.Args, "--plain-http")
}
if len(gOpts.HostsDir) > 0 {
sociCmd.Args = append(sociCmd.Args, "--hosts-dir")
sociCmd.Args = append(sociCmd.Args, gOpts.HostsDir...)
}
sociCmd.Args = append(sociCmd.Args, rawRef)

logrus.Debugf("running %s %v", sociExecutable, sociCmd.Args)

err = processSociIO(sociCmd)
if err != nil {
return err
}
return sociCmd.Wait()
}

func processSociIO(sociCmd *exec.Cmd) error {
stdout, err := sociCmd.StdoutPipe()
if err != nil {
logrus.Warn("soci: " + err.Error())
}
stderr, err := sociCmd.StderrPipe()
if err != nil {
logrus.Warn("soci: " + err.Error())
}
if err := sociCmd.Start(); err != nil {
// only return err if it's critical (soci start failed.)
return err
}

scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
logrus.Info("soci: " + scanner.Text())
}
if err := scanner.Err(); err != nil {
logrus.Warn("soci: " + err.Error())
}

errScanner := bufio.NewScanner(stderr)
for errScanner.Scan() {
logrus.Info("soci: " + errScanner.Text())
}
if err := errScanner.Err(); err != nil {
logrus.Warn("soci: " + err.Error())
}

return nil
}
1 change: 1 addition & 0 deletions pkg/testutil/testutil_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ var (
const (
FedoraESGZImage = "ghcr.io/stargz-containers/fedora:30-esgz" // eStargz
FfmpegSociImage = "public.ecr.aws/soci-workshop-examples/ffmpeg:latest" // SOCI
UbuntuImage = "public.ecr.aws/docker/library/ubuntu:latest" // Large enough for testing soci index creation
)

type delayOnceReader struct {
Expand Down

0 comments on commit d4fd880

Please sign in to comment.