mirror of
https://github.com/linuxkit/linuxkit.git
synced 2026-04-03 10:33:29 +00:00
Split out into a small stub command line and a library
- this is pretty much the smallest change to split this out and it exposes a few things that can be improved later - no change to logging yet Signed-off-by: Justin Cormack <justin.cormack@docker.com>
This commit is contained in:
@@ -1,23 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/moby/tool/src/moby"
|
||||
)
|
||||
|
||||
const defaultNameForStdin = "moby"
|
||||
@@ -36,52 +33,32 @@ func (o *outputList) Set(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var streamable = map[string]bool{
|
||||
"docker": true,
|
||||
"tar": true,
|
||||
}
|
||||
|
||||
type addFun func(*tar.Writer) error
|
||||
|
||||
const dockerfile = `
|
||||
FROM scratch
|
||||
|
||||
COPY . ./
|
||||
RUN rm -f Dockerfile
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--", "/bin/rc.init"]
|
||||
`
|
||||
|
||||
var additions = map[string]addFun{
|
||||
"docker": func(tw *tar.Writer) error {
|
||||
log.Infof(" Adding Dockerfile")
|
||||
hdr := &tar.Header{
|
||||
Name: "Dockerfile",
|
||||
Mode: 0644,
|
||||
Size: int64(len(dockerfile)),
|
||||
// Parse a string which is either a number in MB, or a number with
|
||||
// either M (for Megabytes) or G (for GigaBytes) as a suffix and
|
||||
// returns the number in MB. Return 0 if string is empty.
|
||||
func getDiskSizeMB(s string) (int, error) {
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
sz := len(s)
|
||||
if strings.HasSuffix(s, "G") {
|
||||
i, err := strconv.Atoi(s[:sz-1])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tw.Write([]byte(dockerfile)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
return i * 1024, nil
|
||||
}
|
||||
if strings.HasSuffix(s, "M") {
|
||||
s = s[:sz-1]
|
||||
}
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
|
||||
// Process the build arguments and execute build
|
||||
func build(args []string) {
|
||||
var buildOut outputList
|
||||
|
||||
outputTypes := []string{}
|
||||
for k := range streamable {
|
||||
outputTypes = append(outputTypes, k)
|
||||
}
|
||||
for k := range outFuns {
|
||||
outputTypes = append(outputTypes, k)
|
||||
}
|
||||
sort.Strings(outputTypes)
|
||||
outputTypes := moby.OutputTypes()
|
||||
|
||||
buildCmd := flag.NewFlagSet("build", flag.ExitOnError)
|
||||
buildCmd.Usage = func() {
|
||||
@@ -135,13 +112,13 @@ func build(args []string) {
|
||||
|
||||
if len(buildOut) > 1 {
|
||||
for _, o := range buildOut {
|
||||
if streamable[o] {
|
||||
if moby.Streamable(o) {
|
||||
log.Fatalf("Output type %s must be the only output specified", o)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(buildOut) == 1 && streamable[buildOut[0]] {
|
||||
if len(buildOut) == 1 && moby.Streamable(buildOut[0]) {
|
||||
if *buildOutputFile == "" {
|
||||
*buildOutputFile = filepath.Join(*buildDir, name+"."+buildOut[0])
|
||||
// stop the errors in the validation below
|
||||
@@ -150,7 +127,7 @@ func build(args []string) {
|
||||
}
|
||||
|
||||
} else {
|
||||
err := validateOutputs(buildOut)
|
||||
err := moby.ValidateOutputs(buildOut)
|
||||
if err != nil {
|
||||
log.Errorf("Error parsing outputs: %v", err)
|
||||
buildCmd.Usage()
|
||||
@@ -159,7 +136,6 @@ func build(args []string) {
|
||||
}
|
||||
|
||||
var outputFile *os.File
|
||||
var addition addFun
|
||||
if *buildOutputFile != "" {
|
||||
if len(buildOut) > 1 {
|
||||
log.Fatal("The -output option can only be specified when generating a single output format")
|
||||
@@ -170,7 +146,7 @@ func build(args []string) {
|
||||
if *buildDir != "" {
|
||||
log.Fatal("The -output option cannot be specified with -dir")
|
||||
}
|
||||
if !streamable[buildOut[0]] {
|
||||
if !moby.Streamable(buildOut[0]) {
|
||||
log.Fatalf("The -output option cannot be specified for build type %s as it cannot be streamed", buildOut[0])
|
||||
}
|
||||
if *buildOutputFile == "-" {
|
||||
@@ -183,7 +159,6 @@ func build(args []string) {
|
||||
}
|
||||
defer outputFile.Close()
|
||||
}
|
||||
addition = additions[buildOut[0]]
|
||||
}
|
||||
|
||||
size, err := getDiskSizeMB(*buildSize)
|
||||
@@ -191,7 +166,7 @@ func build(args []string) {
|
||||
log.Fatalf("Unable to parse disk size: %v", err)
|
||||
}
|
||||
|
||||
var moby Moby
|
||||
var m moby.Moby
|
||||
for _, arg := range remArgs {
|
||||
var config []byte
|
||||
if conf := arg; conf == "-" {
|
||||
@@ -220,16 +195,16 @@ func build(args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
m, err := NewConfig(config)
|
||||
c, err := moby.NewConfig(config)
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid config: %v", err)
|
||||
}
|
||||
moby = AppendConfig(moby, m)
|
||||
m = moby.AppendConfig(m, c)
|
||||
}
|
||||
|
||||
if *buildDisableTrust {
|
||||
log.Debugf("Disabling content trust checks for this build")
|
||||
moby.Trust = TrustConfig{}
|
||||
m.Trust = moby.TrustConfig{}
|
||||
}
|
||||
|
||||
var buf *bytes.Buffer
|
||||
@@ -240,7 +215,13 @@ func build(args []string) {
|
||||
buf = new(bytes.Buffer)
|
||||
w = buf
|
||||
}
|
||||
err = buildInternal(moby, w, *buildPull, addition)
|
||||
// this is a weird interface, but currently only streamable types can have additional files
|
||||
// need to split up the base tarball outputs from the secondary stages
|
||||
var tp string
|
||||
if moby.Streamable(buildOut[0]) {
|
||||
tp = buildOut[0]
|
||||
}
|
||||
err = moby.Build(m, w, *buildPull, tp)
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
@@ -248,418 +229,9 @@ func build(args []string) {
|
||||
if outputFile == nil {
|
||||
image := buf.Bytes()
|
||||
log.Infof("Create outputs:")
|
||||
err = outputs(filepath.Join(*buildDir, name), image, buildOut, size, *buildHyperkit)
|
||||
err = moby.Outputs(filepath.Join(*buildDir, name), image, buildOut, size, *buildHyperkit)
|
||||
if err != nil {
|
||||
log.Fatalf("Error writing outputs: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse a string which is either a number in MB, or a number with
|
||||
// either M (for Megabytes) or G (for GigaBytes) as a suffix and
|
||||
// returns the number in MB. Return 0 if string is empty.
|
||||
func getDiskSizeMB(s string) (int, error) {
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
sz := len(s)
|
||||
if strings.HasSuffix(s, "G") {
|
||||
i, err := strconv.Atoi(s[:sz-1])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return i * 1024, nil
|
||||
}
|
||||
if strings.HasSuffix(s, "M") {
|
||||
s = s[:sz-1]
|
||||
}
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
|
||||
func enforceContentTrust(fullImageName string, config *TrustConfig) bool {
|
||||
for _, img := range config.Image {
|
||||
// First check for an exact name match
|
||||
if img == fullImageName {
|
||||
return true
|
||||
}
|
||||
// Also check for an image name only match
|
||||
// by removing a possible tag (with possibly added digest):
|
||||
imgAndTag := strings.Split(fullImageName, ":")
|
||||
if len(imgAndTag) >= 2 && img == imgAndTag[0] {
|
||||
return true
|
||||
}
|
||||
// and by removing a possible digest:
|
||||
imgAndDigest := strings.Split(fullImageName, "@sha256:")
|
||||
if len(imgAndDigest) >= 2 && img == imgAndDigest[0] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, org := range config.Org {
|
||||
var imgOrg string
|
||||
splitName := strings.Split(fullImageName, "/")
|
||||
switch len(splitName) {
|
||||
case 0:
|
||||
// if the image is empty, return false
|
||||
return false
|
||||
case 1:
|
||||
// for single names like nginx, use library
|
||||
imgOrg = "library"
|
||||
case 2:
|
||||
// for names that assume docker hub, like linxukit/alpine, take the first split
|
||||
imgOrg = splitName[0]
|
||||
default:
|
||||
// for names that include the registry, the second piece is the org, ex: docker.io/library/alpine
|
||||
imgOrg = splitName[1]
|
||||
}
|
||||
if imgOrg == org {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Perform the actual build process
|
||||
func buildInternal(m Moby, w io.Writer, pull bool, addition addFun) error {
|
||||
iw := tar.NewWriter(w)
|
||||
|
||||
if m.Kernel.Image != "" {
|
||||
// get kernel and initrd tarball from container
|
||||
log.Infof("Extract kernel image: %s", m.Kernel.Image)
|
||||
kf := newKernelFilter(iw, m.Kernel.Cmdline)
|
||||
err := ImageTar(m.Kernel.Image, "", kf, enforceContentTrust(m.Kernel.Image, &m.Trust), pull)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to extract kernel image and tarball: %v", err)
|
||||
}
|
||||
err = kf.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Close error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// convert init images to tarballs
|
||||
if len(m.Init) != 0 {
|
||||
log.Infof("Add init containers:")
|
||||
}
|
||||
for _, ii := range m.Init {
|
||||
log.Infof("Process init image: %s", ii)
|
||||
err := ImageTar(ii, "", iw, enforceContentTrust(ii, &m.Trust), pull)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to build init tarball from %s: %v", ii, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.Onboot) != 0 {
|
||||
log.Infof("Add onboot containers:")
|
||||
}
|
||||
for i, image := range m.Onboot {
|
||||
log.Infof(" Create OCI config for %s", image.Image)
|
||||
useTrust := enforceContentTrust(image.Image, &m.Trust)
|
||||
config, err := ConfigToOCI(image, useTrust)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create config.json for %s: %v", image.Image, err)
|
||||
}
|
||||
so := fmt.Sprintf("%03d", i)
|
||||
path := "containers/onboot/" + so + "-" + image.Name
|
||||
err = ImageBundle(path, image.Image, config, iw, useTrust, pull)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to extract root filesystem for %s: %v", image.Image, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.Services) != 0 {
|
||||
log.Infof("Add service containers:")
|
||||
}
|
||||
for _, image := range m.Services {
|
||||
log.Infof(" Create OCI config for %s", image.Image)
|
||||
useTrust := enforceContentTrust(image.Image, &m.Trust)
|
||||
config, err := ConfigToOCI(image, useTrust)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create config.json for %s: %v", image.Image, err)
|
||||
}
|
||||
path := "containers/services/" + image.Name
|
||||
err = ImageBundle(path, image.Image, config, iw, useTrust, pull)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to extract root filesystem for %s: %v", image.Image, err)
|
||||
}
|
||||
}
|
||||
|
||||
// add files
|
||||
err := filesystem(m, iw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add filesystem parts: %v", err)
|
||||
}
|
||||
|
||||
// add anything additional for this output type
|
||||
if addition != nil {
|
||||
err = addition(iw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to add additional files: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = iw.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("initrd close error: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// kernelFilter is a tar.Writer that transforms a kernel image into the output we want on underlying tar writer
|
||||
type kernelFilter struct {
|
||||
tw *tar.Writer
|
||||
buffer *bytes.Buffer
|
||||
cmdline string
|
||||
discard bool
|
||||
foundKernel bool
|
||||
foundKTar bool
|
||||
}
|
||||
|
||||
func newKernelFilter(tw *tar.Writer, cmdline string) *kernelFilter {
|
||||
return &kernelFilter{tw: tw, cmdline: cmdline}
|
||||
}
|
||||
|
||||
func (k *kernelFilter) finishTar() error {
|
||||
if k.buffer == nil {
|
||||
return nil
|
||||
}
|
||||
tr := tar.NewReader(k.buffer)
|
||||
err := tarAppend(k.tw, tr)
|
||||
k.buffer = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (k *kernelFilter) Close() error {
|
||||
if !k.foundKernel {
|
||||
return errors.New("did not find kernel in kernel image")
|
||||
}
|
||||
if !k.foundKTar {
|
||||
return errors.New("did not find kernel.tar in kernel image")
|
||||
}
|
||||
return k.finishTar()
|
||||
}
|
||||
|
||||
func (k *kernelFilter) Flush() error {
|
||||
err := k.finishTar()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return k.tw.Flush()
|
||||
}
|
||||
|
||||
func (k *kernelFilter) Write(b []byte) (n int, err error) {
|
||||
if k.discard {
|
||||
return len(b), nil
|
||||
}
|
||||
if k.buffer != nil {
|
||||
return k.buffer.Write(b)
|
||||
}
|
||||
return k.tw.Write(b)
|
||||
}
|
||||
|
||||
func (k *kernelFilter) WriteHeader(hdr *tar.Header) error {
|
||||
err := k.finishTar()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tw := k.tw
|
||||
switch hdr.Name {
|
||||
case "kernel":
|
||||
if k.foundKernel {
|
||||
return errors.New("found more than one possible kernel image")
|
||||
}
|
||||
k.foundKernel = true
|
||||
k.discard = false
|
||||
whdr := &tar.Header{
|
||||
Name: "boot",
|
||||
Mode: 0755,
|
||||
Typeflag: tar.TypeDir,
|
||||
}
|
||||
if err := tw.WriteHeader(whdr); err != nil {
|
||||
return err
|
||||
}
|
||||
// add the cmdline in /boot/cmdline
|
||||
whdr = &tar.Header{
|
||||
Name: "boot/cmdline",
|
||||
Mode: 0644,
|
||||
Size: int64(len(k.cmdline)),
|
||||
}
|
||||
if err := tw.WriteHeader(whdr); err != nil {
|
||||
return err
|
||||
}
|
||||
buf := bytes.NewBufferString(k.cmdline)
|
||||
_, err = io.Copy(tw, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
whdr = &tar.Header{
|
||||
Name: "boot/kernel",
|
||||
Mode: hdr.Mode,
|
||||
Size: hdr.Size,
|
||||
}
|
||||
if err := tw.WriteHeader(whdr); err != nil {
|
||||
return err
|
||||
}
|
||||
case "kernel.tar":
|
||||
k.foundKTar = true
|
||||
k.discard = false
|
||||
k.buffer = new(bytes.Buffer)
|
||||
default:
|
||||
k.discard = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func tarAppend(iw *tar.Writer, tr *tar.Reader) error {
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = iw.WriteHeader(hdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(iw, tr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func filesystem(m Moby, tw *tar.Writer) error {
|
||||
// TODO also include the files added in other parts of the build
|
||||
var addedFiles = map[string]bool{}
|
||||
|
||||
if len(m.Files) != 0 {
|
||||
log.Infof("Add files:")
|
||||
}
|
||||
for _, f := range m.Files {
|
||||
log.Infof(" %s", f.Path)
|
||||
if f.Path == "" {
|
||||
return errors.New("Did not specify path for file")
|
||||
}
|
||||
// tar archives should not have absolute paths
|
||||
if f.Path[0] == os.PathSeparator {
|
||||
f.Path = f.Path[1:]
|
||||
}
|
||||
mode := int64(0600)
|
||||
if f.Directory {
|
||||
mode = 0700
|
||||
}
|
||||
if f.Mode != "" {
|
||||
var err error
|
||||
mode, err = strconv.ParseInt(f.Mode, 8, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot parse file mode as octal value: %v", err)
|
||||
}
|
||||
}
|
||||
dirMode := mode
|
||||
if dirMode&0700 != 0 {
|
||||
dirMode |= 0100
|
||||
}
|
||||
if dirMode&0070 != 0 {
|
||||
dirMode |= 0010
|
||||
}
|
||||
if dirMode&0007 != 0 {
|
||||
dirMode |= 0001
|
||||
}
|
||||
var contents []byte
|
||||
if f.Contents != nil {
|
||||
contents = []byte(*f.Contents)
|
||||
}
|
||||
if !f.Directory && f.Contents == nil && f.Symlink == "" {
|
||||
if f.Source == "" {
|
||||
return errors.New("Contents of file not specified")
|
||||
}
|
||||
if len(f.Source) > 2 && f.Source[:2] == "~/" {
|
||||
f.Source = homeDir() + f.Source[1:]
|
||||
}
|
||||
if f.Optional {
|
||||
_, err := os.Stat(f.Source)
|
||||
if err != nil {
|
||||
// skip if not found or readable
|
||||
log.Debugf("Skipping file [%s] as not readable and marked optional", f.Source)
|
||||
continue
|
||||
}
|
||||
}
|
||||
var err error
|
||||
contents, err = ioutil.ReadFile(f.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// we need all the leading directories
|
||||
parts := strings.Split(path.Dir(f.Path), "/")
|
||||
root := ""
|
||||
for _, p := range parts {
|
||||
if p == "." || p == "/" {
|
||||
continue
|
||||
}
|
||||
if root == "" {
|
||||
root = p
|
||||
} else {
|
||||
root = root + "/" + p
|
||||
}
|
||||
if !addedFiles[root] {
|
||||
hdr := &tar.Header{
|
||||
Name: root,
|
||||
Typeflag: tar.TypeDir,
|
||||
Mode: dirMode,
|
||||
}
|
||||
err := tw.WriteHeader(hdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addedFiles[root] = true
|
||||
}
|
||||
}
|
||||
addedFiles[f.Path] = true
|
||||
if f.Directory {
|
||||
if f.Contents != nil {
|
||||
return errors.New("Directory with contents not allowed")
|
||||
}
|
||||
hdr := &tar.Header{
|
||||
Name: f.Path,
|
||||
Typeflag: tar.TypeDir,
|
||||
Mode: mode,
|
||||
}
|
||||
err := tw.WriteHeader(hdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if f.Symlink != "" {
|
||||
hdr := &tar.Header{
|
||||
Name: f.Path,
|
||||
Typeflag: tar.TypeSymlink,
|
||||
Mode: mode,
|
||||
Linkname: f.Symlink,
|
||||
}
|
||||
err := tw.WriteHeader(hdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
hdr := &tar.Header{
|
||||
Name: f.Path,
|
||||
Mode: mode,
|
||||
Size: int64(len(contents)),
|
||||
}
|
||||
err := tw.WriteHeader(hdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tw.Write(contents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,758 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/xeipuuv/gojsonschema"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Moby is the type of a Moby config file
|
||||
type Moby struct {
|
||||
Kernel struct {
|
||||
Image string
|
||||
Cmdline string
|
||||
}
|
||||
Init []string
|
||||
Onboot []MobyImage
|
||||
Services []MobyImage
|
||||
Trust TrustConfig
|
||||
Files []struct {
|
||||
Path string
|
||||
Directory bool
|
||||
Symlink string
|
||||
Contents *string
|
||||
Source string
|
||||
Optional bool
|
||||
Mode string
|
||||
}
|
||||
}
|
||||
|
||||
// TrustConfig is the type of a content trust config
|
||||
type TrustConfig struct {
|
||||
Image []string
|
||||
Org []string
|
||||
}
|
||||
|
||||
// MobyImage is the type of an image config
|
||||
type MobyImage struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Image string `yaml:"image" json:"image"`
|
||||
Capabilities *[]string `yaml:"capabilities" json:"capabilities,omitempty"`
|
||||
Mounts *[]specs.Mount `yaml:"mounts" json:"mounts,omitempty"`
|
||||
Binds *[]string `yaml:"binds" json:"binds,omitempty"`
|
||||
Tmpfs *[]string `yaml:"tmpfs" json:"tmpfs,omitempty"`
|
||||
Command *[]string `yaml:"command" json:"command,omitempty"`
|
||||
Env *[]string `yaml:"env" json:"env,omitempty"`
|
||||
Cwd string `yaml:"cwd" json:"cwd"`
|
||||
Net string `yaml:"net" json:"net"`
|
||||
Pid string `yaml:"pid" json:"pid"`
|
||||
Ipc string `yaml:"ipc" json:"ipc"`
|
||||
Uts string `yaml:"uts" json:"uts"`
|
||||
Hostname string `yaml:"hostname" json:"hostname"`
|
||||
Readonly *bool `yaml:"readonly" json:"readonly,omitempty"`
|
||||
MaskedPaths *[]string `yaml:"maskedPaths" json:"maskedPaths,omitempty"`
|
||||
ReadonlyPaths *[]string `yaml:"readonlyPaths" json:"readonlyPaths,omitempty"`
|
||||
UID *uint32 `yaml:"uid" json:"uid,omitempty"`
|
||||
GID *uint32 `yaml:"gid" json:"gid,omitempty"`
|
||||
AdditionalGids *[]uint32 `yaml:"additionalGids" json:"additionalGids,omitempty"`
|
||||
NoNewPrivileges *bool `yaml:"noNewPrivileges" json:"noNewPrivileges,omitempty"`
|
||||
OOMScoreAdj *int `yaml:"oomScoreAdj" json:"oomScoreAdj,omitempty"`
|
||||
DisableOOMKiller *bool `yaml:"disableOOMKiller" json:"disableOOMKiller,omitempty"`
|
||||
RootfsPropagation *string `yaml:"rootfsPropagation" json:"rootfsPropagation,omitempty"`
|
||||
CgroupsPath *string `yaml:"cgroupsPath" json:"cgroupsPath,omitempty"`
|
||||
Sysctl *map[string]string `yaml:"sysctl" json:"sysctl,omitempty"`
|
||||
Rlimits *[]string `yaml:"rlimits" json:"rlimits,omitempty"`
|
||||
}
|
||||
|
||||
// github.com/go-yaml/yaml treats map keys as interface{} while encoding/json
|
||||
// requires them to be strings, integers or to implement encoding.TextMarshaler.
|
||||
// Fix this up by recursively mapping all map[interface{}]interface{} types into
|
||||
// map[string]interface{}.
|
||||
// see http://stackoverflow.com/questions/40737122/convert-yaml-to-json-without-struct-golang#answer-40737676
|
||||
func convert(i interface{}) interface{} {
|
||||
switch x := i.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
m2 := map[string]interface{}{}
|
||||
for k, v := range x {
|
||||
m2[k.(string)] = convert(v)
|
||||
}
|
||||
return m2
|
||||
case []interface{}:
|
||||
for i, v := range x {
|
||||
x[i] = convert(v)
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// NewConfig parses a config file
|
||||
func NewConfig(config []byte) (Moby, error) {
|
||||
m := Moby{}
|
||||
|
||||
// Parse raw yaml
|
||||
var rawYaml interface{}
|
||||
err := yaml.Unmarshal(config, &rawYaml)
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Convert to raw JSON
|
||||
rawJSON := convert(rawYaml)
|
||||
|
||||
// Validate raw yaml with JSON schema
|
||||
schemaLoader := gojsonschema.NewStringLoader(schema)
|
||||
documentLoader := gojsonschema.NewGoLoader(rawJSON)
|
||||
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
if !result.Valid() {
|
||||
fmt.Printf("The configuration file is invalid:\n")
|
||||
for _, desc := range result.Errors() {
|
||||
fmt.Printf("- %s\n", desc)
|
||||
}
|
||||
return m, fmt.Errorf("invalid configuration file")
|
||||
}
|
||||
|
||||
// Parse yaml
|
||||
err = yaml.Unmarshal(config, &m)
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// AppendConfig appends two configs.
|
||||
func AppendConfig(m0, m1 Moby) Moby {
|
||||
moby := m0
|
||||
if m1.Kernel.Image != "" {
|
||||
moby.Kernel.Image = m1.Kernel.Image
|
||||
}
|
||||
if m1.Kernel.Cmdline != "" {
|
||||
moby.Kernel.Cmdline = m1.Kernel.Cmdline
|
||||
}
|
||||
moby.Init = append(moby.Init, m1.Init...)
|
||||
moby.Onboot = append(moby.Onboot, m1.Onboot...)
|
||||
moby.Services = append(moby.Services, m1.Services...)
|
||||
moby.Files = append(moby.Files, m1.Files...)
|
||||
moby.Trust.Image = append(moby.Trust.Image, m1.Trust.Image...)
|
||||
moby.Trust.Org = append(moby.Trust.Org, m1.Trust.Org...)
|
||||
|
||||
return moby
|
||||
}
|
||||
|
||||
// NewImage validates an parses yaml or json for a MobyImage
|
||||
func NewImage(config []byte) (MobyImage, error) {
|
||||
log.Debugf("Reading label config: %s", string(config))
|
||||
|
||||
mi := MobyImage{}
|
||||
|
||||
// Parse raw yaml
|
||||
var rawYaml interface{}
|
||||
err := yaml.Unmarshal(config, &rawYaml)
|
||||
if err != nil {
|
||||
return mi, err
|
||||
}
|
||||
|
||||
// Convert to raw JSON
|
||||
rawJSON := convert(rawYaml)
|
||||
|
||||
// check it is an object not an array
|
||||
jsonObject, ok := rawJSON.(map[string]interface{})
|
||||
if !ok {
|
||||
return mi, fmt.Errorf("JSON is an array not an object: %s", string(config))
|
||||
}
|
||||
|
||||
// add a dummy name and image to pass validation
|
||||
var dummyName interface{}
|
||||
var dummyImage interface{}
|
||||
dummyName = "dummyname"
|
||||
dummyImage = "dummyimage"
|
||||
jsonObject["name"] = dummyName
|
||||
jsonObject["image"] = dummyImage
|
||||
|
||||
// Validate it as {"services": [config]}
|
||||
var services [1]interface{}
|
||||
services[0] = rawJSON
|
||||
serviceJSON := map[string]interface{}{"services": services}
|
||||
|
||||
// Validate serviceJSON with JSON schema
|
||||
schemaLoader := gojsonschema.NewStringLoader(schema)
|
||||
documentLoader := gojsonschema.NewGoLoader(serviceJSON)
|
||||
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
|
||||
if err != nil {
|
||||
return mi, err
|
||||
}
|
||||
if !result.Valid() {
|
||||
fmt.Printf("The org.mobyproject.config label is invalid:\n")
|
||||
for _, desc := range result.Errors() {
|
||||
fmt.Printf("- %s\n", desc)
|
||||
}
|
||||
return mi, fmt.Errorf("invalid configuration label")
|
||||
}
|
||||
|
||||
// Parse yaml
|
||||
err = yaml.Unmarshal(config, &mi)
|
||||
if err != nil {
|
||||
return mi, err
|
||||
}
|
||||
|
||||
if mi.Name != "" {
|
||||
return mi, fmt.Errorf("name cannot be set in metadata label")
|
||||
}
|
||||
if mi.Image != "" {
|
||||
return mi, fmt.Errorf("image cannot be set in metadata label")
|
||||
}
|
||||
|
||||
return mi, nil
|
||||
}
|
||||
|
||||
// ConfigToOCI converts a config specification to an OCI config file
|
||||
func ConfigToOCI(image MobyImage, trust bool) ([]byte, error) {
|
||||
|
||||
// TODO pass through same docker client to all functions
|
||||
cli, err := dockerClient()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
inspect, err := dockerInspectImage(cli, image.Image, trust)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
oci, err := ConfigInspectToOCI(image, inspect)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
return json.MarshalIndent(oci, "", " ")
|
||||
}
|
||||
|
||||
func defaultMountpoint(tp string) string {
|
||||
switch tp {
|
||||
case "proc":
|
||||
return "/proc"
|
||||
case "devpts":
|
||||
return "/dev/pts"
|
||||
case "sysfs":
|
||||
return "/sys"
|
||||
case "cgroup":
|
||||
return "/sys/fs/cgroup"
|
||||
case "mqueue":
|
||||
return "/dev/mqueue"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Sort mounts by number of path components so /dev/pts is listed after /dev
|
||||
type mlist []specs.Mount
|
||||
|
||||
func (m mlist) Len() int {
|
||||
return len(m)
|
||||
}
|
||||
func (m mlist) Less(i, j int) bool {
|
||||
return m.parts(i) < m.parts(j)
|
||||
}
|
||||
func (m mlist) Swap(i, j int) {
|
||||
m[i], m[j] = m[j], m[i]
|
||||
}
|
||||
func (m mlist) parts(i int) int {
|
||||
return strings.Count(filepath.Clean(m[i].Destination), string(os.PathSeparator))
|
||||
}
|
||||
|
||||
// assignBool does ordered overrides from JSON bool pointers
|
||||
func assignBool(v1, v2 *bool) bool {
|
||||
if v2 != nil {
|
||||
return *v2
|
||||
}
|
||||
if v1 != nil {
|
||||
return *v1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// assignBoolPtr does ordered overrides from JSON bool pointers
|
||||
func assignBoolPtr(v1, v2 *bool) *bool {
|
||||
if v2 != nil {
|
||||
return v2
|
||||
}
|
||||
if v1 != nil {
|
||||
return v1
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// assignIntPtr does ordered overrides from JSON int pointers
|
||||
func assignIntPtr(v1, v2 *int) *int {
|
||||
if v2 != nil {
|
||||
return v2
|
||||
}
|
||||
if v1 != nil {
|
||||
return v1
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// assignUint32 does ordered overrides from JSON uint32 pointers
|
||||
func assignUint32(v1, v2 *uint32) uint32 {
|
||||
if v2 != nil {
|
||||
return *v2
|
||||
}
|
||||
if v1 != nil {
|
||||
return *v1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// assignUint32Array does ordered overrides from JSON uint32 array pointers
|
||||
func assignUint32Array(v1, v2 *[]uint32) []uint32 {
|
||||
if v2 != nil {
|
||||
return *v2
|
||||
}
|
||||
if v1 != nil {
|
||||
return *v1
|
||||
}
|
||||
return []uint32{}
|
||||
}
|
||||
|
||||
// assignStrings does ordered overrides from JSON string array pointers
|
||||
func assignStrings(v1, v2 *[]string) []string {
|
||||
if v2 != nil {
|
||||
return *v2
|
||||
}
|
||||
if v1 != nil {
|
||||
return *v1
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// assignStrings3 does ordered overrides from JSON string array pointers
|
||||
func assignStrings3(v1 []string, v2, v3 *[]string) []string {
|
||||
if v3 != nil {
|
||||
return *v3
|
||||
}
|
||||
if v2 != nil {
|
||||
return *v2
|
||||
}
|
||||
return v1
|
||||
}
|
||||
|
||||
// assignMaps does ordered overrides from JSON string map pointers
|
||||
func assignMaps(v1, v2 *map[string]string) map[string]string {
|
||||
if v2 != nil {
|
||||
return *v2
|
||||
}
|
||||
if v1 != nil {
|
||||
return *v1
|
||||
}
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
// assignBinds does ordered overrides from JSON Bind array pointers
|
||||
func assignBinds(v1, v2 *[]specs.Mount) []specs.Mount {
|
||||
if v2 != nil {
|
||||
return *v2
|
||||
}
|
||||
if v1 != nil {
|
||||
return *v1
|
||||
}
|
||||
return []specs.Mount{}
|
||||
}
|
||||
|
||||
// assignString does ordered overrides from JSON string pointers
|
||||
func assignString(v1, v2 *string) string {
|
||||
if v2 != nil {
|
||||
return *v2
|
||||
}
|
||||
if v1 != nil {
|
||||
return *v1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// assignStringEmpty does ordered overrides if strings are empty, for
|
||||
// values where there is always an explicit override eg "none"
|
||||
func assignStringEmpty(v1, v2 string) string {
|
||||
if v2 != "" {
|
||||
return v2
|
||||
}
|
||||
return v1
|
||||
}
|
||||
|
||||
// assignStringEmpty3 does ordered overrides if strings are empty, for
|
||||
// values where there is always an explicit override eg "none"
|
||||
func assignStringEmpty3(v1, v2, v3 string) string {
|
||||
if v3 != "" {
|
||||
return v3
|
||||
}
|
||||
if v2 != "" {
|
||||
return v2
|
||||
}
|
||||
return v1
|
||||
}
|
||||
|
||||
// assign StringEmpty4 does ordered overrides if strings are empty, for
|
||||
// values where there is always an explicit override eg "none"
|
||||
func assignStringEmpty4(v1, v2, v3, v4 string) string {
|
||||
if v4 != "" {
|
||||
return v4
|
||||
}
|
||||
if v3 != "" {
|
||||
return v3
|
||||
}
|
||||
if v2 != "" {
|
||||
return v2
|
||||
}
|
||||
return v1
|
||||
}
|
||||
|
||||
// ConfigInspectToOCI converts a config and the output of image inspect to an OCI config
|
||||
func ConfigInspectToOCI(yaml MobyImage, inspect types.ImageInspect) (specs.Spec, error) {
|
||||
oci := specs.Spec{}
|
||||
|
||||
var inspectConfig container.Config
|
||||
if inspect.Config != nil {
|
||||
inspectConfig = *inspect.Config
|
||||
}
|
||||
|
||||
// look for org.mobyproject.config label
|
||||
var label MobyImage
|
||||
labelString := inspectConfig.Labels["org.mobyproject.config"]
|
||||
if labelString != "" {
|
||||
var err error
|
||||
label, err = NewImage([]byte(labelString))
|
||||
if err != nil {
|
||||
return oci, err
|
||||
}
|
||||
}
|
||||
|
||||
// command, env and cwd can be taken from image, as they are commonly specified in Dockerfile
|
||||
|
||||
// TODO we could handle entrypoint and cmd independently more like Docker
|
||||
inspectCommand := append(inspectConfig.Entrypoint, inspect.Config.Cmd...)
|
||||
args := assignStrings3(inspectCommand, label.Command, yaml.Command)
|
||||
|
||||
env := assignStrings3(inspectConfig.Env, label.Env, yaml.Env)
|
||||
|
||||
// empty Cwd not allowed in OCI, must be / in that case
|
||||
cwd := assignStringEmpty4("/", inspectConfig.WorkingDir, label.Cwd, yaml.Cwd)
|
||||
|
||||
// the other options will never be in the image config, but may be in label or yaml
|
||||
|
||||
readonly := assignBool(label.Readonly, yaml.Readonly)
|
||||
|
||||
// default options match what Docker does
|
||||
procOptions := []string{"nosuid", "nodev", "noexec", "relatime"}
|
||||
devOptions := []string{"nosuid", "strictatime", "mode=755", "size=65536k"}
|
||||
if readonly {
|
||||
devOptions = append(devOptions, "ro")
|
||||
}
|
||||
ptsOptions := []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620"}
|
||||
sysOptions := []string{"nosuid", "noexec", "nodev"}
|
||||
if readonly {
|
||||
sysOptions = append(sysOptions, "ro")
|
||||
}
|
||||
cgroupOptions := []string{"nosuid", "noexec", "nodev", "relatime", "ro"}
|
||||
// note omits "standard" /dev/shm and /dev/mqueue
|
||||
mounts := map[string]specs.Mount{
|
||||
"/proc": {Destination: "/proc", Type: "proc", Source: "proc", Options: procOptions},
|
||||
"/dev": {Destination: "/dev", Type: "tmpfs", Source: "tmpfs", Options: devOptions},
|
||||
"/dev/pts": {Destination: "/dev/pts", Type: "devpts", Source: "devpts", Options: ptsOptions},
|
||||
"/sys": {Destination: "/sys", Type: "sysfs", Source: "sysfs", Options: sysOptions},
|
||||
"/sys/fs/cgroup": {Destination: "/sys/fs/cgroup", Type: "cgroup", Source: "cgroup", Options: cgroupOptions},
|
||||
}
|
||||
for _, t := range assignStrings(label.Tmpfs, yaml.Tmpfs) {
|
||||
parts := strings.Split(t, ":")
|
||||
if len(parts) > 2 {
|
||||
return oci, fmt.Errorf("Cannot parse tmpfs, too many ':': %s", t)
|
||||
}
|
||||
dest := parts[0]
|
||||
opts := []string{}
|
||||
if len(parts) == 2 {
|
||||
opts = strings.Split(parts[1], ",")
|
||||
}
|
||||
mounts[dest] = specs.Mount{Destination: dest, Type: "tmpfs", Source: "tmpfs", Options: opts}
|
||||
}
|
||||
for _, b := range assignStrings(label.Binds, yaml.Binds) {
|
||||
parts := strings.Split(b, ":")
|
||||
if len(parts) < 2 {
|
||||
return oci, fmt.Errorf("Cannot parse bind, missing ':': %s", b)
|
||||
}
|
||||
if len(parts) > 3 {
|
||||
return oci, fmt.Errorf("Cannot parse bind, too many ':': %s", b)
|
||||
}
|
||||
src := parts[0]
|
||||
dest := parts[1]
|
||||
opts := []string{"rw", "rbind", "rprivate"}
|
||||
if len(parts) == 3 {
|
||||
opts = append(strings.Split(parts[2], ","), "rbind")
|
||||
}
|
||||
mounts[dest] = specs.Mount{Destination: dest, Type: "bind", Source: src, Options: opts}
|
||||
}
|
||||
for _, m := range assignBinds(label.Mounts, yaml.Mounts) {
|
||||
tp := m.Type
|
||||
src := m.Source
|
||||
dest := m.Destination
|
||||
opts := m.Options
|
||||
if tp == "" {
|
||||
switch src {
|
||||
case "mqueue", "devpts", "proc", "sysfs", "cgroup":
|
||||
tp = src
|
||||
}
|
||||
}
|
||||
if tp == "" && dest == "/dev" {
|
||||
tp = "tmpfs"
|
||||
}
|
||||
if tp == "" {
|
||||
return oci, fmt.Errorf("Mount for destination %s is missing type", dest)
|
||||
}
|
||||
if src == "" {
|
||||
// usually sane, eg proc, tmpfs etc
|
||||
src = tp
|
||||
}
|
||||
if dest == "" {
|
||||
dest = defaultMountpoint(tp)
|
||||
}
|
||||
if dest == "" {
|
||||
return oci, fmt.Errorf("Mount type %s is missing destination", tp)
|
||||
}
|
||||
mounts[dest] = specs.Mount{Destination: dest, Type: tp, Source: src, Options: opts}
|
||||
}
|
||||
mountList := mlist{}
|
||||
for _, m := range mounts {
|
||||
mountList = append(mountList, m)
|
||||
}
|
||||
sort.Sort(mountList)
|
||||
|
||||
namespaces := []specs.LinuxNamespace{}
|
||||
// to attach to an existing namespace, easiest to bind mount with nsfs in a system container
|
||||
|
||||
// net, ipc and uts namespaces: default to not creating a new namespace (usually host namespace)
|
||||
netNS := assignStringEmpty3("root", label.Net, yaml.Net)
|
||||
if netNS != "host" && netNS != "root" {
|
||||
if netNS == "none" || netNS == "new" {
|
||||
netNS = ""
|
||||
}
|
||||
namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.NetworkNamespace, Path: netNS})
|
||||
}
|
||||
ipcNS := assignStringEmpty3("root", label.Ipc, yaml.Ipc)
|
||||
if ipcNS != "host" && ipcNS != "root" {
|
||||
if ipcNS == "new" {
|
||||
ipcNS = ""
|
||||
}
|
||||
namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.IPCNamespace, Path: ipcNS})
|
||||
}
|
||||
utsNS := assignStringEmpty3("root", label.Uts, yaml.Uts)
|
||||
if utsNS != "host" && utsNS != "root" {
|
||||
if utsNS == "new" {
|
||||
utsNS = ""
|
||||
}
|
||||
namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.UTSNamespace, Path: utsNS})
|
||||
}
|
||||
|
||||
// default to creating a new pid namespace
|
||||
pidNS := assignStringEmpty(label.Pid, yaml.Pid)
|
||||
if pidNS != "host" && pidNS != "root" {
|
||||
if pidNS == "new" {
|
||||
pidNS = ""
|
||||
}
|
||||
namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.PIDNamespace, Path: pidNS})
|
||||
}
|
||||
|
||||
// Always create a new mount namespace
|
||||
namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.MountNamespace})
|
||||
|
||||
// TODO user, cgroup namespaces
|
||||
|
||||
caps := assignStrings(label.Capabilities, yaml.Capabilities)
|
||||
if len(caps) == 1 {
|
||||
switch cap := strings.ToLower(caps[0]); cap {
|
||||
case "none":
|
||||
caps = []string{}
|
||||
case "all":
|
||||
caps = []string{
|
||||
"CAP_AUDIT_CONTROL",
|
||||
"CAP_AUDIT_READ",
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_BLOCK_SUSPEND",
|
||||
"CAP_CHOWN",
|
||||
"CAP_DAC_OVERRIDE",
|
||||
"CAP_DAC_READ_SEARCH",
|
||||
"CAP_FOWNER",
|
||||
"CAP_FSETID",
|
||||
"CAP_IPC_LOCK",
|
||||
"CAP_IPC_OWNER",
|
||||
"CAP_KILL",
|
||||
"CAP_LEASE",
|
||||
"CAP_LINUX_IMMUTABLE",
|
||||
"CAP_MAC_ADMIN",
|
||||
"CAP_MAC_OVERRIDE",
|
||||
"CAP_MKNOD",
|
||||
"CAP_NET_ADMIN",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_BROADCAST",
|
||||
"CAP_NET_RAW",
|
||||
"CAP_SETFCAP",
|
||||
"CAP_SETGID",
|
||||
"CAP_SETPCAP",
|
||||
"CAP_SETUID",
|
||||
"CAP_SYSLOG",
|
||||
"CAP_SYS_ADMIN",
|
||||
"CAP_SYS_BOOT",
|
||||
"CAP_SYS_CHROOT",
|
||||
"CAP_SYS_MODULE",
|
||||
"CAP_SYS_NICE",
|
||||
"CAP_SYS_PACCT",
|
||||
"CAP_SYS_PTRACE",
|
||||
"CAP_SYS_RAWIO",
|
||||
"CAP_SYS_RESOURCE",
|
||||
"CAP_SYS_TIME",
|
||||
"CAP_SYS_TTY_CONFIG",
|
||||
"CAP_WAKE_ALARM",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rlimitsString := assignStrings(label.Rlimits, yaml.Rlimits)
|
||||
rlimits := []specs.LinuxRlimit{}
|
||||
for _, limitString := range rlimitsString {
|
||||
rs := strings.SplitN(limitString, ",", 3)
|
||||
var limit string
|
||||
var soft, hard uint64
|
||||
switch len(rs) {
|
||||
case 3:
|
||||
origLimit := limit
|
||||
limit = strings.ToUpper(strings.TrimSpace(rs[0]))
|
||||
if !strings.HasPrefix(limit, "RLIMIT_") {
|
||||
limit = "RLIMIT_" + limit
|
||||
}
|
||||
softString := strings.TrimSpace(rs[1])
|
||||
if strings.ToLower(softString) == "unlimited" {
|
||||
soft = 18446744073709551615
|
||||
} else {
|
||||
var err error
|
||||
soft, err = strconv.ParseUint(softString, 10, 64)
|
||||
if err != nil {
|
||||
return oci, fmt.Errorf("Cannot parse %s as uint64: %v", softString, err)
|
||||
}
|
||||
}
|
||||
hardString := strings.TrimSpace(rs[2])
|
||||
if strings.ToLower(hardString) == "unlimited" {
|
||||
hard = 18446744073709551615
|
||||
} else {
|
||||
var err error
|
||||
hard, err = strconv.ParseUint(hardString, 10, 64)
|
||||
if err != nil {
|
||||
return oci, fmt.Errorf("Cannot parse %s as uint64: %v", hardString, err)
|
||||
}
|
||||
}
|
||||
switch limit {
|
||||
case
|
||||
"RLIMIT_CPU",
|
||||
"RLIMIT_FSIZE",
|
||||
"RLIMIT_DATA",
|
||||
"RLIMIT_STACK",
|
||||
"RLIMIT_CORE",
|
||||
"RLIMIT_RSS",
|
||||
"RLIMIT_NPROC",
|
||||
"RLIMIT_NOFILE",
|
||||
"RLIMIT_MEMLOCK",
|
||||
"RLIMIT_AS",
|
||||
"RLIMIT_LOCKS",
|
||||
"RLIMIT_SIGPENDING",
|
||||
"RLIMIT_MSGQUEUE",
|
||||
"RLIMIT_NICE",
|
||||
"RLIMIT_RTPRIO",
|
||||
"RLIMIT_RTTIME":
|
||||
rlimits = append(rlimits, specs.LinuxRlimit{Type: limit, Soft: soft, Hard: hard})
|
||||
default:
|
||||
return oci, fmt.Errorf("Unknown limit: %s", origLimit)
|
||||
}
|
||||
default:
|
||||
return oci, fmt.Errorf("Cannot parse rlimit: %s", rlimitsString)
|
||||
}
|
||||
}
|
||||
|
||||
oci.Version = specs.Version
|
||||
|
||||
oci.Platform = specs.Platform{
|
||||
OS: inspect.Os,
|
||||
Arch: inspect.Architecture,
|
||||
}
|
||||
|
||||
oci.Process = specs.Process{
|
||||
Terminal: false,
|
||||
//ConsoleSize
|
||||
User: specs.User{
|
||||
UID: assignUint32(label.UID, yaml.UID),
|
||||
GID: assignUint32(label.GID, yaml.GID),
|
||||
AdditionalGids: assignUint32Array(label.AdditionalGids, yaml.AdditionalGids),
|
||||
// Username (Windows)
|
||||
},
|
||||
Args: args,
|
||||
Env: env,
|
||||
Cwd: cwd,
|
||||
Capabilities: &specs.LinuxCapabilities{
|
||||
Bounding: caps,
|
||||
Effective: caps,
|
||||
Inheritable: caps,
|
||||
Permitted: caps,
|
||||
Ambient: []string{},
|
||||
},
|
||||
Rlimits: rlimits,
|
||||
NoNewPrivileges: assignBool(label.NoNewPrivileges, yaml.NoNewPrivileges),
|
||||
// ApparmorProfile
|
||||
// TODO FIXME this has moved in runc spec and needs a revendor and update
|
||||
//OOMScoreAdj: assignIntPtr(label.OOMScoreAdj, yaml.OOMScoreAdj),
|
||||
// SelinuxLabel
|
||||
}
|
||||
|
||||
oci.Root = specs.Root{
|
||||
Path: "rootfs",
|
||||
Readonly: readonly,
|
||||
}
|
||||
|
||||
oci.Hostname = assignStringEmpty(label.Hostname, yaml.Hostname)
|
||||
oci.Mounts = mountList
|
||||
|
||||
oci.Linux = &specs.Linux{
|
||||
// UIDMappings
|
||||
// GIDMappings
|
||||
Sysctl: assignMaps(label.Sysctl, yaml.Sysctl),
|
||||
Resources: &specs.LinuxResources{
|
||||
// Devices
|
||||
DisableOOMKiller: assignBoolPtr(label.DisableOOMKiller, yaml.DisableOOMKiller),
|
||||
// Memory
|
||||
// CPU
|
||||
// Pids
|
||||
// BlockIO
|
||||
// HugepageLimits
|
||||
// Network
|
||||
},
|
||||
CgroupsPath: assignString(label.CgroupsPath, yaml.CgroupsPath),
|
||||
Namespaces: namespaces,
|
||||
// Devices
|
||||
// Seccomp
|
||||
RootfsPropagation: assignString(label.RootfsPropagation, yaml.RootfsPropagation),
|
||||
MaskedPaths: assignStrings(label.MaskedPaths, yaml.MaskedPaths),
|
||||
ReadonlyPaths: assignStrings(label.ReadonlyPaths, yaml.ReadonlyPaths),
|
||||
// MountLabel
|
||||
// IntelRdt
|
||||
}
|
||||
|
||||
return oci, nil
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
func TestOverrides(t *testing.T) {
|
||||
var yamlCaps = []string{"CAP_SYS_ADMIN"}
|
||||
|
||||
var yaml = MobyImage{
|
||||
Name: "test",
|
||||
Image: "testimage",
|
||||
Capabilities: &yamlCaps,
|
||||
}
|
||||
|
||||
var labelCaps = []string{"CAP_SYS_CHROOT"}
|
||||
|
||||
var label = MobyImage{
|
||||
Capabilities: &labelCaps,
|
||||
Cwd: "/label/directory",
|
||||
}
|
||||
|
||||
var inspect types.ImageInspect
|
||||
var config container.Config
|
||||
|
||||
labelJSON, err := json.Marshal(label)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
config.Labels = map[string]string{"org.mobyproject.config": string(labelJSON)}
|
||||
|
||||
inspect.Config = &config
|
||||
|
||||
oci, err := ConfigInspectToOCI(yaml, inspect)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(oci.Process.Capabilities.Bounding, yamlCaps) {
|
||||
t.Error("Expected yaml capabilities to override but got", oci.Process.Capabilities.Bounding)
|
||||
}
|
||||
if oci.Process.Cwd != label.Cwd {
|
||||
t.Error("Expected label Cwd to be applied, got", oci.Process.Cwd)
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
package main
|
||||
|
||||
// We want to replace much of this with use of containerd tools
|
||||
// and also using the Docker API not shelling out
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func dockerRun(input io.Reader, args ...string) ([]byte, error) {
|
||||
log.Debugf("docker run (input): %s", strings.Join(args, " "))
|
||||
docker, err := exec.LookPath("docker")
|
||||
if err != nil {
|
||||
return []byte{}, errors.New("Docker does not seem to be installed")
|
||||
}
|
||||
args = append([]string{"run", "--rm", "-i"}, args...)
|
||||
cmd := exec.Command(docker, args...)
|
||||
cmd.Stdin = input
|
||||
|
||||
stderrPipe, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
stdout, err := ioutil.ReadAll(stdoutPipe)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
stderr, err := ioutil.ReadAll(stderrPipe)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
return []byte{}, fmt.Errorf("%v: %s", err, stderr)
|
||||
}
|
||||
|
||||
log.Debugf("docker run (input): %s...Done", strings.Join(args, " "))
|
||||
return stdout, nil
|
||||
}
|
||||
|
||||
func dockerCreate(image string) (string, error) {
|
||||
log.Debugf("docker create: %s", image)
|
||||
cli, err := dockerClient()
|
||||
if err != nil {
|
||||
return "", errors.New("could not initialize Docker API client")
|
||||
}
|
||||
// we do not ever run the container, so /dev/null is used as command
|
||||
config := &container.Config{
|
||||
Cmd: []string{"/dev/null"},
|
||||
Image: image,
|
||||
}
|
||||
respBody, err := cli.ContainerCreate(context.Background(), config, nil, nil, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Debugf("docker create: %s...Done", image)
|
||||
return respBody.ID, nil
|
||||
}
|
||||
|
||||
func dockerExport(container string) ([]byte, error) {
|
||||
log.Debugf("docker export: %s", container)
|
||||
cli, err := dockerClient()
|
||||
if err != nil {
|
||||
return []byte{}, errors.New("could not initialize Docker API client")
|
||||
}
|
||||
responseBody, err := cli.ContainerExport(context.Background(), container)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
defer responseBody.Close()
|
||||
|
||||
output := bytes.NewBuffer(nil)
|
||||
_, err = io.Copy(output, responseBody)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
return output.Bytes(), nil
|
||||
}
|
||||
|
||||
func dockerRm(container string) error {
|
||||
log.Debugf("docker rm: %s", container)
|
||||
cli, err := dockerClient()
|
||||
if err != nil {
|
||||
return errors.New("could not initialize Docker API client")
|
||||
}
|
||||
if err = cli.ContainerRemove(context.Background(), container, types.ContainerRemoveOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("docker rm: %s...Done", container)
|
||||
return nil
|
||||
}
|
||||
|
||||
func dockerPull(image string, forcePull, trustedPull bool) error {
|
||||
log.Debugf("docker pull: %s", image)
|
||||
cli, err := dockerClient()
|
||||
if err != nil {
|
||||
return errors.New("could not initialize Docker API client")
|
||||
}
|
||||
|
||||
if trustedPull {
|
||||
log.Debugf("pulling %s with content trust", image)
|
||||
trustedImg, err := TrustedReference(image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Trusted pull for %s failed: %v", image, err)
|
||||
}
|
||||
|
||||
// tag the image on a best-effort basis after pulling with content trust,
|
||||
// ensuring that docker picks up the tag and digest fom the canonical format
|
||||
defer func(src, dst string) {
|
||||
if err := cli.ImageTag(context.Background(), src, dst); err != nil {
|
||||
log.Debugf("could not tag trusted image %s to %s", src, dst)
|
||||
}
|
||||
}(trustedImg.String(), image)
|
||||
|
||||
log.Debugf("successfully verified trusted reference %s from notary", trustedImg.String())
|
||||
image = trustedImg.String()
|
||||
|
||||
imageSearchArg := filters.NewArgs()
|
||||
imageSearchArg.Add("reference", trustedImg.String())
|
||||
if _, err := cli.ImageList(context.Background(), types.ImageListOptions{Filters: imageSearchArg}); err == nil && !forcePull {
|
||||
log.Debugf("docker pull: trusted image %s already cached...Done", trustedImg.String())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Pull image: %s", image)
|
||||
r, err := cli.ImagePull(context.Background(), image, types.ImagePullOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
_, err = io.Copy(ioutil.Discard, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("docker pull: %s...Done", image)
|
||||
return nil
|
||||
}
|
||||
|
||||
func dockerClient() (*client.Client, error) {
|
||||
// for maximum compatibility as we use nothing new
|
||||
err := os.Setenv("DOCKER_API_VERSION", "1.23")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.NewEnvClient()
|
||||
}
|
||||
|
||||
func dockerInspectImage(cli *client.Client, image string, trustedPull bool) (types.ImageInspect, error) {
|
||||
log.Debugf("docker inspect image: %s", image)
|
||||
|
||||
inspect, _, err := cli.ImageInspectWithRaw(context.Background(), image)
|
||||
if err != nil {
|
||||
if client.IsErrImageNotFound(err) {
|
||||
pullErr := dockerPull(image, true, trustedPull)
|
||||
if pullErr != nil {
|
||||
return types.ImageInspect{}, pullErr
|
||||
}
|
||||
inspect, _, err = cli.ImageInspectWithRaw(context.Background(), image)
|
||||
if err != nil {
|
||||
return types.ImageInspect{}, err
|
||||
}
|
||||
} else {
|
||||
return types.ImageInspect{}, err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("docker inspect image: %s...Done", image)
|
||||
|
||||
return inspect, nil
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
type tarWriter interface {
|
||||
Close() error
|
||||
Flush() error
|
||||
Write(b []byte) (n int, err error)
|
||||
WriteHeader(hdr *tar.Header) error
|
||||
}
|
||||
|
||||
// This uses Docker to convert a Docker image into a tarball. It would be an improvement if we
|
||||
// used the containerd libraries to do this instead locally direct from a local image
|
||||
// cache as it would be much simpler.
|
||||
|
||||
var exclude = map[string]bool{
|
||||
".dockerenv": true,
|
||||
"Dockerfile": true,
|
||||
"dev/console": true,
|
||||
"dev/pts": true,
|
||||
"dev/shm": true,
|
||||
"etc/hostname": true,
|
||||
}
|
||||
|
||||
var replace = map[string]string{
|
||||
"etc/hosts": `127.0.0.1 localhost
|
||||
::1 localhost ip6-localhost ip6-loopback
|
||||
fe00::0 ip6-localnet
|
||||
ff00::0 ip6-mcastprefix
|
||||
ff02::1 ip6-allnodes
|
||||
ff02::2 ip6-allrouters
|
||||
`,
|
||||
"etc/resolv.conf": `nameserver 8.8.8.8
|
||||
nameserver 8.8.4.4
|
||||
nameserver 2001:4860:4860::8888
|
||||
nameserver 2001:4860:4860::8844
|
||||
`,
|
||||
}
|
||||
|
||||
// tarPrefix creates the leading directories for a path
|
||||
func tarPrefix(path string, tw tarWriter) error {
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
if path[len(path)-1] != byte('/') {
|
||||
return fmt.Errorf("path does not end with /: %s", path)
|
||||
}
|
||||
path = path[:len(path)-1]
|
||||
if path[0] == byte('/') {
|
||||
return fmt.Errorf("path should be relative: %s", path)
|
||||
}
|
||||
mkdir := ""
|
||||
for _, dir := range strings.Split(path, "/") {
|
||||
mkdir = mkdir + dir
|
||||
hdr := &tar.Header{
|
||||
Name: mkdir,
|
||||
Mode: 0755,
|
||||
Typeflag: tar.TypeDir,
|
||||
}
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
mkdir = mkdir + "/"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImageTar takes a Docker image and outputs it to a tar stream
|
||||
func ImageTar(image, prefix string, tw tarWriter, trust bool, pull bool) error {
|
||||
log.Debugf("image tar: %s %s", image, prefix)
|
||||
if prefix != "" && prefix[len(prefix)-1] != byte('/') {
|
||||
return fmt.Errorf("prefix does not end with /: %s", prefix)
|
||||
}
|
||||
|
||||
err := tarPrefix(prefix, tw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pull || trust {
|
||||
err := dockerPull(image, pull, trust)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not pull image %s: %v", image, err)
|
||||
}
|
||||
}
|
||||
container, err := dockerCreate(image)
|
||||
if err != nil {
|
||||
// if the image wasn't found, pull it down. Bail on other errors.
|
||||
if strings.Contains(err.Error(), "No such image") {
|
||||
err := dockerPull(image, true, trust)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not pull image %s: %v", image, err)
|
||||
}
|
||||
container, err = dockerCreate(image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to docker create image %s: %v", image, err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("Failed to create docker image %s: %v", image, err)
|
||||
}
|
||||
}
|
||||
contents, err := dockerExport(container)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to docker export container from container %s: %v", container, err)
|
||||
}
|
||||
err = dockerRm(container)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to docker rm container %s: %v", container, err)
|
||||
}
|
||||
|
||||
// now we need to filter out some files from the resulting tar archive
|
||||
|
||||
r := bytes.NewReader(contents)
|
||||
tr := tar.NewReader(r)
|
||||
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exclude[hdr.Name] {
|
||||
log.Debugf("image tar: %s %s exclude %s", image, prefix, hdr.Name)
|
||||
_, err = io.Copy(ioutil.Discard, tr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if replace[hdr.Name] != "" {
|
||||
contents := replace[hdr.Name]
|
||||
hdr.Size = int64(len(contents))
|
||||
hdr.Name = prefix + hdr.Name
|
||||
log.Debugf("image tar: %s %s add %s", image, prefix, hdr.Name)
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
buf := bytes.NewBufferString(contents)
|
||||
_, err = io.Copy(tw, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(ioutil.Discard, tr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Debugf("image tar: %s %s add %s", image, prefix, hdr.Name)
|
||||
hdr.Name = prefix + hdr.Name
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(tw, tr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImageBundle produces an OCI bundle at the given path in a tarball, given an image and a config.json
|
||||
func ImageBundle(path string, image string, config []byte, tw tarWriter, trust bool, pull bool) error {
|
||||
log.Debugf("image bundle: %s %s cfg: %s", path, image, string(config))
|
||||
err := ImageTar(image, path+"/rootfs/", tw, trust, pull)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hdr := &tar.Header{
|
||||
Name: path + "/" + "config.json",
|
||||
Mode: 0644,
|
||||
Size: int64(len(config)),
|
||||
}
|
||||
err = tw.WriteHeader(hdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf := bytes.NewBuffer(config)
|
||||
_, err = io.Copy(tw, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
var linuxkitYaml = map[string]string{"mkimage": `
|
||||
kernel:
|
||||
image: "linuxkit/kernel:4.9.x"
|
||||
cmdline: "console=ttyS0"
|
||||
init:
|
||||
- linuxkit/init:1b8a7e394d2ec2f1fdb4d67645829d1b5bdca037
|
||||
- linuxkit/runc:3a4e6cbf15470f62501b019b55e1caac5ee7689f
|
||||
- linuxkit/containerd:b1766e4c4c09f63ac4925a6e4612852a93f7e73b
|
||||
onboot:
|
||||
- name: mkimage
|
||||
image: "linuxkit/mkimage:5ad60299be03008f29c5caec3c5ea4ac0387aae6"
|
||||
- name: poweroff
|
||||
image: "linuxkit/poweroff:a8f1e4ad8d459f1fdaad9e4b007512cb3b504ae8"
|
||||
trust:
|
||||
org:
|
||||
- linuxkit
|
||||
`}
|
||||
|
||||
func imageFilename(name string) string {
|
||||
yaml := linuxkitYaml[name]
|
||||
hash := sha256.Sum256([]byte(yaml))
|
||||
return filepath.Join(MobyDir, "linuxkit", name+"-"+fmt.Sprintf("%x", hash))
|
||||
}
|
||||
|
||||
func ensureLinuxkitImage(name string) error {
|
||||
filename := imageFilename(name)
|
||||
_, err1 := os.Stat(filename + "-kernel")
|
||||
_, err2 := os.Stat(filename + "-initrd.img")
|
||||
_, err3 := os.Stat(filename + "-cmdline")
|
||||
if err1 == nil && err2 == nil && err3 == nil {
|
||||
return nil
|
||||
}
|
||||
err := os.MkdirAll(filepath.Join(MobyDir, "linuxkit"), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO clean up old files
|
||||
log.Infof("Building LinuxKit image %s to generate output formats", name)
|
||||
|
||||
yaml := linuxkitYaml[name]
|
||||
|
||||
m, err := NewConfig([]byte(yaml))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO pass through --pull to here
|
||||
buf := new(bytes.Buffer)
|
||||
buildInternal(m, buf, false, nil)
|
||||
image := buf.Bytes()
|
||||
kernel, initrd, cmdline, err := tarToInitrd(image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error converting to initrd: %v", err)
|
||||
}
|
||||
err = writeKernelInitrd(filename, kernel, initrd, cmdline)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeKernelInitrd(filename string, kernel []byte, initrd []byte, cmdline string) error {
|
||||
err := ioutil.WriteFile(filename+"-kernel", kernel, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(filename+"-initrd.img", initrd, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(filename+"-cmdline", []byte(cmdline), 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputLinuxKit(format string, filename string, kernel []byte, initrd []byte, cmdline string, size int, hyperkit bool) error {
|
||||
log.Debugf("output linuxkit generated img: %s %s size %d", format, filename, size)
|
||||
|
||||
tmp, err := ioutil.TempDir(filepath.Join(MobyDir, "tmp"), "moby")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
buf, err := tarInitrdKernel(kernel, initrd, cmdline)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tardisk := filepath.Join(tmp, "tardisk")
|
||||
f, err := os.Create(tardisk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(f, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sizeString := fmt.Sprintf("%dM", size)
|
||||
_ = os.Remove(filename)
|
||||
_, err = os.Stat(filename)
|
||||
if err == nil || !os.IsNotExist(err) {
|
||||
return fmt.Errorf("Cannot remove existing file [%s]", filename)
|
||||
}
|
||||
linuxkit, err := exec.LookPath("linuxkit")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot find linuxkit executable, needed to build %s output type: %v", format, err)
|
||||
}
|
||||
commandLine := []string{"-q", "run", "qemu", "-disk", fmt.Sprintf("%s,size=%s,format=%s", filename, sizeString, format), "-disk", fmt.Sprintf("%s,format=raw", tardisk), "-kernel", imageFilename("mkimage")}
|
||||
if hyperkit && format == "raw" {
|
||||
state, err := ioutil.TempDir("", "s")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(state)
|
||||
commandLine = []string{"-q", "run", "hyperkit", "-state", state, "-disk", fmt.Sprintf("%s,size=%s,format=%s", filename, sizeString, format), "-disk", fmt.Sprintf("%s,format=raw", tardisk), imageFilename("mkimage")}
|
||||
}
|
||||
log.Debugf("run %s: %v", linuxkit, commandLine)
|
||||
cmd := exec.Command(linuxkit, commandLine...)
|
||||
cmd.Stderr = os.Stderr
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/moby/tool/src/moby"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -17,9 +18,6 @@ var (
|
||||
|
||||
// GitCommit hash, set at compile time
|
||||
GitCommit = "unknown"
|
||||
|
||||
// MobyDir is the location of the cache directory ~/.moby by default
|
||||
MobyDir string
|
||||
)
|
||||
|
||||
// infoFormatter overrides the default format for Info() log events to
|
||||
@@ -91,16 +89,17 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
MobyDir = *flagConfigDir
|
||||
err := os.MkdirAll(MobyDir, 0755)
|
||||
mobyDir := *flagConfigDir
|
||||
err := os.MkdirAll(mobyDir, 0755)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not create config directory [%s]: %v", MobyDir, err)
|
||||
log.Fatalf("Could not create config directory [%s]: %v", mobyDir, err)
|
||||
}
|
||||
|
||||
err = os.MkdirAll(filepath.Join(MobyDir, "tmp"), 0755)
|
||||
err = os.MkdirAll(filepath.Join(mobyDir, "tmp"), 0755)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not create config tmp directory [%s]: %v", filepath.Join(MobyDir, "tmp"), err)
|
||||
log.Fatalf("Could not create config tmp directory [%s]: %v", filepath.Join(mobyDir, "tmp"), err)
|
||||
}
|
||||
moby.MobyDir = mobyDir
|
||||
|
||||
switch args[0] {
|
||||
case "build":
|
||||
|
||||
@@ -1,286 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/moby/tool/src/initrd"
|
||||
)
|
||||
|
||||
const (
|
||||
bios = "linuxkit/mkimage-iso-bios:db791abed6f2b5320feb6cec255a635aee3756f6@sha256:e57483075307bcea4a7257f87eee733d3e24e7a964ba15dcc01111df6729ab3b"
|
||||
efi = "linuxkit/mkimage-iso-efi:5c2fc616bde288476a14f4f6dd0d273a66832822@sha256:876ef47ec2b30af40e70f1e98f496206eb430915867c4f9f400e1af47fd58d7c"
|
||||
gcp = "linuxkit/mkimage-gcp:46716b3d3f7aa1a7607a3426fe0ccebc554b14ee@sha256:18d8e0482f65a2481f5b6ba1e7ce77723b246bf13bdb612be5e64df90297940c"
|
||||
vhd = "linuxkit/mkimage-vhd:a04c8480d41ca9cef6b7710bd45a592220c3acb2@sha256:ba373dc8ae5dc72685dbe4b872d8f588bc68b2114abd8bdc6a74d82a2b62cce3"
|
||||
vmdk = "linuxkit/mkimage-vmdk:182b541474ca7965c8e8f987389b651859f760da@sha256:99638c5ddb17614f54c6b8e11bd9d49d1dea9d837f38e0f6c1a5f451085d449b"
|
||||
)
|
||||
|
||||
var outFuns = map[string]func(string, []byte, int, bool) error{
|
||||
"kernel+initrd": func(base string, image []byte, size int, hyperkit bool) error {
|
||||
kernel, initrd, cmdline, err := tarToInitrd(image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error converting to initrd: %v", err)
|
||||
}
|
||||
err = outputKernelInitrd(base, kernel, initrd, cmdline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error writing kernel+initrd output: %v", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
"iso-bios": func(base string, image []byte, size int, hyperkit bool) error {
|
||||
kernel, initrd, cmdline, err := tarToInitrd(image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error converting to initrd: %v", err)
|
||||
}
|
||||
err = outputImg(bios, base+".iso", kernel, initrd, cmdline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error writing iso-bios output: %v", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
"iso-efi": func(base string, image []byte, size int, hyperkit bool) error {
|
||||
kernel, initrd, cmdline, err := tarToInitrd(image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error converting to initrd: %v", err)
|
||||
}
|
||||
err = outputImg(efi, base+"-efi.iso", kernel, initrd, cmdline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error writing iso-efi output: %v", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
"raw": func(base string, image []byte, size int, hyperkit bool) error {
|
||||
filename := base + ".raw"
|
||||
log.Infof(" %s", filename)
|
||||
kernel, initrd, cmdline, err := tarToInitrd(image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error converting to initrd: %v", err)
|
||||
}
|
||||
err = outputLinuxKit("raw", filename, kernel, initrd, cmdline, size, hyperkit)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error writing raw output: %v", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
"gcp": func(base string, image []byte, size int, hyperkit bool) error {
|
||||
kernel, initrd, cmdline, err := tarToInitrd(image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error converting to initrd: %v", err)
|
||||
}
|
||||
err = outputImg(gcp, base+".img.tar.gz", kernel, initrd, cmdline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error writing gcp output: %v", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
"qcow2": func(base string, image []byte, size int, hyperkit bool) error {
|
||||
filename := base + ".qcow2"
|
||||
log.Infof(" %s", filename)
|
||||
kernel, initrd, cmdline, err := tarToInitrd(image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error converting to initrd: %v", err)
|
||||
}
|
||||
err = outputLinuxKit("qcow2", filename, kernel, initrd, cmdline, size, hyperkit)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error writing qcow2 output: %v", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
"vhd": func(base string, image []byte, size int, hyperkit bool) error {
|
||||
kernel, initrd, cmdline, err := tarToInitrd(image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error converting to initrd: %v", err)
|
||||
}
|
||||
err = outputImg(vhd, base+".vhd", kernel, initrd, cmdline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error writing vhd output: %v", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
"vmdk": func(base string, image []byte, size int, hyperkit bool) error {
|
||||
kernel, initrd, cmdline, err := tarToInitrd(image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error converting to initrd: %v", err)
|
||||
}
|
||||
err = outputImg(vmdk, base+".vmdk", kernel, initrd, cmdline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error writing vmdk output: %v", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var prereq = map[string]string{
|
||||
"raw": "mkimage",
|
||||
"qcow2": "mkimage",
|
||||
}
|
||||
|
||||
func ensurePrereq(out string) error {
|
||||
var err error
|
||||
p := prereq[out]
|
||||
if p != "" {
|
||||
err = ensureLinuxkitImage(p)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func validateOutputs(out outputList) error {
|
||||
log.Debugf("validating output: %v", out)
|
||||
|
||||
for _, o := range out {
|
||||
f := outFuns[o]
|
||||
if f == nil {
|
||||
return fmt.Errorf("Unknown output type %s", o)
|
||||
}
|
||||
err := ensurePrereq(o)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to set up output type %s: %v", o, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputs(base string, image []byte, out outputList, size int, hyperkit bool) error {
|
||||
log.Debugf("output: %v %s", out, base)
|
||||
|
||||
err := validateOutputs(out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, o := range out {
|
||||
f := outFuns[o]
|
||||
err := f(base, image, size, hyperkit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func tarToInitrd(image []byte) ([]byte, []byte, string, error) {
|
||||
w := new(bytes.Buffer)
|
||||
iw := initrd.NewWriter(w)
|
||||
r := bytes.NewReader(image)
|
||||
tr := tar.NewReader(r)
|
||||
kernel, cmdline, err := initrd.CopySplitTar(iw, tr)
|
||||
if err != nil {
|
||||
return []byte{}, []byte{}, "", err
|
||||
}
|
||||
iw.Close()
|
||||
return kernel, w.Bytes(), cmdline, nil
|
||||
}
|
||||
|
||||
func tarInitrdKernel(kernel, initrd []byte, cmdline string) (*bytes.Buffer, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
tw := tar.NewWriter(buf)
|
||||
hdr := &tar.Header{
|
||||
Name: "kernel",
|
||||
Mode: 0600,
|
||||
Size: int64(len(kernel)),
|
||||
}
|
||||
err := tw.WriteHeader(hdr)
|
||||
if err != nil {
|
||||
return buf, err
|
||||
}
|
||||
_, err = tw.Write(kernel)
|
||||
if err != nil {
|
||||
return buf, err
|
||||
}
|
||||
hdr = &tar.Header{
|
||||
Name: "initrd.img",
|
||||
Mode: 0600,
|
||||
Size: int64(len(initrd)),
|
||||
}
|
||||
err = tw.WriteHeader(hdr)
|
||||
if err != nil {
|
||||
return buf, err
|
||||
}
|
||||
_, err = tw.Write(initrd)
|
||||
if err != nil {
|
||||
return buf, err
|
||||
}
|
||||
hdr = &tar.Header{
|
||||
Name: "cmdline",
|
||||
Mode: 0600,
|
||||
Size: int64(len(cmdline)),
|
||||
}
|
||||
err = tw.WriteHeader(hdr)
|
||||
if err != nil {
|
||||
return buf, err
|
||||
}
|
||||
_, err = tw.Write([]byte(cmdline))
|
||||
if err != nil {
|
||||
return buf, err
|
||||
}
|
||||
err = tw.Close()
|
||||
if err != nil {
|
||||
return buf, err
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func outputImg(image, filename string, kernel []byte, initrd []byte, cmdline string) error {
|
||||
log.Debugf("output img: %s %s", image, filename)
|
||||
log.Infof(" %s", filename)
|
||||
buf, err := tarInitrdKernel(kernel, initrd, cmdline)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
img, err := dockerRun(buf, image, cmdline)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(filename, img, os.FileMode(0644))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// this should replace the other version for types that can specify a size
|
||||
func outputImgSize(image, filename string, kernel []byte, initrd []byte, cmdline string, size int) error {
|
||||
log.Debugf("output img: %s %s size %d", image, filename, size)
|
||||
log.Infof(" %s", filename)
|
||||
buf, err := tarInitrdKernel(kernel, initrd, cmdline)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var img []byte
|
||||
if size == 0 {
|
||||
img, err = dockerRun(buf, image)
|
||||
} else {
|
||||
img, err = dockerRun(buf, image, fmt.Sprintf("%dM", size))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(filename, img, os.FileMode(0644))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputKernelInitrd(base string, kernel []byte, initrd []byte, cmdline string) error {
|
||||
log.Debugf("output kernel/initrd: %s %s", base, cmdline)
|
||||
log.Infof(" %s %s %s", base+"-kernel", base+"-initrd.img", base+"-cmdline")
|
||||
err := ioutil.WriteFile(base+"-initrd.img", initrd, os.FileMode(0644))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(base+"-kernel", kernel, os.FileMode(0644))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(base+"-cmdline", []byte(cmdline), os.FileMode(0644))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package main
|
||||
|
||||
var schema = string(`
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Moby Config",
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"kernel": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"image": { "type": "string"},
|
||||
"cmdline": { "type": "string"}
|
||||
}
|
||||
},
|
||||
"file": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"path": {"type": "string"},
|
||||
"directory": {"type": "boolean"},
|
||||
"symlink": {"type": "string"},
|
||||
"contents": {"type": "string"},
|
||||
"source": {"type": "string"},
|
||||
"optional": {"type": "boolean"},
|
||||
"mode": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/file" }
|
||||
},
|
||||
"trust": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"image": { "$ref": "#/definitions/strings" },
|
||||
"org": { "$ref": "#/definitions/strings" }
|
||||
}
|
||||
},
|
||||
"strings": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
},
|
||||
"mount": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"destination": { "type": "string" },
|
||||
"type": { "type": "string" },
|
||||
"source": { "type": "string" },
|
||||
"options": { "$ref": "#/definitions/strings" }
|
||||
}
|
||||
},
|
||||
"mounts": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/mount" }
|
||||
},
|
||||
"image": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "image"],
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"image": {"type": "string"},
|
||||
"capabilities": { "$ref": "#/definitions/strings" },
|
||||
"mounts": { "$ref": "#/definitions/mounts" },
|
||||
"binds": { "$ref": "#/definitions/strings" },
|
||||
"tmpfs": { "$ref": "#/definitions/strings" },
|
||||
"command": { "$ref": "#/definitions/strings" },
|
||||
"env": { "$ref": "#/definitions/strings" },
|
||||
"cwd": { "type": "string"},
|
||||
"net": { "type": "string"},
|
||||
"pid": { "type": "string"},
|
||||
"ipc": { "type": "string"},
|
||||
"uts": { "type": "string"},
|
||||
"readonly": { "type": "boolean"},
|
||||
"maskedPaths": { "$ref": "#/definitions/strings" },
|
||||
"readonlyPaths": { "$ref": "#/definitions/strings" },
|
||||
"uid": {"type": "integer"},
|
||||
"gid": {"type": "integer"},
|
||||
"additionalGids": {
|
||||
"type": "array",
|
||||
"items": { "type": "integer" }
|
||||
},
|
||||
"noNewPrivileges": {"type": "boolean"},
|
||||
"hostname": {"type": "string"},
|
||||
"oomScoreAdj": {"type": "integer"},
|
||||
"disableOOMKiller": {"type": "boolean"},
|
||||
"rootfsPropagation": {"type": "string"},
|
||||
"cgroupsPath": {"type": "string"},
|
||||
"sysctl": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/strings" }
|
||||
},
|
||||
"rlimits": { "$ref": "#/definitions/strings" }
|
||||
}
|
||||
},
|
||||
"images": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/image" }
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"kernel": { "$ref": "#/definitions/kernel" },
|
||||
"init": { "$ref": "#/definitions/strings" },
|
||||
"onboot": { "$ref": "#/definitions/images" },
|
||||
"services": { "$ref": "#/definitions/images" },
|
||||
"trust": { "$ref": "#/definitions/trust" },
|
||||
"files": { "$ref": "#/definitions/files" }
|
||||
}
|
||||
}
|
||||
`)
|
||||
@@ -1,208 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/distribution/registry/client/auth/challenge"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/cli/trust"
|
||||
notaryClient "github.com/docker/notary/client"
|
||||
"github.com/docker/notary/trustpinning"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
// TrustedReference parses an image string, and does a notary lookup to verify and retrieve the signed digest reference
|
||||
func TrustedReference(image string) (reference.Reference, error) {
|
||||
ref, err := reference.ParseAnyReference(image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// to mimic docker pull: if we have a digest already, it's implicitly trusted
|
||||
if digestRef, ok := ref.(reference.Digested); ok {
|
||||
return digestRef, nil
|
||||
}
|
||||
// to mimic docker pull: if we have a digest already, it's implicitly trusted
|
||||
if canonicalRef, ok := ref.(reference.Canonical); ok {
|
||||
return canonicalRef, nil
|
||||
}
|
||||
|
||||
namedRef, ok := ref.(reference.Named)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to resolve image digest using content trust: reference is not named")
|
||||
}
|
||||
taggedRef, ok := namedRef.(reference.NamedTagged)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to resolve image digest using content trust: reference is not tagged")
|
||||
}
|
||||
|
||||
gun := taggedRef.Name()
|
||||
targetName := taggedRef.Tag()
|
||||
server, err := getTrustServer(gun)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rt, err := GetReadOnlyAuthTransport(server, []string{gun}, "", "", "")
|
||||
if err != nil {
|
||||
log.Debugf("failed to reach %s notary server for repo: %s, falling back to cache: %v", server, gun, err)
|
||||
rt = nil
|
||||
}
|
||||
|
||||
nRepo, err := notaryClient.NewNotaryRepository(
|
||||
trustDirectory(),
|
||||
gun,
|
||||
server,
|
||||
rt,
|
||||
nil,
|
||||
trustpinning.TrustPinConfig{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target, err := nRepo.GetTargetByName(targetName, trust.ReleasesRole, data.CanonicalTargetsRole)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Only get the tag if it's in the top level targets role or the releases delegation role
|
||||
// ignore it if it's in any other delegation roles
|
||||
if target.Role != trust.ReleasesRole && target.Role != data.CanonicalTargetsRole {
|
||||
return nil, errors.New("not signed in valid role")
|
||||
}
|
||||
|
||||
h, ok := target.Hashes["sha256"]
|
||||
if !ok {
|
||||
return nil, errors.New("no valid hash, expecting sha256")
|
||||
}
|
||||
|
||||
dgst := digest.NewDigestFromHex("sha256", hex.EncodeToString(h))
|
||||
|
||||
// Allow returning canonical reference with tag and digest
|
||||
return reference.WithDigest(taggedRef, dgst)
|
||||
}
|
||||
|
||||
func getTrustServer(gun string) (string, error) {
|
||||
if strings.HasPrefix(gun, "docker.io/") {
|
||||
return "https://notary.docker.io", nil
|
||||
}
|
||||
return "", errors.New("non-hub images not yet supported")
|
||||
}
|
||||
|
||||
func trustDirectory() string {
|
||||
return filepath.Join(MobyDir, "trust")
|
||||
}
|
||||
|
||||
type credentialStore struct {
|
||||
username string
|
||||
password string
|
||||
refreshTokens map[string]string
|
||||
}
|
||||
|
||||
func (tcs *credentialStore) Basic(url *url.URL) (string, string) {
|
||||
return tcs.username, tcs.password
|
||||
}
|
||||
|
||||
// refresh tokens are the long lived tokens that can be used instead of a password
|
||||
func (tcs *credentialStore) RefreshToken(u *url.URL, service string) string {
|
||||
return tcs.refreshTokens[service]
|
||||
}
|
||||
|
||||
func (tcs *credentialStore) SetRefreshToken(u *url.URL, service string, token string) {
|
||||
if tcs.refreshTokens != nil {
|
||||
tcs.refreshTokens[service] = token
|
||||
}
|
||||
}
|
||||
|
||||
// GetReadOnlyAuthTransport gets the Auth Transport used to communicate with notary
|
||||
func GetReadOnlyAuthTransport(server string, scopes []string, username, password, rootCAPath string) (http.RoundTripper, error) {
|
||||
httpsTransport, err := httpsTransport(rootCAPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/v2/", server), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pingClient := &http.Client{
|
||||
Transport: httpsTransport,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
resp, err := pingClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
challengeManager := challenge.NewSimpleManager()
|
||||
if err := challengeManager.AddResponse(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
creds := credentialStore{
|
||||
username: username,
|
||||
password: password,
|
||||
refreshTokens: make(map[string]string),
|
||||
}
|
||||
|
||||
var scopeObjs []auth.Scope
|
||||
for _, scopeName := range scopes {
|
||||
scopeObjs = append(scopeObjs, auth.RepositoryScope{
|
||||
Repository: scopeName,
|
||||
Actions: []string{"pull"},
|
||||
})
|
||||
}
|
||||
|
||||
// allow setting multiple scopes so we don't have to reauth
|
||||
tokenHandler := auth.NewTokenHandlerWithOptions(auth.TokenHandlerOptions{
|
||||
Transport: httpsTransport,
|
||||
Credentials: &creds,
|
||||
Scopes: scopeObjs,
|
||||
})
|
||||
|
||||
authedTransport := transport.NewTransport(httpsTransport, auth.NewAuthorizer(challengeManager, tokenHandler))
|
||||
return authedTransport, nil
|
||||
}
|
||||
|
||||
func httpsTransport(caFile string) (*http.Transport, error) {
|
||||
tlsConfig := &tls.Config{}
|
||||
transport := http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
// Override with the system cert pool if the caFile was empty
|
||||
if caFile == "" {
|
||||
systemCertPool, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transport.TLSClientConfig.RootCAs = systemCertPool
|
||||
} else {
|
||||
certPool := x509.NewCertPool()
|
||||
pems, err := ioutil.ReadFile(caFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certPool.AppendCertsFromPEM(pems)
|
||||
transport.TLSClientConfig.RootCAs = certPool
|
||||
}
|
||||
return &transport, nil
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEnforceContentTrust(t *testing.T) {
|
||||
type enforceContentTrustCase struct {
|
||||
result bool
|
||||
imageName string
|
||||
trustConfig *TrustConfig
|
||||
}
|
||||
testCases := []enforceContentTrustCase{
|
||||
// Simple positive and negative cases for Image subkey
|
||||
{true, "image", &TrustConfig{Image: []string{"image"}}},
|
||||
{true, "image", &TrustConfig{Image: []string{"more", "than", "one", "image"}}},
|
||||
{true, "image", &TrustConfig{Image: []string{"more", "than", "one", "image"}, Org: []string{"random", "orgs"}}},
|
||||
{false, "image", &TrustConfig{}},
|
||||
{false, "image", &TrustConfig{Image: []string{"not", "in", "here!"}}},
|
||||
{false, "image", &TrustConfig{Image: []string{"not", "in", "here!"}, Org: []string{""}}},
|
||||
|
||||
// Tests for Image subkey with tags
|
||||
{true, "image:tag", &TrustConfig{Image: []string{"image:tag"}}},
|
||||
{true, "image:tag", &TrustConfig{Image: []string{"image"}}},
|
||||
{false, "image:tag", &TrustConfig{Image: []string{"image:otherTag"}}},
|
||||
{false, "image:tag", &TrustConfig{Image: []string{"image@sha256:abc123"}}},
|
||||
|
||||
// Tests for Image subkey with digests
|
||||
{true, "image@sha256:abc123", &TrustConfig{Image: []string{"image@sha256:abc123"}}},
|
||||
{true, "image@sha256:abc123", &TrustConfig{Image: []string{"image"}}},
|
||||
{false, "image@sha256:abc123", &TrustConfig{Image: []string{"image:Tag"}}},
|
||||
{false, "image@sha256:abc123", &TrustConfig{Image: []string{"image@sha256:def456"}}},
|
||||
|
||||
// Tests for Image subkey with digests
|
||||
{true, "image@sha256:abc123", &TrustConfig{Image: []string{"image@sha256:abc123"}}},
|
||||
{true, "image@sha256:abc123", &TrustConfig{Image: []string{"image"}}},
|
||||
{false, "image@sha256:abc123", &TrustConfig{Image: []string{"image:Tag"}}},
|
||||
{false, "image@sha256:abc123", &TrustConfig{Image: []string{"image@sha256:def456"}}},
|
||||
|
||||
// Tests for Org subkey
|
||||
{true, "linuxkit/image", &TrustConfig{Image: []string{"notImage"}, Org: []string{"linuxkit"}}},
|
||||
{true, "linuxkit/differentImage", &TrustConfig{Image: []string{}, Org: []string{"linuxkit"}}},
|
||||
{true, "linuxkit/differentImage:tag", &TrustConfig{Image: []string{}, Org: []string{"linuxkit"}}},
|
||||
{true, "linuxkit/differentImage@sha256:abc123", &TrustConfig{Image: []string{}, Org: []string{"linuxkit"}}},
|
||||
{false, "linuxkit/differentImage", &TrustConfig{Image: []string{}, Org: []string{"notlinuxkit"}}},
|
||||
{false, "linuxkit/differentImage:tag", &TrustConfig{Image: []string{}, Org: []string{"notlinuxkit"}}},
|
||||
{false, "linuxkit/differentImage@sha256:abc123", &TrustConfig{Image: []string{}, Org: []string{"notlinuxkit"}}},
|
||||
|
||||
// Tests for Org with library organization
|
||||
{true, "nginx", &TrustConfig{Image: []string{}, Org: []string{"library"}}},
|
||||
{true, "nginx:alpine", &TrustConfig{Image: []string{}, Org: []string{"library"}}},
|
||||
{true, "library/nginx:alpine", &TrustConfig{Image: []string{}, Org: []string{"library"}}},
|
||||
{false, "nginx", &TrustConfig{Image: []string{}, Org: []string{"notLibrary"}}},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
if enforceContentTrust(testCase.imageName, testCase.trustConfig) != testCase.result {
|
||||
t.Errorf("incorrect trust enforcement result for %s against configuration %v, expected: %v", testCase.imageName, testCase.trustConfig, testCase.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user