Commit 61db139f authored by Yifan Gu's avatar Yifan Gu

Godep: Add godep for rkt.

rkt: v0.5.4 appc spec: v0.5.1
parent 082757cc
...@@ -49,30 +49,130 @@ ...@@ -49,30 +49,130 @@
"Rev": "c0ef4539dfab4d21c8ef20ba2924f9fc6f186d35" "Rev": "c0ef4539dfab4d21c8ef20ba2924f9fc6f186d35"
}, },
{ {
"ImportPath": "github.com/appc/spec/aci",
"Comment": "v0.5.1-55-g87808a3",
"Rev": "87808a37061a4a2e6204ccea5fd2fc930576db94"
},
{
"ImportPath": "github.com/appc/spec/pkg/acirenderer",
"Comment": "v0.5.1-55-g87808a3",
"Rev": "87808a37061a4a2e6204ccea5fd2fc930576db94"
},
{
"ImportPath": "github.com/appc/spec/pkg/tarheader",
"Comment": "v0.5.1-55-g87808a3",
"Rev": "87808a37061a4a2e6204ccea5fd2fc930576db94"
},
{
"ImportPath": "github.com/appc/spec/schema",
"Comment": "v0.5.1-55-g87808a3",
"Rev": "87808a37061a4a2e6204ccea5fd2fc930576db94"
},
{
"ImportPath": "github.com/beorn7/perks/quantile", "ImportPath": "github.com/beorn7/perks/quantile",
"Rev": "b965b613227fddccbfffe13eae360ed3fa822f8d" "Rev": "b965b613227fddccbfffe13eae360ed3fa822f8d"
}, },
{ {
"ImportPath": "github.com/camlistore/lock",
"Rev": "ae27720f340952636b826119b58130b9c1a847a0"
},
{
"ImportPath": "github.com/codegangsta/negroni", "ImportPath": "github.com/codegangsta/negroni",
"Comment": "v0.1-62-g8d75e11", "Comment": "v0.1-62-g8d75e11",
"Rev": "8d75e11374a1928608c906fe745b538483e7aeb2" "Rev": "8d75e11374a1928608c906fe745b538483e7aeb2"
}, },
{ {
"ImportPath": "github.com/coreos/etcd/etcdserver/etcdhttp/httptypes",
"Comment": "v2.0.4-288-g866a9d4",
"Rev": "866a9d4e41401657ea44bf539b2c5561d6fdcd67"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/types",
"Comment": "v2.0.4-288-g866a9d4",
"Rev": "866a9d4e41401657ea44bf539b2c5561d6fdcd67"
},
{
"ImportPath": "github.com/coreos/go-etcd/etcd", "ImportPath": "github.com/coreos/go-etcd/etcd",
"Comment": "v2.0.0-3-g0424b5f", "Comment": "v2.0.0-3-g0424b5f",
"Rev": "0424b5f86ef0ca57a5309c599f74bbb3e97ecd9d" "Rev": "0424b5f86ef0ca57a5309c599f74bbb3e97ecd9d"
}, },
{ {
"ImportPath": "github.com/coreos/go-semver/semver",
"Rev": "6fe83ccda8fb9b7549c9ab4ba47f47858bc950aa"
},
{
"ImportPath": "github.com/coreos/go-systemd/dbus", "ImportPath": "github.com/coreos/go-systemd/dbus",
"Comment": "v2-27-g97e243d", "Comment": "v2-27-g97e243d",
"Rev": "97e243d21a8e232e9d8af38ba2366dfcfceebeba" "Rev": "97e243d21a8e232e9d8af38ba2366dfcfceebeba"
}, },
{ {
"ImportPath": "github.com/coreos/go-systemd/unit",
"Comment": "v2-27-g97e243d",
"Rev": "97e243d21a8e232e9d8af38ba2366dfcfceebeba"
},
{
"ImportPath": "github.com/coreos/rkt/pkg/aci",
"Comment": "v0.5.4",
"Rev": "c8a7050a883653266137ae05f6e8f166db52eb67"
},
{
"ImportPath": "github.com/coreos/rkt/pkg/lock",
"Comment": "v0.5.4",
"Rev": "c8a7050a883653266137ae05f6e8f166db52eb67"
},
{
"ImportPath": "github.com/coreos/rkt/pkg/sys",
"Comment": "v0.5.4",
"Rev": "c8a7050a883653266137ae05f6e8f166db52eb67"
},
{
"ImportPath": "github.com/coreos/rkt/pkg/tar",
"Comment": "v0.5.4",
"Rev": "c8a7050a883653266137ae05f6e8f166db52eb67"
},
{
"ImportPath": "github.com/coreos/rkt/store",
"Comment": "v0.5.4",
"Rev": "c8a7050a883653266137ae05f6e8f166db52eb67"
},
{
"ImportPath": "github.com/cpuguy83/go-md2man/mangen", "ImportPath": "github.com/cpuguy83/go-md2man/mangen",
"Comment": "v1.0.2-5-g2831f11", "Comment": "v1.0.2-5-g2831f11",
"Rev": "2831f11f66ff4008f10e2cd7ed9a85e3d3fc2bed" "Rev": "2831f11f66ff4008f10e2cd7ed9a85e3d3fc2bed"
}, },
{ {
"ImportPath": "github.com/cznic/bufs",
"Rev": "3dcccbd7064a1689f9c093a988ea11ac00e21f51"
},
{
"ImportPath": "github.com/cznic/exp/lldb",
"Rev": "9b0e4be12fbdb7b843e0a658a04c35d160371789"
},
{
"ImportPath": "github.com/cznic/fileutil",
"Rev": "21ae57c9dce724a15e88bd9cd46d5668f3e880a5"
},
{
"ImportPath": "github.com/cznic/mathutil",
"Rev": "250d0b9d3304c5ea0c4cfc7d9efc7ee528b81f3b"
},
{
"ImportPath": "github.com/cznic/ql",
"Rev": "fc1b91b82089d3f132fbed8a7c9f349c3133eb96"
},
{
"ImportPath": "github.com/cznic/sortutil",
"Rev": "d4401851b4c370f979b842fa1e45e0b3b718b391"
},
{
"ImportPath": "github.com/cznic/strutil",
"Rev": "97bc31f80ac4c9fa9c5dc5fea74c383858988ea2"
},
{
"ImportPath": "github.com/cznic/zappy",
"Rev": "47331054e4f96186e3ff772877c0443909368a45"
},
{
"ImportPath": "github.com/davecgh/go-spew/spew", "ImportPath": "github.com/davecgh/go-spew/spew",
"Rev": "3e6e67c4dcea3ac2f25fd4731abc0e1deaf36216" "Rev": "3e6e67c4dcea3ac2f25fd4731abc0e1deaf36216"
}, },
...@@ -332,6 +432,14 @@ ...@@ -332,6 +432,14 @@
"Rev": "8adf9e1730c55cdc590de7d49766cb2acc88d8f2" "Rev": "8adf9e1730c55cdc590de7d49766cb2acc88d8f2"
}, },
{ {
"ImportPath": "github.com/petar/GoLLRB/llrb",
"Rev": "53be0d36a84c2a886ca057d34b6aa4468df9ccb4"
},
{
"ImportPath": "github.com/peterbourgon/diskv",
"Rev": "508f5671a72eeaef05cf8c24abe7fbc1c07faf69"
},
{
"ImportPath": "github.com/prometheus/client_golang/model", "ImportPath": "github.com/prometheus/client_golang/model",
"Comment": "0.4.0-1-g692492e", "Comment": "0.4.0-1-g692492e",
"Rev": "692492e54b553a81013254cc1fba4b6dd76fad30" "Rev": "692492e54b553a81013254cc1fba4b6dd76fad30"
...@@ -412,6 +520,14 @@ ...@@ -412,6 +520,14 @@
"Rev": "5292687f5379e01054407da44d7c4590a61fd3de" "Rev": "5292687f5379e01054407da44d7c4590a61fd3de"
}, },
{ {
"ImportPath": "golang.org/x/crypto/cast5",
"Rev": "a7ead6ddf06233883deca151dffaef2effbf498f"
},
{
"ImportPath": "golang.org/x/crypto/openpgp",
"Rev": "a7ead6ddf06233883deca151dffaef2effbf498f"
},
{
"ImportPath": "golang.org/x/net/context", "ImportPath": "golang.org/x/net/context",
"Rev": "cbcac7bb8415db9b6cb4d1ebab1dc9afbd688b97" "Rev": "cbcac7bb8415db9b6cb4d1ebab1dc9afbd688b97"
}, },
......
package aci
import (
"archive/tar"
"io"
"os"
"path/filepath"
"github.com/appc/spec/pkg/tarheader"
)
// BuildWalker creates a filepath.WalkFunc that walks over the given root
// (which should represent an ACI layout on disk) and adds the files in the
// rootfs/ subdirectory to the given ArchiveWriter
func BuildWalker(root string, aw ArchiveWriter) filepath.WalkFunc {
// cache of inode -> filepath, used to leverage hard links in the archive
inos := map[uint64]string{}
return func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relpath, err := filepath.Rel(root, path)
if err != nil {
return err
}
if relpath == "." {
return nil
}
if relpath == ManifestFile {
// ignore; this will be written by the archive writer
// TODO(jonboulle): does this make sense? maybe just remove from archivewriter?
return nil
}
link := ""
var r io.Reader
switch info.Mode() & os.ModeType {
case os.ModeSocket:
return nil
case os.ModeNamedPipe:
case os.ModeCharDevice:
case os.ModeDevice:
case os.ModeDir:
case os.ModeSymlink:
target, err := os.Readlink(path)
if err != nil {
return err
}
link = target
default:
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
r = file
}
hdr, err := tar.FileInfoHeader(info, link)
if err != nil {
panic(err)
}
// Because os.FileInfo's Name method returns only the base
// name of the file it describes, it may be necessary to
// modify the Name field of the returned header to provide the
// full path name of the file.
hdr.Name = relpath
tarheader.Populate(hdr, info, inos)
// If the file is a hard link to a file we've already seen, we
// don't need the contents
if hdr.Typeflag == tar.TypeLink {
hdr.Size = 0
r = nil
}
if err := aw.AddFile(hdr, r); err != nil {
return err
}
return nil
}
}
// Package aci contains various functions for working with App Container Images.
package aci
package aci
import (
"archive/tar"
"bytes"
"compress/bzip2"
"compress/gzip"
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os/exec"
"path/filepath"
"github.com/appc/spec/schema"
)
type FileType string
const (
TypeGzip = FileType("gz")
TypeBzip2 = FileType("bz2")
TypeXz = FileType("xz")
TypeTar = FileType("tar")
TypeText = FileType("text")
TypeUnknown = FileType("unknown")
readLen = 512 // max bytes to sniff
hexHdrGzip = "1f8b"
hexHdrBzip2 = "425a68"
hexHdrXz = "fd377a585a00"
hexSigTar = "7573746172"
tarOffset = 257
textMime = "text/plain; charset=utf-8"
)
var (
hdrGzip []byte
hdrBzip2 []byte
hdrXz []byte
sigTar []byte
tarEnd int
)
func mustDecodeHex(s string) []byte {
b, err := hex.DecodeString(s)
if err != nil {
panic(err)
}
return b
}
func init() {
hdrGzip = mustDecodeHex(hexHdrGzip)
hdrBzip2 = mustDecodeHex(hexHdrBzip2)
hdrXz = mustDecodeHex(hexHdrXz)
sigTar = mustDecodeHex(hexSigTar)
tarEnd = tarOffset + len(sigTar)
}
// DetectFileType attempts to detect the type of file that the given reader
// represents by comparing it against known file signatures (magic numbers)
func DetectFileType(r io.Reader) (FileType, error) {
var b bytes.Buffer
n, err := io.CopyN(&b, r, readLen)
if err != nil && err != io.EOF {
return TypeUnknown, err
}
bs := b.Bytes()
switch {
case bytes.HasPrefix(bs, hdrGzip):
return TypeGzip, nil
case bytes.HasPrefix(bs, hdrBzip2):
return TypeBzip2, nil
case bytes.HasPrefix(bs, hdrXz):
return TypeXz, nil
case n > int64(tarEnd) && bytes.Equal(bs[tarOffset:tarEnd], sigTar):
return TypeTar, nil
case http.DetectContentType(bs) == textMime:
return TypeText, nil
default:
return TypeUnknown, nil
}
}
// XzReader shells out to a command line xz executable (if
// available) to decompress the given io.Reader using the xz
// compression format
func XzReader(r io.Reader) io.ReadCloser {
rpipe, wpipe := io.Pipe()
ex, err := exec.LookPath("xz")
if err != nil {
log.Fatalf("couldn't find xz executable: %v", err)
}
cmd := exec.Command(ex, "--decompress", "--stdout")
cmd.Stdin = r
cmd.Stdout = wpipe
go func() {
err := cmd.Run()
wpipe.CloseWithError(err)
}()
return rpipe
}
// ManifestFromImage extracts a new schema.ImageManifest from the given ACI image.
func ManifestFromImage(rs io.ReadSeeker) (*schema.ImageManifest, error) {
var im schema.ImageManifest
tr, err := NewCompressedTarReader(rs)
if err != nil {
return nil, err
}
for {
hdr, err := tr.Next()
switch err {
case io.EOF:
return nil, errors.New("missing manifest")
case nil:
if filepath.Clean(hdr.Name) == ManifestFile {
data, err := ioutil.ReadAll(tr)
if err != nil {
return nil, err
}
if err := im.UnmarshalJSON(data); err != nil {
return nil, err
}
return &im, nil
}
default:
return nil, fmt.Errorf("error extracting tarball: %v", err)
}
}
}
// NewCompressedTarReader creates a new tar.Reader reading from the given ACI image.
func NewCompressedTarReader(rs io.ReadSeeker) (*tar.Reader, error) {
cr, err := NewCompressedReader(rs)
if err != nil {
return nil, err
}
return tar.NewReader(cr), nil
}
// NewCompressedReader creates a new io.Reader from the given ACI image.
func NewCompressedReader(rs io.ReadSeeker) (io.Reader, error) {
var (
dr io.Reader
err error
)
_, err = rs.Seek(0, 0)
if err != nil {
return nil, err
}
ftype, err := DetectFileType(rs)
if err != nil {
return nil, err
}
_, err = rs.Seek(0, 0)
if err != nil {
return nil, err
}
switch ftype {
case TypeGzip:
dr, err = gzip.NewReader(rs)
if err != nil {
return nil, err
}
case TypeBzip2:
dr = bzip2.NewReader(rs)
case TypeXz:
dr = XzReader(rs)
case TypeTar:
dr = rs
case TypeUnknown:
return nil, errors.New("error: unknown image filetype")
default:
return nil, errors.New("no type returned from DetectFileType?")
}
return dr, nil
}
package aci
import (
"archive/tar"
"compress/gzip"
"io/ioutil"
"os"
"testing"
)
func newTestACI(usedotslash bool) (*os.File, error) {
tf, err := ioutil.TempFile("", "")
if err != nil {
return nil, err
}
manifestBody := `{"acKind":"ImageManifest","acVersion":"0.5.1","name":"example.com/app"}`
gw := gzip.NewWriter(tf)
tw := tar.NewWriter(gw)
manifestPath := "manifest"
if usedotslash {
manifestPath = "./" + manifestPath
}
hdr := &tar.Header{
Name: manifestPath,
Size: int64(len(manifestBody)),
}
if err := tw.WriteHeader(hdr); err != nil {
return nil, err
}
if _, err := tw.Write([]byte(manifestBody)); err != nil {
return nil, err
}
if err := tw.Close(); err != nil {
return nil, err
}
if err := gw.Close(); err != nil {
return nil, err
}
return tf, nil
}
func newEmptyTestACI() (*os.File, error) {
tf, err := ioutil.TempFile("", "")
if err != nil {
return nil, err
}
gw := gzip.NewWriter(tf)
tw := tar.NewWriter(gw)
if err := tw.Close(); err != nil {
return nil, err
}
if err := gw.Close(); err != nil {
return nil, err
}
return tf, nil
}
func TestManifestFromImage(t *testing.T) {
for _, usedotslash := range []bool{false, true} {
img, err := newTestACI(usedotslash)
if err != nil {
t.Fatalf("newTestACI: unexpected error: %v", err)
}
defer img.Close()
defer os.Remove(img.Name())
im, err := ManifestFromImage(img)
if err != nil {
t.Fatalf("ManifestFromImage: unexpected error: %v", err)
}
if im.Name.String() != "example.com/app" {
t.Errorf("expected %s, got %s", "example.com/app", im.Name.String())
}
emptyImg, err := newEmptyTestACI()
if err != nil {
t.Fatalf("newEmptyTestACI: unexpected error: %v", err)
}
defer emptyImg.Close()
defer os.Remove(emptyImg.Name())
im, err = ManifestFromImage(emptyImg)
if err == nil {
t.Fatalf("ManifestFromImage: expected error")
}
}
}
func TestNewCompressedTarReader(t *testing.T) {
img, err := newTestACI(false)
if err != nil {
t.Fatalf("newTestACI: unexpected error: %v", err)
}
defer img.Close()
defer os.Remove(img.Name())
cr, err := NewCompressedTarReader(img)
if err != nil {
t.Fatalf("NewCompressedTarReader: unexpected error: %v", err)
}
ftype, err := DetectFileType(cr)
if err != nil {
t.Fatalf("DetectFileType: unexpected error: %v", err)
}
if ftype != TypeText {
t.Errorf("expected %v, got %v", TypeText, ftype)
}
}
func TestNewCompressedReader(t *testing.T) {
img, err := newTestACI(false)
if err != nil {
t.Fatalf("newTestACI: unexpected error: %v", err)
}
defer img.Close()
defer os.Remove(img.Name())
cr, err := NewCompressedReader(img)
if err != nil {
t.Fatalf("NewCompressedReader: unexpected error: %v", err)
}
ftype, err := DetectFileType(cr)
if err != nil {
t.Fatalf("DetectFileType: unexpected error: %v", err)
}
if ftype != TypeTar {
t.Errorf("expected %v, got %v", TypeTar, ftype)
}
}
package aci
/*
Image Layout
The on-disk layout of an app container is straightforward.
It includes a rootfs with all of the files that will exist in the root of the app and a manifest describing the image.
The layout MUST contain an image manifest.
/manifest
/rootfs/
/rootfs/usr/bin/mysql
*/
import (
"archive/tar"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/appc/spec/schema"
)
const (
// Path to manifest file inside the layout
ManifestFile = "manifest"
// Path to rootfs directory inside the layout
RootfsDir = "rootfs"
)
var (
ErrNoRootFS = errors.New("no rootfs found in layout")
ErrNoManifest = errors.New("no image manifest found in layout")
)
// ValidateLayout takes a directory and validates that the layout of the directory
// matches that expected by the Application Container Image format.
// If any errors are encountered during the validation, it will abort and
// return the first one.
func ValidateLayout(dir string) error {
fi, err := os.Stat(dir)
if err != nil {
return fmt.Errorf("error accessing layout: %v", err)
}
if !fi.IsDir() {
return fmt.Errorf("given path %q is not a directory", dir)
}
var flist []string
var imOK, rfsOK bool
var im io.Reader
walkLayout := func(fpath string, fi os.FileInfo, err error) error {
rpath, err := filepath.Rel(dir, fpath)
if err != nil {
return err
}
switch rpath {
case ".":
case ManifestFile:
im, err = os.Open(fpath)
if err != nil {
return err
}
imOK = true
case RootfsDir:
if !fi.IsDir() {
return errors.New("rootfs is not a directory")
}
rfsOK = true
default:
flist = append(flist, rpath)
}
return nil
}
if err := filepath.Walk(dir, walkLayout); err != nil {
return err
}
return validate(imOK, im, rfsOK, flist)
}
// ValidateArchive takes a *tar.Reader and validates that the layout of the
// filesystem the reader encapsulates matches that expected by the
// Application Container Image format. If any errors are encountered during
// the validation, it will abort and return the first one.
func ValidateArchive(tr *tar.Reader) error {
var fseen map[string]bool = make(map[string]bool)
var imOK, rfsOK bool
var im bytes.Buffer
Tar:
for {
hdr, err := tr.Next()
switch {
case err == nil:
case err == io.EOF:
break Tar
default:
return err
}
name := filepath.Clean(hdr.Name)
switch name {
case ".":
case ManifestFile:
_, err := io.Copy(&im, tr)
if err != nil {
return err
}
imOK = true
case RootfsDir:
if !hdr.FileInfo().IsDir() {
return fmt.Errorf("rootfs is not a directory")
}
rfsOK = true
default:
if _, seen := fseen[name]; seen {
return fmt.Errorf("duplicate file entry in archive: %s", name)
}
fseen[name] = true
}
}
var flist []string
for key := range fseen {
flist = append(flist, key)
}
return validate(imOK, &im, rfsOK, flist)
}
func validate(imOK bool, im io.Reader, rfsOK bool, files []string) error {
defer func() {
if rc, ok := im.(io.Closer); ok {
rc.Close()
}
}()
if !imOK {
return ErrNoManifest
}
if !rfsOK {
return ErrNoRootFS
}
b, err := ioutil.ReadAll(im)
if err != nil {
return fmt.Errorf("error reading image manifest: %v", err)
}
var a schema.ImageManifest
if err := a.UnmarshalJSON(b); err != nil {
return fmt.Errorf("image manifest validation failed: %v", err)
}
for _, f := range files {
if !strings.HasPrefix(f, "rootfs") {
return fmt.Errorf("unrecognized file path in layout: %q", f)
}
}
return nil
}
package aci
import (
"io/ioutil"
"os"
"path"
"testing"
)
func newValidateLayoutTest() (string, error) {
td, err := ioutil.TempDir("", "")
if err != nil {
return "", err
}
if err := os.MkdirAll(path.Join(td, "rootfs"), 0755); err != nil {
return "", err
}
if err := os.MkdirAll(path.Join(td, "rootfs", "dir", "rootfs"), 0755); err != nil {
return "", err
}
evilManifestBody := "malformedManifest"
manifestBody := `{"acKind":"ImageManifest","acVersion":"0.3.0","name":"example.com/app"}`
evilManifestPath := "rootfs/manifest"
evilManifestPath = path.Join(td, evilManifestPath)
em, err := os.Create(evilManifestPath)
if err != nil {
return "", err
}
em.WriteString(evilManifestBody)
em.Close()
manifestPath := path.Join(td, "manifest")
m, err := os.Create(manifestPath)
if err != nil {
return "", err
}
m.WriteString(manifestBody)
m.Close()
return td, nil
}
func TestValidateLayout(t *testing.T) {
layoutPath, err := newValidateLayoutTest()
if err != nil {
t.Fatalf("newValidateLayoutTest: unexpected error: %v", err)
}
defer os.RemoveAll(layoutPath)
err = ValidateLayout(layoutPath)
if err != nil {
t.Fatalf("ValidateLayout: unexpected error: %v", err)
}
}
package aci
import (
"archive/tar"
"bytes"
"encoding/json"
"io"
"time"
"github.com/appc/spec/schema"
)
// ArchiveWriter writes App Container Images. Users wanting to create an ACI or
// should create an ArchiveWriter and add files to it; the ACI will be written
// to the underlying tar.Writer
type ArchiveWriter interface {
AddFile(hdr *tar.Header, r io.Reader) error
Close() error
}
type imageArchiveWriter struct {
*tar.Writer
am *schema.ImageManifest
}
// NewImageWriter creates a new ArchiveWriter which will generate an App
// Container Image based on the given manifest and write it to the given
// tar.Writer
func NewImageWriter(am schema.ImageManifest, w *tar.Writer) ArchiveWriter {
aw := &imageArchiveWriter{
w,
&am,
}
return aw
}
func (aw *imageArchiveWriter) AddFile(hdr *tar.Header, r io.Reader) error {
err := aw.Writer.WriteHeader(hdr)
if err != nil {
return err
}
if r != nil {
_, err := io.Copy(aw.Writer, r)
if err != nil {
return err
}
}
return nil
}
func (aw *imageArchiveWriter) addFileNow(path string, contents []byte) error {
buf := bytes.NewBuffer(contents)
now := time.Now()
hdr := tar.Header{
Name: path,
Mode: 0644,
Uid: 0,
Gid: 0,
Size: int64(buf.Len()),
ModTime: now,
Typeflag: tar.TypeReg,
Uname: "root",
Gname: "root",
ChangeTime: now,
}
return aw.AddFile(&hdr, buf)
}
func (aw *imageArchiveWriter) addManifest(name string, m json.Marshaler) error {
out, err := m.MarshalJSON()
if err != nil {
return err
}
return aw.addFileNow(name, out)
}
func (aw *imageArchiveWriter) Close() error {
if err := aw.addManifest(ManifestFile, aw.am); err != nil {
return err
}
return aw.Writer.Close()
}
package acirenderer
import (
"archive/tar"
"crypto/sha512"
"fmt"
"hash"
"io"
"io/ioutil"
"path/filepath"
"strings"
"github.com/appc/spec/schema"
"github.com/appc/spec/schema/types"
)
// An ACIRegistry provides all functions of an ACIProvider plus functions to
// search for an aci and get its contents
type ACIRegistry interface {
ACIProvider
GetImageManifest(key string) (*schema.ImageManifest, error)
GetACI(name types.ACName, labels types.Labels) (string, error)
}
// An ACIProvider provides functions to get an ACI contents, to convert an
// ACI hash to the key under which the ACI is known to the provider and to resolve an
// ImageID to the key under which it's known to the provider.
type ACIProvider interface {
// Read the ACI contents stream given the key. Use ResolveKey to
// convert an ImageID to the relative provider's key.
ReadStream(key string) (io.ReadCloser, error)
// Converts an ImageID to the, if existent, key under which the
// ACI is known to the provider
ResolveKey(key string) (string, error)
// Converts a Hash to the provider's key
HashToKey(h hash.Hash) string
}
// An Image contains the ImageManifest, the ACIProvider's key and its Level in
// the dependency tree.
type Image struct {
Im *schema.ImageManifest
Key string
Level uint16
}
// Images encapsulates an ordered slice of Image structs. It represents a flat
// dependency tree.
// The upper Image should be the first in the slice with a level of 0.
// For example if A is the upper image and has two deps (in order B and C). And C has one dep (D),
// the slice (reporting the app name and excluding im and Hash) should be:
// [{A, Level: 0}, {C, Level:1}, {D, Level: 2}, {B, Level: 1}]
type Images []Image
// ACIFiles represents which files to extract for every ACI
type ACIFiles struct {
Key string
FileMap map[string]struct{}
}
// RenderedACI is an (ordered) slice of ACIFiles
type RenderedACI []*ACIFiles
// GetRenderedACIWithImageID, given an imageID, starts with the matching image
// available in the store, creates the dependencies list and returns the
// RenderedACI list.
func GetRenderedACIWithImageID(imageID types.Hash, ap ACIRegistry) (RenderedACI, error) {
imgs, err := CreateDepListFromImageID(imageID, ap)
if err != nil {
return nil, err
}
return GetRenderedACIFromList(imgs, ap)
}
// GetRenderedACI, given an image app name and optional labels, starts with the
// best matching image available in the store, creates the dependencies list
// and returns the RenderedACI list.
func GetRenderedACI(name types.ACName, labels types.Labels, ap ACIRegistry) (RenderedACI, error) {
imgs, err := CreateDepListFromNameLabels(name, labels, ap)
if err != nil {
return nil, err
}
return GetRenderedACIFromList(imgs, ap)
}
// GetRenderedACIFromList returns the RenderedACI list. All file outside rootfs
// are excluded (at the moment only "manifest").
func GetRenderedACIFromList(imgs Images, ap ACIProvider) (RenderedACI, error) {
if len(imgs) == 0 {
return nil, fmt.Errorf("image list empty")
}
allFiles := make(map[string]struct{})
renderedACI := RenderedACI{}
first := true
for i, img := range imgs {
pwlm := getUpperPWLM(imgs, i)
ra, err := getACIFiles(img, ap, allFiles, pwlm)
if err != nil {
return nil, err
}
// Use the manifest from the upper ACI
if first {
ra.FileMap["manifest"] = struct{}{}
first = false
}
renderedACI = append(renderedACI, ra)
}
return renderedACI, nil
}
// getUpperPWLM returns the pwl at the lower level for the branch where
// img[pos] lives.
func getUpperPWLM(imgs Images, pos int) map[string]struct{} {
var pwlm map[string]struct{}
curlevel := imgs[pos].Level
// Start from our position and go back ignoring the other leafs.
for i := pos; i >= 0; i-- {
img := imgs[i]
if img.Level < curlevel && len(img.Im.PathWhitelist) > 0 {
pwlm = pwlToMap(img.Im.PathWhitelist)
}
curlevel = img.Level
}
return pwlm
}
// getACIFiles returns the ACIFiles struct for the given image. All files
// outside rootfs are excluded (at the moment only "manifest").
func getACIFiles(img Image, ap ACIProvider, allFiles map[string]struct{}, pwlm map[string]struct{}) (*ACIFiles, error) {
rs, err := ap.ReadStream(img.Key)
if err != nil {
return nil, err
}
defer rs.Close()
hash := sha512.New()
r := io.TeeReader(rs, hash)
thispwlm := pwlToMap(img.Im.PathWhitelist)
ra := &ACIFiles{FileMap: make(map[string]struct{})}
if err = Walk(tar.NewReader(r), func(hdr *tar.Header) error {
name := hdr.Name
cleanName := filepath.Clean(name)
// Ignore files outside /rootfs/ (at the moment only "manifest")
if !strings.HasPrefix(cleanName, "rootfs/") {
return nil
}
// Is the file in our PathWhiteList?
// If the file is a directory continue also if not in PathWhiteList
if hdr.Typeflag != tar.TypeDir {
if len(img.Im.PathWhitelist) > 0 {
if _, ok := thispwlm[cleanName]; !ok {
return nil
}
}
}
// Is the file in the lower level PathWhiteList of this img branch?
if pwlm != nil {
if _, ok := pwlm[cleanName]; !ok {
return nil
}
}
// Is the file already provided by a previous image?
if _, ok := allFiles[cleanName]; ok {
return nil
}
ra.FileMap[cleanName] = struct{}{}
allFiles[cleanName] = struct{}{}
return nil
}); err != nil {
return nil, err
}
// Tar does not necessarily read the complete file, so ensure we read the entirety into the hash
if _, err := io.Copy(ioutil.Discard, r); err != nil {
return nil, fmt.Errorf("error reading ACI: %v", err)
}
if g := ap.HashToKey(hash); g != img.Key {
return nil, fmt.Errorf("image hash does not match expected (%s != %s)", g, img.Key)
}
ra.Key = img.Key
return ra, nil
}
// pwlToMap converts a pathWhiteList slice to a map for faster search
// It will also prepend "rootfs/" to the provided paths and they will be
// relative to "/" so they can be easily compared with the tar.Header.Name
// If pwl length is 0, a nil map is returned
func pwlToMap(pwl []string) map[string]struct{} {
if len(pwl) == 0 {
return nil
}
m := make(map[string]struct{}, len(pwl))
for _, name := range pwl {
relpath := filepath.Join("rootfs", name)
m[relpath] = struct{}{}
}
return m
}
func Walk(tarReader *tar.Reader, walkFunc func(hdr *tar.Header) error) error {
for {
hdr, err := tarReader.Next()
if err == io.EOF {
// end of tar archive
break
}
if err != nil {
return fmt.Errorf("Error reading tar entry: %v", err)
}
if err := walkFunc(hdr); err != nil {
return err
}
}
return nil
}
package acirenderer
import (
"container/list"
"github.com/appc/spec/schema/types"
)
// CreateDepListFromImageID returns the flat dependency tree of the image with
// the provided imageID
func CreateDepListFromImageID(imageID types.Hash, ap ACIRegistry) (Images, error) {
key, err := ap.ResolveKey(imageID.String())
if err != nil {
return nil, err
}
return createDepList(key, ap)
}
// CreateDepListFromNameLabels returns the flat dependency tree of the image
// with the provided app name and optional labels.
func CreateDepListFromNameLabels(name types.ACName, labels types.Labels, ap ACIRegistry) (Images, error) {
key, err := ap.GetACI(name, labels)
if err != nil {
return nil, err
}
return createDepList(key, ap)
}
// createDepList returns the flat dependency tree as a list of Image type
func createDepList(key string, ap ACIRegistry) (Images, error) {
imgsl := list.New()
im, err := ap.GetImageManifest(key)
if err != nil {
return nil, err
}
img := Image{Im: im, Key: key, Level: 0}
imgsl.PushFront(img)
// Create a flat dependency tree. Use a LinkedList to be able to
// insert elements in the list while working on it.
for el := imgsl.Front(); el != nil; el = el.Next() {
img := el.Value.(Image)
dependencies := img.Im.Dependencies
for _, d := range dependencies {
var depimg Image
var depKey string
if d.ImageID != nil && !d.ImageID.Empty() {
depKey, err = ap.ResolveKey(d.ImageID.String())
if err != nil {
return nil, err
}
} else {
var err error
depKey, err = ap.GetACI(d.App, d.Labels)
if err != nil {
return nil, err
}
}
im, err := ap.GetImageManifest(depKey)
if err != nil {
return nil, err
}
depimg = Image{Im: im, Key: depKey, Level: img.Level + 1}
imgsl.InsertAfter(depimg, el)
}
}
imgs := Images{}
for el := imgsl.Front(); el != nil; el = el.Next() {
imgs = append(imgs, el.Value.(Image))
}
return imgs, nil
}
package acirenderer
import (
"bytes"
"fmt"
"hash"
"io"
"io/ioutil"
"os"
"github.com/appc/spec/aci"
"github.com/appc/spec/schema"
"github.com/appc/spec/schema/types"
)
const (
hashPrefix = "sha512-"
)
type TestStoreAci struct {
data []byte
key string
ImageManifest *schema.ImageManifest
}
type TestStore struct {
acis map[string]*TestStoreAci
}
func NewTestStore() *TestStore {
return &TestStore{acis: make(map[string]*TestStoreAci)}
}
func (ts *TestStore) WriteACI(path string) (string, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
imageID := types.NewHashSHA512(data)
rs, err := os.Open(path)
if err != nil {
return "", err
}
defer rs.Close()
im, err := aci.ManifestFromImage(rs)
if err != nil {
return "", fmt.Errorf("error retrieving ImageManifest: %v", err)
}
key := imageID.String()
ts.acis[key] = &TestStoreAci{data: data, key: key, ImageManifest: im}
return key, nil
}
func (ts *TestStore) GetImageManifest(key string) (*schema.ImageManifest, error) {
aci, ok := ts.acis[key]
if !ok {
return nil, fmt.Errorf("aci with key: %s not found", key)
}
return aci.ImageManifest, nil
}
func (ts *TestStore) GetACI(name types.ACName, labels types.Labels) (string, error) {
for _, aci := range ts.acis {
if aci.ImageManifest.Name.String() == name.String() {
return aci.key, nil
}
}
return "", fmt.Errorf("aci not found")
}
func (ts *TestStore) ReadStream(key string) (io.ReadCloser, error) {
aci, ok := ts.acis[key]
if !ok {
return nil, fmt.Errorf("stream for key: %s not found", key)
}
return ioutil.NopCloser(bytes.NewReader(aci.data)), nil
}
func (ts *TestStore) ResolveKey(key string) (string, error) {
return key, nil
}
// HashToKey takes a hash.Hash (which currently _MUST_ represent a full SHA512),
// calculates its sum, and returns a string which should be used as the key to
// store the data matching the hash.
func (ts *TestStore) HashToKey(h hash.Hash) string {
s := h.Sum(nil)
return fmt.Sprintf("%s%x", hashPrefix, s)
}
// Package tarheader contains a simple abstraction to accurately create
// tar.Headers on different operating systems.
package tarheader
//+build darwin
package tarheader
import (
"archive/tar"
"os"
"syscall"
"time"
)
func init() {
populateHeaderStat = append(populateHeaderStat, populateHeaderCtime)
}
func populateHeaderCtime(h *tar.Header, fi os.FileInfo, _ map[uint64]string) {
st, ok := fi.Sys().(*syscall.Stat_t)
if !ok {
return
}
sec, nsec := st.Ctimespec.Unix()
ctime := time.Unix(sec, nsec)
h.ChangeTime = ctime
}
package tarheader
import (
"archive/tar"
"os"
"syscall"
"time"
)
func init() {
populateHeaderStat = append(populateHeaderStat, populateHeaderCtime)
}
func populateHeaderCtime(h *tar.Header, fi os.FileInfo, _ map[uint64]string) {
st, ok := fi.Sys().(*syscall.Stat_t)
if !ok {
return
}
sec, nsec := st.Ctim.Unix()
ctime := time.Unix(sec, nsec)
h.ChangeTime = ctime
}
package tarheader
/*
#define _BSD_SOURCE
#define _DEFAULT_SOURCE
#include <sys/types.h>
unsigned int
my_major(dev_t dev)
{
return major(dev);
}
unsigned int
my_minor(dev_t dev)
{
return minor(dev);
}
*/
import "C"
import (
"archive/tar"
"os"
"syscall"
)
func init() {
populateHeaderStat = append(populateHeaderStat, populateHeaderUnix)
}
func populateHeaderUnix(h *tar.Header, fi os.FileInfo, seen map[uint64]string) {
st, ok := fi.Sys().(*syscall.Stat_t)
if !ok {
return
}
h.Uid = int(st.Uid)
h.Gid = int(st.Gid)
if st.Mode&syscall.S_IFMT == syscall.S_IFBLK || st.Mode&syscall.S_IFMT == syscall.S_IFCHR {
h.Devminor = int64(C.my_minor(C.dev_t(st.Rdev)))
h.Devmajor = int64(C.my_major(C.dev_t(st.Rdev)))
}
// If we have already seen this inode, generate a hardlink
p, ok := seen[uint64(st.Ino)]
if ok {
h.Linkname = p
h.Typeflag = tar.TypeLink
} else {
seen[uint64(st.Ino)] = h.Name
}
}
package tarheader
import (
"archive/tar"
"io/ioutil"
"os"
"path/filepath"
"syscall"
"testing"
)
// mknod requires privilege ...
func TestHeaderUnixDev(t *testing.T) {
hExpect := tar.Header{
Name: "./dev/test0",
Size: 0,
Typeflag: tar.TypeBlock,
Devminor: 5,
Devmajor: 233,
}
// make our test block device
var path string
{
var err error
path, err = ioutil.TempDir("", "tarheader-test-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(path)
if err := os.Mkdir(filepath.Join(path, "dev"), os.FileMode(0755)); err != nil {
t.Fatal(err)
}
mode := uint32(hExpect.Mode&07777) | syscall.S_IFBLK
dev := uint32(((hExpect.Devminor & 0xfff00) << 12) | ((hExpect.Devmajor & 0xfff) << 8) | (hExpect.Devminor & 0xff))
if err := syscall.Mknod(filepath.Join(path, hExpect.Name), mode, int(dev)); err != nil {
if err == syscall.EPERM {
t.Skip("no permission to CAP_MKNOD")
}
t.Fatal(err)
}
}
fi, err := os.Stat(filepath.Join(path, hExpect.Name))
if err != nil {
t.Fatal(err)
}
hGot := tar.Header{
Name: "./dev/test0",
Size: 0,
Typeflag: tar.TypeBlock,
}
seen := map[uint64]string{}
populateHeaderUnix(&hGot, fi, seen)
if hGot.Devminor != hExpect.Devminor {
t.Errorf("dev minor: got %d, expected %d", hGot.Devminor, hExpect.Devminor)
}
if hGot.Devmajor != hExpect.Devmajor {
t.Errorf("dev major: got %d, expected %d", hGot.Devmajor, hExpect.Devmajor)
}
}
package tarheader
import (
"archive/tar"
"os"
)
var populateHeaderStat []func(h *tar.Header, fi os.FileInfo, seen map[uint64]string)
func Populate(h *tar.Header, fi os.FileInfo, seen map[uint64]string) {
for _, pop := range populateHeaderStat {
pop(h, fi, seen)
}
}
// Package schema provides definitions for the JSON schema of the different
// manifests in the App Container Specification. The manifests are canonically
// represented in their respective structs:
// - `ImageManifest`
// - `PodManifest`
//
// Validation is performed through serialization: if a blob of JSON data will
// unmarshal to one of the *Manifests, it is considered a valid implementation
// of the standard. Similarly, if a constructed *Manifest struct marshals
// successfully to JSON, it must be valid.
package schema
package schema
import (
"encoding/json"
"errors"
"github.com/appc/spec/schema/types"
)
const (
ACIExtension = ".aci"
ImageManifestKind = types.ACKind("ImageManifest")
)
type ImageManifest struct {
ACKind types.ACKind `json:"acKind"`
ACVersion types.SemVer `json:"acVersion"`
Name types.ACName `json:"name"`
Labels types.Labels `json:"labels,omitempty"`
App *types.App `json:"app,omitempty"`
Annotations types.Annotations `json:"annotations,omitempty"`
Dependencies types.Dependencies `json:"dependencies,omitempty"`
PathWhitelist []string `json:"pathWhitelist,omitempty"`
}
// imageManifest is a model to facilitate extra validation during the
// unmarshalling of the ImageManifest
type imageManifest ImageManifest
func BlankImageManifest() *ImageManifest {
return &ImageManifest{ACKind: ImageManifestKind, ACVersion: AppContainerVersion}
}
func (im *ImageManifest) UnmarshalJSON(data []byte) error {
a := imageManifest(*im)
err := json.Unmarshal(data, &a)
if err != nil {
return err
}
nim := ImageManifest(a)
if err := nim.assertValid(); err != nil {
return err
}
*im = nim
return nil
}
func (im ImageManifest) MarshalJSON() ([]byte, error) {
if err := im.assertValid(); err != nil {
return nil, err
}
return json.Marshal(imageManifest(im))
}
var imKindError = types.InvalidACKindError(ImageManifestKind)
// assertValid performs extra assertions on an ImageManifest to ensure that
// fields are set appropriately, etc. It is used exclusively when marshalling
// and unmarshalling an ImageManifest. Most field-specific validation is
// performed through the individual types being marshalled; assertValid()
// should only deal with higher-level validation.
func (im *ImageManifest) assertValid() error {
if im.ACKind != ImageManifestKind {
return imKindError
}
if im.ACVersion.Empty() {
return errors.New(`acVersion must be set`)
}
if im.Name.Empty() {
return errors.New(`name must be set`)
}
return nil
}
func (im *ImageManifest) GetLabel(name string) (val string, ok bool) {
return im.Labels.Get(name)
}
func (im *ImageManifest) GetAnnotation(name string) (val string, ok bool) {
return im.Annotations.Get(name)
}
package schema
import "testing"
func TestEmptyApp(t *testing.T) {
imj := `
{
"acKind": "ImageManifest",
"acVersion": "0.5.1",
"name": "example.com/test"
}
`
var im ImageManifest
err := im.UnmarshalJSON([]byte(imj))
if err != nil {
t.Errorf("unexpected error: %v", err)
}
// Marshal and Unmarshal to verify that no "app": {} is generated on
// Marshal and converted to empty struct on Unmarshal
buf, err := im.MarshalJSON()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = im.UnmarshalJSON(buf)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func TestImageManifestMerge(t *testing.T) {
imj := `{"name": "example.com/test"}`
im := &ImageManifest{}
if im.UnmarshalJSON([]byte(imj)) == nil {
t.Fatal("Manifest JSON without acKind and acVersion unmarshalled successfully")
}
im = BlankImageManifest()
err := im.UnmarshalJSON([]byte(imj))
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
package schema
import (
"encoding/json"
"github.com/appc/spec/schema/types"
)
type Kind struct {
ACVersion types.SemVer `json:"acVersion"`
ACKind types.ACKind `json:"acKind"`
}
type kind Kind
func (k *Kind) UnmarshalJSON(data []byte) error {
nk := kind{}
err := json.Unmarshal(data, &nk)
if err != nil {
return err
}
*k = Kind(nk)
return nil
}
func (k Kind) MarshalJSON() ([]byte, error) {
return json.Marshal(kind(k))
}
package schema
import (
"encoding/json"
"errors"
"fmt"
"github.com/appc/spec/schema/types"
)
const PodManifestKind = types.ACKind("PodManifest")
type PodManifest struct {
ACVersion types.SemVer `json:"acVersion"`
ACKind types.ACKind `json:"acKind"`
Apps AppList `json:"apps"`
Volumes []types.Volume `json:"volumes"`
Isolators []types.Isolator `json:"isolators"`
Annotations types.Annotations `json:"annotations"`
Ports []types.ExposedPort `json:"ports"`
}
// podManifest is a model to facilitate extra validation during the
// unmarshalling of the PodManifest
type podManifest PodManifest
func BlankPodManifest() *PodManifest {
return &PodManifest{ACKind: PodManifestKind, ACVersion: AppContainerVersion}
}
func (pm *PodManifest) UnmarshalJSON(data []byte) error {
p := podManifest(*pm)
err := json.Unmarshal(data, &p)
if err != nil {
return err
}
npm := PodManifest(p)
if err := npm.assertValid(); err != nil {
return err
}
*pm = npm
return nil
}
func (pm PodManifest) MarshalJSON() ([]byte, error) {
if err := pm.assertValid(); err != nil {
return nil, err
}
return json.Marshal(podManifest(pm))
}
var pmKindError = types.InvalidACKindError(PodManifestKind)
// assertValid performs extra assertions on an PodManifest to
// ensure that fields are set appropriately, etc. It is used exclusively when
// marshalling and unmarshalling an PodManifest. Most
// field-specific validation is performed through the individual types being
// marshalled; assertValid() should only deal with higher-level validation.
func (pm *PodManifest) assertValid() error {
if pm.ACKind != PodManifestKind {
return pmKindError
}
return nil
}
type AppList []RuntimeApp
type appList AppList
func (al *AppList) UnmarshalJSON(data []byte) error {
a := appList{}
err := json.Unmarshal(data, &a)
if err != nil {
return err
}
nal := AppList(a)
if err := nal.assertValid(); err != nil {
return err
}
*al = nal
return nil
}
func (al AppList) MarshalJSON() ([]byte, error) {
if err := al.assertValid(); err != nil {
return nil, err
}
return json.Marshal(appList(al))
}
func (al AppList) assertValid() error {
seen := map[types.ACName]bool{}
for _, a := range al {
if _, ok := seen[a.Name]; ok {
return fmt.Errorf(`duplicate apps of name %q`, a.Name)
}
seen[a.Name] = true
}
return nil
}
// Get retrieves an app by the specified name from the AppList; if there is
// no such app, nil is returned. The returned *RuntimeApp MUST be considered
// read-only.
func (al AppList) Get(name types.ACName) *RuntimeApp {
for _, a := range al {
if name.Equals(a.Name) {
aa := a
return &aa
}
}
return nil
}
// Mount describes the mapping between a volume and an apps
// MountPoint that will be fulfilled at runtime.
type Mount struct {
Volume types.ACName `json:"volume"`
MountPoint types.ACName `json:"mountPoint"`
}
func (r Mount) assertValid() error {
if r.Volume.Empty() {
return errors.New("volume must be set")
}
if r.MountPoint.Empty() {
return errors.New("mountPoint must be set")
}
return nil
}
// RuntimeApp describes an application referenced in a PodManifest
type RuntimeApp struct {
Name types.ACName `json:"name"`
Image RuntimeImage `json:"image"`
App *types.App `json:"app,omitempty"`
Mounts []Mount `json:"mounts,omitempty"`
Annotations types.Annotations `json:"annotations,omitempty"`
}
// RuntimeImage describes an image referenced in a RuntimeApp
type RuntimeImage struct {
Name *types.ACName `json:"name,omitempty"`
ID types.Hash `json:"id"`
Labels types.Labels `json:"labels,omitempty"`
}
package schema
import (
"testing"
"github.com/appc/spec/schema/types"
)
func TestPodManifestMerge(t *testing.T) {
pmj := `{}`
pm := &PodManifest{}
if pm.UnmarshalJSON([]byte(pmj)) == nil {
t.Fatal("Manifest JSON without acKind and acVersion unmarshalled successfully")
}
pm = BlankPodManifest()
err := pm.UnmarshalJSON([]byte(pmj))
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func TestAppList(t *testing.T) {
ri := RuntimeImage{
ID: *types.NewHashSHA512([]byte{}),
}
al := AppList{
RuntimeApp{
Name: "foo",
Image: ri,
},
RuntimeApp{
Name: "bar",
Image: ri,
},
}
if _, err := al.MarshalJSON(); err != nil {
t.Errorf("want err=nil, got %v", err)
}
dal := AppList{
RuntimeApp{
Name: "foo",
Image: ri,
},
RuntimeApp{
Name: "bar",
Image: ri,
},
RuntimeApp{
Name: "foo",
Image: ri,
},
}
if _, err := dal.MarshalJSON(); err == nil {
t.Errorf("want err, got nil")
}
}
package types
import (
"encoding/json"
"fmt"
)
var (
ErrNoACKind = ACKindError("ACKind must be set")
)
// ACKind wraps a string to define a field which must be set with one of
// several ACKind values. If it is unset, or has an invalid value, the field
// will refuse to marshal/unmarshal.
type ACKind string
func (a ACKind) String() string {
return string(a)
}
func (a ACKind) assertValid() error {
s := a.String()
switch s {
case "ImageManifest", "PodManifest":
return nil
case "":
return ErrNoACKind
default:
msg := fmt.Sprintf("bad ACKind: %s", s)
return ACKindError(msg)
}
}
func (a ACKind) MarshalJSON() ([]byte, error) {
if err := a.assertValid(); err != nil {
return nil, err
}
return json.Marshal(a.String())
}
func (a *ACKind) UnmarshalJSON(data []byte) error {
var s string
err := json.Unmarshal(data, &s)
if err != nil {
return err
}
na := ACKind(s)
if err := na.assertValid(); err != nil {
return err
}
*a = na
return nil
}
package types
import (
"encoding/json"
"reflect"
"testing"
)
func TestACKindMarshalBad(t *testing.T) {
tests := map[string]error{
"Foo": ACKindError("bad ACKind: Foo"),
"ApplicationManifest": ACKindError("bad ACKind: ApplicationManifest"),
"": ErrNoACKind,
}
for in, werr := range tests {
a := ACKind(in)
b, gerr := json.Marshal(a)
if b != nil {
t.Errorf("ACKind(%q): want b=nil, got %v", in, b)
}
if jerr, ok := gerr.(*json.MarshalerError); !ok {
t.Errorf("expected JSONMarshalerError")
} else {
if e := jerr.Err; e != werr {
t.Errorf("err=%#v, want %#v", e, werr)
}
}
}
}
func TestACKindMarshalGood(t *testing.T) {
for i, in := range []string{
"ImageManifest",
"PodManifest",
} {
a := ACKind(in)
b, err := json.Marshal(a)
if !reflect.DeepEqual(b, []byte(`"`+in+`"`)) {
t.Errorf("#%d: marshalled=%v, want %v", i, b, []byte(in))
}
if err != nil {
t.Errorf("#%d: err=%v, want nil", i, err)
}
}
}
func TestACKindUnmarshalBad(t *testing.T) {
tests := []string{
"ImageManifest", // Not a valid JSON-encoded string
`"garbage"`,
`"AppManifest"`,
`""`,
}
for i, in := range tests {
var a, b ACKind
err := a.UnmarshalJSON([]byte(in))
if err == nil {
t.Errorf("#%d: err=nil, want non-nil", i)
} else if !reflect.DeepEqual(a, b) {
t.Errorf("#%d: a=%v, want empty", i, a)
}
}
}
func TestACKindUnmarshalGood(t *testing.T) {
tests := map[string]ACKind{
`"PodManifest"`: ACKind("PodManifest"),
`"ImageManifest"`: ACKind("ImageManifest"),
}
for in, w := range tests {
var a ACKind
err := json.Unmarshal([]byte(in), &a)
if err != nil {
t.Errorf("%v: err=%v, want nil", in, err)
} else if !reflect.DeepEqual(a, w) {
t.Errorf("%v: a=%v, want %v", in, a, w)
}
}
}
package types
import (
"encoding/json"
"errors"
"regexp"
"strings"
)
var (
// ValidACName is a regular expression that defines a valid ACName
ValidACName = regexp.MustCompile("^[a-z0-9]+([-./][a-z0-9]+)*$")
invalidChars = regexp.MustCompile("[^a-z0-9./-]")
invalidEdges = regexp.MustCompile("(^[./-]+)|([./-]+$)")
ErrEmptyACName = ACNameError("ACName cannot be empty")
ErrInvalidEdge = ACNameError("ACName must start and end with only lower case " +
"alphanumeric characters")
ErrInvalidChar = ACNameError("ACName must contain only lower case " +
`alphanumeric characters plus ".", "-", "/"`)
)
// ACName (an App-Container Name) is a format used by keys in different formats
// of the App Container Standard. An ACName is restricted to characters
// accepted by the DNS RFC[1] and "/"; all alphabetical characters must be
// lowercase only. Furthermore, the first and last character ("edges") must be
// alphanumeric, and an ACName cannot be empty. Programmatically, an ACName
// must conform to the regular expression ValidACName.
//
// [1] http://tools.ietf.org/html/rfc1123#page-13
type ACName string
func (n ACName) String() string {
return string(n)
}
// Set sets the ACName to the given value, if it is valid; if not,
// an error is returned.
func (n *ACName) Set(s string) error {
nn, err := NewACName(s)
if err == nil {
*n = *nn
}
return err
}
// Equals checks whether a given ACName is equal to this one.
func (n ACName) Equals(o ACName) bool {
return strings.ToLower(string(n)) == strings.ToLower(string(o))
}
// Empty returns a boolean indicating whether this ACName is empty.
func (n ACName) Empty() bool {
return n.String() == ""
}
// NewACName generates a new ACName from a string. If the given string is
// not a valid ACName, nil and an error are returned.
func NewACName(s string) (*ACName, error) {
n := ACName(s)
if err := n.assertValid(); err != nil {
return nil, err
}
return &n, nil
}
// MustACName generates a new ACName from a string, If the given string is
// not a valid ACName, it panics.
func MustACName(s string) *ACName {
n, err := NewACName(s)
if err != nil {
panic(err)
}
return n
}
func (n ACName) assertValid() error {
s := string(n)
if len(s) == 0 {
return ErrEmptyACName
}
if invalidChars.MatchString(s) {
return ErrInvalidChar
}
if invalidEdges.MatchString(s) {
return ErrInvalidEdge
}
return nil
}
// UnmarshalJSON implements the json.Unmarshaler interface
func (n *ACName) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
nn, err := NewACName(s)
if err != nil {
return err
}
*n = *nn
return nil
}
// MarshalJSON implements the json.Marshaler interface
func (n ACName) MarshalJSON() ([]byte, error) {
if err := n.assertValid(); err != nil {
return nil, err
}
return json.Marshal(n.String())
}
// SanitizeACName replaces every invalid ACName character in s with a dash
// making it a legal ACName string. If the character is an upper case letter it
// replaces it with its lower case. It also removes illegal edge characters
// (hyphens, periods and slashes).
//
// This is a helper function and its algorithm is not part of the spec. It
// should not be called without the user explicitly asking for a suggestion.
func SanitizeACName(s string) (string, error) {
s = strings.ToLower(s)
s = invalidChars.ReplaceAllString(s, "-")
s = invalidEdges.ReplaceAllString(s, "")
if s == "" {
return "", errors.New("must contain at least one valid character")
}
return s, nil
}
package types
import (
"encoding/json"
"reflect"
"testing"
)
var (
goodNames = []string{
"asdf",
"foo-bar-baz",
"database",
"example.com/database",
"example.com/ourapp-1.0.0",
"sub-domain.example.com/org/product/release-1.0.0",
}
badNames = []string{
"",
"foo#",
"EXAMPLE.com",
"foo.com/BAR",
"example.com/app_1",
"/app",
"app/",
"-app",
"app-",
".app",
"app.",
}
)
func TestNewACName(t *testing.T) {
for i, in := range goodNames {
l, err := NewACName(in)
if err != nil {
t.Errorf("#%d: got err=%v, want nil", i, err)
}
if l == nil {
t.Errorf("#%d: got l=nil, want non-nil", i)
}
}
}
func TestNewACNameBad(t *testing.T) {
for i, in := range badNames {
l, err := NewACName(in)
if l != nil {
t.Errorf("#%d: got l=%v, want nil", i, l)
}
if err == nil {
t.Errorf("#%d: got err=nil, want non-nil", i)
}
}
}
func TestMustACName(t *testing.T) {
for i, in := range goodNames {
l := MustACName(in)
if l == nil {
t.Errorf("#%d: got l=nil, want non-nil", i)
}
}
}
func expectPanicMustACName(i int, in string, t *testing.T) {
defer func() {
recover()
}()
_ = MustACName(in)
t.Errorf("#%d: panic expected", i)
}
func TestMustACNameBad(t *testing.T) {
for i, in := range badNames {
expectPanicMustACName(i, in, t)
}
}
func TestSanitizeACName(t *testing.T) {
tests := map[string]string{
"foo#": "foo",
"EXAMPLE.com": "example.com",
"foo.com/BAR": "foo.com/bar",
"example.com/app_1": "example.com/app-1",
"/app": "app",
"app/": "app",
"-app": "app",
"app-": "app",
".app": "app",
"app.": "app",
"app///": "app",
"-/.app..": "app",
"-/app.name-test/-/": "app.name-test",
"sub-domain.example.com/org/product/release-1.0.0": "sub-domain.example.com/org/product/release-1.0.0",
}
for in, ex := range tests {
o, err := SanitizeACName(in)
if err != nil {
t.Errorf("got err=%v, want nil", err)
}
if o != ex {
t.Errorf("got l=%s, want %s", o, ex)
}
}
}
func TestACNameSetGood(t *testing.T) {
tests := map[string]ACName{
"blargh": ACName("blargh"),
"example.com/ourapp-1.0.0": ACName("example.com/ourapp-1.0.0"),
}
for in, w := range tests {
// Ensure an empty name is set appropriately
var a ACName
err := a.Set(in)
if err != nil {
t.Errorf("%v: got err=%v, want nil", in, err)
continue
}
if !reflect.DeepEqual(a, w) {
t.Errorf("%v: a=%v, want %v", in, a, w)
}
// Ensure an existing name is overwritten
var b ACName = ACName("orig")
err = b.Set(in)
if err != nil {
t.Errorf("%v: got err=%v, want nil", in, err)
continue
}
if !reflect.DeepEqual(b, w) {
t.Errorf("%v: b=%v, want %v", in, b, w)
}
}
}
func TestACNameSetBad(t *testing.T) {
for i, in := range badNames {
// Ensure an empty name stays empty
var a ACName
err := a.Set(in)
if err == nil {
t.Errorf("#%d: err=%v, want nil", i, err)
continue
}
if w := ACName(""); !reflect.DeepEqual(a, w) {
t.Errorf("%d: a=%v, want %v", i, a, w)
}
// Ensure an existing name is not overwritten
var b ACName = ACName("orig")
err = b.Set(in)
if err == nil {
t.Errorf("#%d: err=%v, want nil", i, err)
continue
}
if w := ACName("orig"); !reflect.DeepEqual(b, w) {
t.Errorf("%d: b=%v, want %v", i, b, w)
}
}
}
func TestSanitizeACNameBad(t *testing.T) {
tests := []string{
"__",
"..",
"//",
"",
".//-"}
for i, in := range tests {
l, err := SanitizeACName(in)
if l != "" {
t.Errorf("#%d: got l=%v, want nil", i, l)
}
if err == nil {
t.Errorf("#%d: got err=nil, want non-nil", i)
}
}
}
func TestACNameUnmarshalBad(t *testing.T) {
tests := []string{
"",
"garbage",
`""`,
`"EXAMPLE"`,
`"example.com/app_1"`,
}
for i, in := range tests {
var a, b ACName
err := a.UnmarshalJSON([]byte(in))
if err == nil {
t.Errorf("#%d: err=nil, want non-nil", i)
} else if !reflect.DeepEqual(a, b) {
t.Errorf("#%d: a=%v, want empty", i, a)
}
}
}
func TestACNameUnmarshalGood(t *testing.T) {
tests := map[string]ACName{
`"example"`: ACName("example"),
`"foo.com/bar"`: ACName("foo.com/bar"),
}
for in, w := range tests {
var a ACName
err := json.Unmarshal([]byte(in), &a)
if err != nil {
t.Errorf("%v: err=%v, want nil", in, err)
} else if !reflect.DeepEqual(a, w) {
t.Errorf("%v: a=%v, want %v", in, a, w)
}
}
}
func TestACNameMarshalBad(t *testing.T) {
tests := map[string]error{
"Foo": ErrInvalidChar,
"foo#": ErrInvalidChar,
"/foo": ErrInvalidEdge,
"example.com/": ErrInvalidEdge,
"": ErrEmptyACName,
}
for in, werr := range tests {
a := ACName(in)
b, gerr := json.Marshal(a)
if b != nil {
t.Errorf("ACName(%q): want b=nil, got %v", in, b)
}
if jerr, ok := gerr.(*json.MarshalerError); !ok {
t.Errorf("expected JSONMarshalerError")
} else {
if e := jerr.Err; e != werr {
t.Errorf("err=%#v, want %#v", e, werr)
}
}
}
}
func TestACNameMarshalGood(t *testing.T) {
for i, in := range goodNames {
a := ACName(in)
b, err := json.Marshal(a)
if !reflect.DeepEqual(b, []byte(`"`+in+`"`)) {
t.Errorf("#%d: marshalled=%v, want %v", i, b, []byte(in))
}
if err != nil {
t.Errorf("#%d: err=%v, want nil", i, err)
}
}
}
package types
import (
"encoding/json"
"fmt"
)
type Annotations []Annotation
type annotations Annotations
type Annotation struct {
Name ACName `json:"name"`
Value string `json:"value"`
}
func (a Annotations) assertValid() error {
seen := map[ACName]string{}
for _, anno := range a {
_, ok := seen[anno.Name]
if ok {
return fmt.Errorf(`duplicate annotations of name %q`, anno.Name)
}
seen[anno.Name] = anno.Value
}
if c, ok := seen["created"]; ok {
if _, err := NewDate(c); err != nil {
return err
}
}
if h, ok := seen["homepage"]; ok {
if _, err := NewURL(h); err != nil {
return err
}
}
if d, ok := seen["documentation"]; ok {
if _, err := NewURL(d); err != nil {
return err
}
}
return nil
}
func (a Annotations) MarshalJSON() ([]byte, error) {
if err := a.assertValid(); err != nil {
return nil, err
}
return json.Marshal(annotations(a))
}
func (a *Annotations) UnmarshalJSON(data []byte) error {
var ja annotations
if err := json.Unmarshal(data, &ja); err != nil {
return err
}
na := Annotations(ja)
if err := na.assertValid(); err != nil {
return err
}
*a = na
return nil
}
// Retrieve the value of an annotation by the given name from Annotations, if
// it exists.
func (a Annotations) Get(name string) (val string, ok bool) {
for _, anno := range a {
if anno.Name.String() == name {
return anno.Value, true
}
}
return "", false
}
// Set sets the value of an annotation by the given name, overwriting if one already exists.
func (a *Annotations) Set(name ACName, value string) {
for i, anno := range *a {
if anno.Name.Equals(name) {
(*a)[i] = Annotation{
Name: name,
Value: value,
}
return
}
}
anno := Annotation{
Name: name,
Value: value,
}
*a = append(*a, anno)
}
package types
import (
"reflect"
"testing"
)
func makeAnno(n, v string) Annotation {
name, err := NewACName(n)
if err != nil {
panic(err)
}
return Annotation{
Name: *name,
Value: v,
}
}
func TestAnnotationsAssertValid(t *testing.T) {
tests := []struct {
in []Annotation
werr bool
}{
// duplicate names should fail
{
[]Annotation{
makeAnno("foo", "bar"),
makeAnno("foo", "baz"),
},
true,
},
// bad created should fail
{
[]Annotation{
makeAnno("created", "garbage"),
},
true,
},
// bad homepage should fail
{
[]Annotation{
makeAnno("homepage", "not-A$@#URL"),
},
true,
},
// bad documentation should fail
{
[]Annotation{
makeAnno("documentation", "ftp://isnotallowed.com"),
},
true,
},
// good cases
{
[]Annotation{
makeAnno("created", "2004-05-14T23:11:14+00:00"),
makeAnno("documentation", "http://example.com/docs"),
},
false,
},
{
[]Annotation{
makeAnno("foo", "bar"),
makeAnno("homepage", "https://homepage.com"),
},
false,
},
// empty is OK
{
[]Annotation{},
false,
},
}
for i, tt := range tests {
a := Annotations(tt.in)
err := a.assertValid()
if gerr := (err != nil); gerr != tt.werr {
t.Errorf("#%d: gerr=%t, want %t (err=%v)", i, gerr, tt.werr, err)
}
}
}
func TestAnnotationsMarshal(t *testing.T) {
for i, tt := range []struct {
in []Annotation
wb []byte
werr bool
}{
{
[]Annotation{
makeAnno("foo", "bar"),
makeAnno("foo", "baz"),
makeAnno("website", "http://example.com/anno"),
},
nil,
true,
},
{
[]Annotation{
makeAnno("a", "b"),
},
[]byte(`[{"name":"a","value":"b"}]`),
false,
},
{
[]Annotation{
makeAnno("foo", "bar"),
makeAnno("website", "http://example.com/anno"),
},
[]byte(`[{"name":"foo","value":"bar"},{"name":"website","value":"http://example.com/anno"}]`),
false,
},
} {
a := Annotations(tt.in)
b, err := a.MarshalJSON()
if !reflect.DeepEqual(b, tt.wb) {
t.Errorf("#%d: b=%s, want %s", i, b, tt.wb)
}
gerr := err != nil
if gerr != tt.werr {
t.Errorf("#%d: gerr=%t, want %t (err=%v)", i, gerr, tt.werr, err)
}
}
}
func TestAnnotationsUnmarshal(t *testing.T) {
tests := []struct {
in string
wann *Annotations
werr bool
}{
{
`garbage`,
&Annotations{},
true,
},
{
`[{"name":"a","value":"b"},{"name":"a","value":"b"}]`,
&Annotations{},
true,
},
{
`[{"name":"a","value":"b"}]`,
&Annotations{
makeAnno("a", "b"),
},
false,
},
}
for i, tt := range tests {
a := &Annotations{}
err := a.UnmarshalJSON([]byte(tt.in))
gerr := err != nil
if gerr != tt.werr {
t.Errorf("#%d: gerr=%t, want %t (err=%v)", i, gerr, tt.werr, err)
}
if !reflect.DeepEqual(a, tt.wann) {
t.Errorf("#%d: ann=%#v, want %#v", i, a, tt.wann)
}
}
}
func TestAnnotationsGet(t *testing.T) {
for i, tt := range []struct {
in string
wval string
wok bool
}{
{"foo", "bar", true},
{"website", "http://example.com/anno", true},
{"baz", "", false},
{"wuuf", "", false},
} {
a := Annotations{
makeAnno("foo", "bar"),
makeAnno("website", "http://example.com/anno"),
}
gval, gok := a.Get(tt.in)
if gval != tt.wval {
t.Errorf("#%d: val=%v, want %v", i, gval, tt.wval)
}
if gok != tt.wok {
t.Errorf("#%d: ok=%t, want %t", i, gok, tt.wok)
}
}
}
func TestAnnotationsSet(t *testing.T) {
a := Annotations{}
a.Set("foo", "bar")
w := Annotations{
Annotation{ACName("foo"), "bar"},
}
if !reflect.DeepEqual(w, a) {
t.Fatalf("want %v, got %v", w, a)
}
a.Set("dog", "woof")
w = Annotations{
Annotation{ACName("foo"), "bar"},
Annotation{ACName("dog"), "woof"},
}
if !reflect.DeepEqual(w, a) {
t.Fatalf("want %v, got %v", w, a)
}
a.Set("foo", "baz")
w = Annotations{
Annotation{ACName("foo"), "baz"},
Annotation{ACName("dog"), "woof"},
}
if !reflect.DeepEqual(w, a) {
t.Fatalf("want %v, got %v", w, a)
}
}
package types
import (
"encoding/json"
"errors"
"fmt"
"path"
)
type App struct {
Exec Exec `json:"exec"`
EventHandlers []EventHandler `json:"eventHandlers,omitempty"`
User string `json:"user"`
Group string `json:"group"`
WorkingDirectory string `json:"workingDirectory,omitempty"`
Environment Environment `json:"environment,omitempty"`
MountPoints []MountPoint `json:"mountPoints,omitempty"`
Ports []Port `json:"ports,omitempty"`
Isolators Isolators `json:"isolators,omitempty"`
}
// app is a model to facilitate extra validation during the
// unmarshalling of the App
type app App
func (a *App) UnmarshalJSON(data []byte) error {
ja := app(*a)
err := json.Unmarshal(data, &ja)
if err != nil {
return err
}
na := App(ja)
if err := na.assertValid(); err != nil {
return err
}
if na.Environment == nil {
na.Environment = make(Environment, 0)
}
*a = na
return nil
}
func (a App) MarshalJSON() ([]byte, error) {
if err := a.assertValid(); err != nil {
return nil, err
}
return json.Marshal(app(a))
}
func (a *App) assertValid() error {
if err := a.Exec.assertValid(); err != nil {
return err
}
if a.User == "" {
return errors.New(`User is required`)
}
if a.Group == "" {
return errors.New(`Group is required`)
}
if !path.IsAbs(a.WorkingDirectory) && a.WorkingDirectory != "" {
return errors.New("WorkingDirectory must be an absolute path")
}
eh := make(map[string]bool)
for _, e := range a.EventHandlers {
name := e.Name
if eh[name] {
return fmt.Errorf("Only one eventHandler of name %q allowed", name)
}
eh[name] = true
}
if err := a.Environment.assertValid(); err != nil {
return err
}
return nil
}
package types
import (
"reflect"
"testing"
)
func TestAppValid(t *testing.T) {
tests := []App{
App{
Exec: []string{"/bin/httpd"},
User: "0",
Group: "0",
WorkingDirectory: "/tmp",
},
App{
Exec: []string{"/app"},
User: "0",
Group: "0",
EventHandlers: []EventHandler{
{Name: "pre-start"},
{Name: "post-stop"},
},
Environment: []EnvironmentVariable{
{Name: "DEBUG", Value: "true"},
},
WorkingDirectory: "/tmp",
},
App{
Exec: []string{"/app", "arg1", "arg2"},
User: "0",
Group: "0",
WorkingDirectory: "/tmp",
},
}
for i, tt := range tests {
if err := tt.assertValid(); err != nil {
t.Errorf("#%d: err == %v, want nil", i, err)
}
}
}
func TestAppExecInvalid(t *testing.T) {
tests := []App{
App{
Exec: nil,
},
App{
Exec: []string{},
User: "0",
Group: "0",
},
App{
Exec: []string{"app"},
User: "0",
Group: "0",
},
App{
Exec: []string{"bin/app", "arg1"},
User: "0",
Group: "0",
},
}
for i, tt := range tests {
if err := tt.assertValid(); err == nil {
t.Errorf("#%d: err == nil, want non-nil", i)
}
}
}
func TestAppEventHandlersInvalid(t *testing.T) {
tests := []App{
App{
Exec: []string{"/bin/httpd"},
User: "0",
Group: "0",
EventHandlers: []EventHandler{
EventHandler{
Name: "pre-start",
},
EventHandler{
Name: "pre-start",
},
},
},
App{
Exec: []string{"/bin/httpd"},
User: "0",
Group: "0",
EventHandlers: []EventHandler{
EventHandler{
Name: "post-stop",
},
EventHandler{
Name: "pre-start",
},
EventHandler{
Name: "post-stop",
},
},
},
}
for i, tt := range tests {
if err := tt.assertValid(); err == nil {
t.Errorf("#%d: err == nil, want non-nil", i)
}
}
}
func TestUserGroupInvalid(t *testing.T) {
tests := []App{
App{
Exec: []string{"/app"},
},
App{
Exec: []string{"/app"},
User: "0",
},
App{
Exec: []string{"app"},
Group: "0",
},
}
for i, tt := range tests {
if err := tt.assertValid(); err == nil {
t.Errorf("#%d: err == nil, want non-nil", i)
}
}
}
func TestAppWorkingDirectoryInvalid(t *testing.T) {
tests := []App{
App{
Exec: []string{"/app"},
User: "foo",
Group: "bar",
WorkingDirectory: "stuff",
},
App{
Exec: []string{"/app"},
User: "foo",
Group: "bar",
WorkingDirectory: "../home/fred",
},
}
for i, tt := range tests {
if err := tt.assertValid(); err == nil {
t.Errorf("#%d: err == nil, want non-nil", i)
}
}
}
func TestAppEnvironmentInvalid(t *testing.T) {
tests := []App{
App{
Exec: []string{"/app"},
User: "foo",
Group: "bar",
Environment: Environment{
EnvironmentVariable{"0DEBUG", "true"},
},
},
}
for i, tt := range tests {
if err := tt.assertValid(); err == nil {
t.Errorf("#%d: err == nil, want non-nil", i)
}
}
}
func TestAppUnmarshal(t *testing.T) {
tests := []struct {
in string
wann *App
werr bool
}{
{
`garbage`,
&App{},
true,
},
{
`{"Exec":"not a list"}`,
&App{},
true,
},
{
`{"Exec":["notfullyqualified"]}`,
&App{},
true,
},
{
`{"Exec":["/a"],"User":"0","Group":"0"}`,
&App{
Exec: Exec{
"/a",
},
User: "0",
Group: "0",
Environment: make(Environment, 0),
},
false,
},
}
for i, tt := range tests {
a := &App{}
err := a.UnmarshalJSON([]byte(tt.in))
gerr := err != nil
if gerr != tt.werr {
t.Errorf("#%d: gerr=%t, want %t (err=%v)", i, gerr, tt.werr, err)
}
if !reflect.DeepEqual(a, tt.wann) {
t.Errorf("#%d: ann=%#v, want %#v", i, a, tt.wann)
}
}
}
package types
import (
"encoding/json"
"fmt"
"time"
)
// Date wraps time.Time to marshal/unmarshal to/from JSON strings in strict
// accordance with RFC3339
// TODO(jonboulle): golang's implementation seems slightly buggy here;
// according to http://tools.ietf.org/html/rfc3339#section-5.6 , applications
// may choose to separate the date and time with a space instead of a T
// character (for example, `date --rfc-3339` on GNU coreutils) - but this is
// considered an error by go's parser. File a bug?
type Date time.Time
func NewDate(s string) (*Date, error) {
t, err := time.Parse(time.RFC3339, s)
if err != nil {
return nil, fmt.Errorf("bad Date: %v", err)
}
d := Date(t)
return &d, nil
}
func (d Date) String() string {
return time.Time(d).Format(time.RFC3339)
}
func (d *Date) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
nd, err := NewDate(s)
if err != nil {
return err
}
*d = *nd
return nil
}
func (d Date) MarshalJSON() ([]byte, error) {
return json.Marshal(d.String())
}
package types
import (
"encoding/json"
"testing"
"time"
)
var (
pst = time.FixedZone("Pacific", -8*60*60)
)
func TestUnmarshalDate(t *testing.T) {
tests := []struct {
in string
wt time.Time
}{
{
`"2004-05-14T23:11:14+00:00"`,
time.Date(2004, 05, 14, 23, 11, 14, 0, time.UTC),
},
{
`"2001-02-03T04:05:06Z"`,
time.Date(2001, 02, 03, 04, 05, 06, 0, time.UTC),
},
{
`"2014-11-14T17:36:54-08:00"`,
time.Date(2014, 11, 14, 17, 36, 54, 0, pst),
},
{
`"2004-05-14T23:11:14+00:00"`,
time.Date(2004, 05, 14, 23, 11, 14, 0, time.UTC),
},
}
for i, tt := range tests {
var d Date
if err := json.Unmarshal([]byte(tt.in), &d); err != nil {
t.Errorf("#%d: got err=%v, want nil", i, err)
}
if gt := time.Time(d); !gt.Equal(tt.wt) {
t.Errorf("#%d: got time=%v, want %v", i, gt, tt.wt)
}
}
}
func TestUnmarshalDateBad(t *testing.T) {
tests := []string{
`not a json string`,
`2014-11-14T17:36:54-08:00`,
`"garbage"`,
`"1416015188"`,
`"Fri Nov 14 17:53:02 PST 2014"`,
`"2014-11-1417:36:54"`,
}
for i, tt := range tests {
var d Date
if err := json.Unmarshal([]byte(tt), &d); err == nil {
t.Errorf("#%d: unexpected nil err", i)
}
}
}
package types
import (
"encoding/json"
"errors"
)
type Dependencies []Dependency
type Dependency struct {
App ACName `json:"app"`
ImageID *Hash `json:"imageID,omitempty"`
Labels Labels `json:"labels,omitempty"`
}
type dependency Dependency
func (d Dependency) assertValid() error {
if len(d.App) < 1 {
return errors.New(`App cannot be empty`)
}
return nil
}
func (d Dependency) MarshalJSON() ([]byte, error) {
if err := d.assertValid(); err != nil {
return nil, err
}
return json.Marshal(dependency(d))
}
func (d *Dependency) UnmarshalJSON(data []byte) error {
var jd dependency
if err := json.Unmarshal(data, &jd); err != nil {
return err
}
nd := Dependency(jd)
if err := nd.assertValid(); err != nil {
return err
}
*d = nd
return nil
}
package types
import "testing"
func TestEmptyHash(t *testing.T) {
dj := `{"app": "example.com/reduce-worker-base"}`
var d Dependency
err := d.UnmarshalJSON([]byte(dj))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Marshal to verify that marshalling works without validation errors
buf, err := d.MarshalJSON()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Unmarshal to verify that the generated json will not create wrong empty hash
err = d.UnmarshalJSON(buf)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
// Package types contains structs representing the various types in the app
// container specification. It is used by the [schema manifest types](../)
// to enforce validation.
package types
package types
import (
"encoding/json"
"fmt"
"regexp"
)
var (
envPattern = regexp.MustCompile("^[A-Za-z_][A-Za-z_0-9]*$")
)
type Environment []EnvironmentVariable
type environment Environment
type EnvironmentVariable struct {
Name string `json:"name"`
Value string `json:"value"`
}
func (ev EnvironmentVariable) assertValid() error {
if len(ev.Name) == 0 {
return fmt.Errorf(`environment variable name must not be empty`)
}
if !envPattern.MatchString(ev.Name) {
return fmt.Errorf(`environment variable does not have valid identifier %q`, ev.Name)
}
return nil
}
func (e Environment) assertValid() error {
seen := map[string]bool{}
for _, env := range e {
if err := env.assertValid(); err != nil {
return err
}
_, ok := seen[env.Name]
if ok {
return fmt.Errorf(`duplicate environment variable of name %q`, env.Name)
}
seen[env.Name] = true
}
return nil
}
func (e Environment) MarshalJSON() ([]byte, error) {
if err := e.assertValid(); err != nil {
return nil, err
}
return json.Marshal(environment(e))
}
func (e *Environment) UnmarshalJSON(data []byte) error {
var je environment
if err := json.Unmarshal(data, &je); err != nil {
return err
}
ne := Environment(je)
if err := ne.assertValid(); err != nil {
return err
}
*e = ne
return nil
}
// Retrieve the value of an environment variable by the given name from
// Environment, if it exists.
func (e Environment) Get(name string) (value string, ok bool) {
for _, env := range e {
if env.Name == name {
return env.Value, true
}
}
return "", false
}
// Set sets the value of an environment variable by the given name,
// overwriting if one already exists.
func (e *Environment) Set(name string, value string) {
for i, env := range *e {
if env.Name == name {
(*e)[i] = EnvironmentVariable{
Name: name,
Value: value,
}
return
}
}
env := EnvironmentVariable{
Name: name,
Value: value,
}
*e = append(*e, env)
}
package types
import (
"testing"
)
func TestEnvironmentAssertValid(t *testing.T) {
tests := []struct {
env Environment
werr bool
}{
// duplicate names should fail
{
Environment{
EnvironmentVariable{"DEBUG", "true"},
EnvironmentVariable{"DEBUG", "true"},
},
true,
},
// empty name should fail
{
Environment{
EnvironmentVariable{"", "value"},
},
true,
},
// name beginning with digit should fail
{
Environment{
EnvironmentVariable{"0DEBUG", "true"},
},
true,
},
// name with non [A-Za-z0-9_] should fail
{
Environment{
EnvironmentVariable{"VERBOSE-DEBUG", "true"},
},
true,
},
// accepted environment variable forms
{
Environment{
EnvironmentVariable{"DEBUG", "true"},
},
false,
},
{
Environment{
EnvironmentVariable{"_0_DEBUG_0_", "true"},
},
false,
},
}
for i, test := range tests {
env := Environment(test.env)
err := env.assertValid()
if gerr := (err != nil); gerr != test.werr {
t.Errorf("#%d: gerr=%t, want %t (err=%v)", i, gerr, test.werr, err)
}
}
}
package types
import "fmt"
// An ACKindError is returned when the wrong ACKind is set in a manifest
type ACKindError string
func (e ACKindError) Error() string {
return string(e)
}
func InvalidACKindError(kind ACKind) ACKindError {
return ACKindError(fmt.Sprintf("missing or bad ACKind (must be %#v)", kind))
}
// An ACVersionError is returned when a bad ACVersion is set in a manifest
type ACVersionError string
func (e ACVersionError) Error() string {
return string(e)
}
// An ACNameError is returned when a bad value is used for an ACName
type ACNameError string
func (e ACNameError) Error() string {
return string(e)
}
package types
import (
"encoding/json"
"errors"
"fmt"
)
type EventHandler struct {
Name string `json:"name"`
Exec Exec `json:"exec"`
}
type eventHandler EventHandler
func (e EventHandler) assertValid() error {
s := e.Name
switch s {
case "pre-start", "post-stop":
return nil
case "":
return errors.New(`eventHandler "name" cannot be empty`)
default:
return fmt.Errorf(`bad eventHandler "name": %q`, s)
}
}
func (e EventHandler) MarshalJSON() ([]byte, error) {
if err := e.assertValid(); err != nil {
return nil, err
}
return json.Marshal(eventHandler(e))
}
func (e *EventHandler) UnmarshalJSON(data []byte) error {
var je eventHandler
err := json.Unmarshal(data, &je)
if err != nil {
return err
}
ne := EventHandler(je)
if err := ne.assertValid(); err != nil {
return err
}
*e = ne
return nil
}
package types
import (
"encoding/json"
"errors"
"path/filepath"
)
type Exec []string
type exec Exec
func (e Exec) assertValid() error {
if len(e) < 1 {
return errors.New(`Exec cannot be empty`)
}
if !filepath.IsAbs(e[0]) {
return errors.New(`Exec[0] must be absolute path`)
}
return nil
}
func (e Exec) MarshalJSON() ([]byte, error) {
if err := e.assertValid(); err != nil {
return nil, err
}
return json.Marshal(exec(e))
}
func (e *Exec) UnmarshalJSON(data []byte) error {
var je exec
err := json.Unmarshal(data, &je)
if err != nil {
return err
}
ne := Exec(je)
if err := ne.assertValid(); err != nil {
return err
}
*e = ne
return nil
}
package types
import "testing"
func TestExecValid(t *testing.T) {
tests := []Exec{
Exec{"/bin/httpd"},
Exec{"/app"},
Exec{"/app", "arg1", "arg2"},
}
for i, tt := range tests {
if err := tt.assertValid(); err != nil {
t.Errorf("#%d: err == %v, want nil", i, err)
}
}
}
func TestExecInvalid(t *testing.T) {
tests := []Exec{
Exec{},
Exec{"app"},
}
for i, tt := range tests {
if err := tt.assertValid(); err == nil {
t.Errorf("#%d: err == nil, want non-nil", i)
}
}
}
package types
import (
"crypto/sha512"
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
)
const (
maxHashSize = (sha512.Size / 2) + len("sha512-")
)
// Hash encodes a hash specified in a string of the form:
// "<type>-<value>"
// for example
// "sha512-06c733b1838136838e6d2d3e8fa5aea4c7905e92[...]"
// Valid types are currently:
// * sha512
type Hash struct {
typ string
Val string
}
func NewHash(s string) (*Hash, error) {
elems := strings.Split(s, "-")
if len(elems) != 2 {
return nil, errors.New("badly formatted hash string")
}
nh := Hash{
typ: elems[0],
Val: elems[1],
}
if err := nh.assertValid(); err != nil {
return nil, err
}
return &nh, nil
}
func (h Hash) String() string {
return fmt.Sprintf("%s-%s", h.typ, h.Val)
}
func (h *Hash) Set(s string) error {
nh, err := NewHash(s)
if err == nil {
*h = *nh
}
return err
}
func (h Hash) Empty() bool {
return reflect.DeepEqual(h, Hash{})
}
func (h Hash) assertValid() error {
switch h.typ {
case "sha512":
case "":
return fmt.Errorf("unexpected empty hash type")
default:
return fmt.Errorf("unrecognized hash type: %v", h.typ)
}
if h.Val == "" {
return fmt.Errorf("unexpected empty hash value")
}
return nil
}
func (h *Hash) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
nh, err := NewHash(s)
if err != nil {
return err
}
*h = *nh
return nil
}
func (h Hash) MarshalJSON() ([]byte, error) {
if err := h.assertValid(); err != nil {
return nil, err
}
return json.Marshal(h.String())
}
func NewHashSHA512(b []byte) *Hash {
h := sha512.New()
h.Write(b)
nh, _ := NewHash(fmt.Sprintf("sha512-%x", h.Sum(nil)))
return nh
}
func ShortHash(hash string) string {
if len(hash) > maxHashSize {
return hash[:maxHashSize]
}
return hash
}
package types
import (
"encoding/json"
"testing"
)
func TestMarshalHash(t *testing.T) {
tests := []struct {
typ string
val string
wout string
}{
{
"sha512",
"abcdefghi",
`"sha512-abcdefghi"`,
},
{
"sha512",
"06c733b1838136838e6d2d3e8fa5aea4c7905e92",
`"sha512-06c733b1838136838e6d2d3e8fa5aea4c7905e92"`,
},
}
for i, tt := range tests {
h := Hash{
typ: tt.typ,
Val: tt.val,
}
b, err := json.Marshal(h)
if err != nil {
t.Errorf("#%d: unexpected err=%v", i, err)
}
if g := string(b); g != tt.wout {
t.Errorf("#%d: got string=%v, want %v", i, g, tt.wout)
}
}
}
func TestMarshalHashBad(t *testing.T) {
tests := []struct {
typ string
val string
}{
{
// empty value
"sha512",
"",
},
{
// bad type
"sha1",
"abcdef",
},
{
// empty type
"",
"abcdef",
},
{
// empty empty
"",
"",
},
}
for i, tt := range tests {
h := Hash{
typ: tt.typ,
Val: tt.val,
}
g, err := json.Marshal(h)
if err == nil {
t.Errorf("#%d: unexpected nil err", i)
}
if g != nil {
t.Errorf("#%d: unexpected non-nil bytes: %v", i, g)
}
}
}
package types
import (
"encoding/json"
)
var (
isolatorMap map[ACName]IsolatorValueConstructor
)
func init() {
isolatorMap = make(map[ACName]IsolatorValueConstructor)
}
type IsolatorValueConstructor func() IsolatorValue
func AddIsolatorValueConstructor(n ACName, i IsolatorValueConstructor) {
isolatorMap[n] = i
}
type Isolators []Isolator
// GetByName returns the last isolator in the list by the given name.
func (is *Isolators) GetByName(name ACName) *Isolator {
var i Isolator
for j := len(*is) - 1; j >= 0; j-- {
i = []Isolator(*is)[j]
if i.Name == name {
return &i
}
}
return nil
}
// Unrecognized returns a set of isolators that are not recognized.
// An isolator is not recognized if it has not had an associated
// constructor registered with AddIsolatorValueConstructor.
func (is *Isolators) Unrecognized() Isolators {
u := Isolators{}
for _, i := range *is {
if i.value == nil {
u = append(u, i)
}
}
return u
}
type IsolatorValue interface {
UnmarshalJSON(b []byte) error
AssertValid() error
}
type Isolator struct {
Name ACName `json:"name"`
ValueRaw *json.RawMessage `json:"value"`
value IsolatorValue
}
type isolator Isolator
func (i *Isolator) Value() IsolatorValue {
return i.value
}
func (i *Isolator) UnmarshalJSON(b []byte) error {
var ii isolator
err := json.Unmarshal(b, &ii)
if err != nil {
return err
}
var dst IsolatorValue
con, ok := isolatorMap[ii.Name]
if ok {
dst = con()
err = dst.UnmarshalJSON(*ii.ValueRaw)
if err != nil {
return err
}
err = dst.AssertValid()
if err != nil {
return err
}
}
i.value = dst
i.ValueRaw = ii.ValueRaw
i.Name = ii.Name
return nil
}
package types
import (
"encoding/json"
"errors"
)
const (
LinuxCapabilitiesRetainSetName = "os/linux/capabilities-retain-set"
LinuxCapabilitiesRevokeSetName = "os/linux/capabilities-revoke-set"
)
func init() {
AddIsolatorValueConstructor(LinuxCapabilitiesRetainSetName, NewLinuxCapabilitiesRetainSet)
AddIsolatorValueConstructor(LinuxCapabilitiesRevokeSetName, NewLinuxCapabilitiesRevokeSet)
}
type LinuxCapabilitiesSet interface {
Set() []LinuxCapability
AssertValid() error
}
type LinuxCapability string
type linuxCapabilitiesSetValue struct {
Set []LinuxCapability `json:"set"`
}
type linuxCapabilitiesSetBase struct {
val linuxCapabilitiesSetValue
}
func (l linuxCapabilitiesSetBase) AssertValid() error {
if len(l.val.Set) == 0 {
return errors.New("set must be non-empty")
}
return nil
}
func (l *linuxCapabilitiesSetBase) UnmarshalJSON(b []byte) error {
var v linuxCapabilitiesSetValue
err := json.Unmarshal(b, &v)
if err != nil {
return err
}
l.val = v
return err
}
func (l linuxCapabilitiesSetBase) Set() []LinuxCapability {
return l.val.Set
}
func NewLinuxCapabilitiesRetainSet() IsolatorValue {
return &LinuxCapabilitiesRetainSet{}
}
type LinuxCapabilitiesRetainSet struct {
linuxCapabilitiesSetBase
}
func NewLinuxCapabilitiesRevokeSet() IsolatorValue {
return &LinuxCapabilitiesRevokeSet{}
}
type LinuxCapabilitiesRevokeSet struct {
linuxCapabilitiesSetBase
}
package types
import (
"encoding/json"
"errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource"
)
var (
ErrDefaultTrue = errors.New("default must be false")
ErrDefaultRequired = errors.New("default must be true")
ErrRequestNonEmpty = errors.New("request not supported by this resource, must be empty")
)
const (
ResourceBlockBandwidthName = "resource/block-bandwidth"
ResourceBlockIOPSName = "resource/block-iops"
ResourceCPUName = "resource/cpu"
ResourceMemoryName = "resource/memory"
ResourceNetworkBandwidthName = "resource/network-bandwidth"
)
func init() {
AddIsolatorValueConstructor(ResourceBlockBandwidthName, NewResourceBlockBandwidth)
AddIsolatorValueConstructor(ResourceBlockIOPSName, NewResourceBlockIOPS)
AddIsolatorValueConstructor(ResourceCPUName, NewResourceCPU)
AddIsolatorValueConstructor(ResourceMemoryName, NewResourceMemory)
AddIsolatorValueConstructor(ResourceNetworkBandwidthName, NewResourceNetworkBandwidth)
}
func NewResourceBlockBandwidth() IsolatorValue {
return &ResourceBlockBandwidth{}
}
func NewResourceBlockIOPS() IsolatorValue {
return &ResourceBlockIOPS{}
}
func NewResourceCPU() IsolatorValue {
return &ResourceCPU{}
}
func NewResourceNetworkBandwidth() IsolatorValue {
return &ResourceNetworkBandwidth{}
}
func NewResourceMemory() IsolatorValue {
return &ResourceMemory{}
}
type Resource interface {
Limit() *resource.Quantity
Request() *resource.Quantity
Default() bool
}
type ResourceBase struct {
val resourceValue
}
type resourceValue struct {
Default bool `json:"default"`
Request *resource.Quantity `json:"request"`
Limit *resource.Quantity `json:"limit"`
}
func (r ResourceBase) Limit() *resource.Quantity {
return r.val.Limit
}
func (r ResourceBase) Request() *resource.Quantity {
return r.val.Request
}
func (r ResourceBase) Default() bool {
return r.val.Default
}
func (r *ResourceBase) UnmarshalJSON(b []byte) error {
return json.Unmarshal(b, &r.val)
}
func (r ResourceBase) AssertValid() error {
return nil
}
type ResourceBlockBandwidth struct {
ResourceBase
}
func (r ResourceBlockBandwidth) AssertValid() error {
if r.Default() != true {
return ErrDefaultRequired
}
if r.Request() != nil {
return ErrRequestNonEmpty
}
return nil
}
type ResourceBlockIOPS struct {
ResourceBase
}
func (r ResourceBlockIOPS) AssertValid() error {
if r.Default() != true {
return ErrDefaultRequired
}
if r.Request() != nil {
return ErrRequestNonEmpty
}
return nil
}
type ResourceCPU struct {
ResourceBase
}
func (r ResourceCPU) AssertValid() error {
if r.Default() != false {
return ErrDefaultTrue
}
return nil
}
type ResourceMemory struct {
ResourceBase
}
func (r ResourceMemory) AssertValid() error {
if r.Default() != false {
return ErrDefaultTrue
}
return nil
}
type ResourceNetworkBandwidth struct {
ResourceBase
}
func (r ResourceNetworkBandwidth) AssertValid() error {
if r.Default() != true {
return ErrDefaultRequired
}
if r.Request() != nil {
return ErrRequestNonEmpty
}
return nil
}
package types
import (
"encoding/json"
"reflect"
"testing"
)
func TestIsolatorUnmarshal(t *testing.T) {
tests := []struct {
msg string
werr bool
}{
{
`{
"name": "os/linux/capabilities-retain-set",
"value": {"set": ["CAP_KILL"]}
}`,
false,
},
{
`{
"name": "os/linux/capabilities-retain-set",
"value": {"set": ["CAP_PONIES"]}
}`,
false,
},
{
`{
"name": "os/linux/capabilities-retain-set",
"value": {"set": []}
}`,
true,
},
{
`{
"name": "os/linux/capabilities-retain-set",
"value": {"set": "CAP_PONIES"}
}`,
true,
},
{
`{
"name": "resource/block-bandwidth",
"value": {"default": true, "limit": "1G"}
}`,
false,
},
{
`{
"name": "resource/block-bandwidth",
"value": {"default": false, "limit": "1G"}
}`,
true,
},
{
`{
"name": "resource/block-bandwidth",
"value": {"request": "30G", "limit": "1G"}
}`,
true,
},
{
`{
"name": "resource/block-iops",
"value": {"default": true, "limit": "1G"}
}`,
false,
},
{
`{
"name": "resource/block-iops",
"value": {"default": false, "limit": "1G"}
}`,
true,
},
{
`{
"name": "resource/block-iops",
"value": {"request": "30G", "limit": "1G"}
}`,
true,
},
{
`{
"name": "resource/cpu",
"value": {"request": "30", "limit": "1"}
}`,
false,
},
{
`{
"name": "resource/memory",
"value": {"request": "1G", "limit": "2Gi"}
}`,
false,
},
{
`{
"name": "resource/memory",
"value": {"default": true, "request": "1G", "limit": "2G"}
}`,
true,
},
{
`{
"name": "resource/network-bandwidth",
"value": {"default": true, "limit": "1G"}
}`,
false,
},
{
`{
"name": "resource/network-bandwidth",
"value": {"default": false, "limit": "1G"}
}`,
true,
},
{
`{
"name": "resource/network-bandwidth",
"value": {"request": "30G", "limit": "1G"}
}`,
true,
},
}
for i, tt := range tests {
var ii Isolator
err := ii.UnmarshalJSON([]byte(tt.msg))
if gerr := (err != nil); gerr != tt.werr {
t.Errorf("#%d: gerr=%t, want %t (err=%v)", i, gerr, tt.werr, err)
}
}
}
func TestIsolatorsGetByName(t *testing.T) {
ex := `
[
{
"name": "resource/cpu",
"value": {"request": "30", "limit": "1"}
},
{
"name": "resource/memory",
"value": {"request": "1G", "limit": "2Gi"}
},
{
"name": "os/linux/capabilities-retain-set",
"value": {"set": ["CAP_KILL"]}
},
{
"name": "os/linux/capabilities-revoke-set",
"value": {"set": ["CAP_KILL"]}
}
]
`
tests := []struct {
name ACName
wlimit int64
wrequest int64
wset []LinuxCapability
}{
{"resource/cpu", 1, 30, nil},
{"resource/memory", 2147483648, 1000000000, nil},
{"os/linux/capabilities-retain-set", 0, 0, []LinuxCapability{"CAP_KILL"}},
{"os/linux/capabilities-revoke-set", 0, 0, []LinuxCapability{"CAP_KILL"}},
}
var is Isolators
err := json.Unmarshal([]byte(ex), &is)
if err != nil {
panic(err)
}
if len(is) < 2 {
t.Fatalf("too few items %v", len(is))
}
for i, tt := range tests {
c := is.GetByName(tt.name)
if c == nil {
t.Fatalf("can't find item %v in %v items", tt.name, len(is))
}
switch v := c.Value().(type) {
case Resource:
var r Resource = v
glimit := r.Limit()
grequest := r.Request()
if glimit.Value() != tt.wlimit || grequest.Value() != tt.wrequest {
t.Errorf("#%d: glimit=%v, want %v, grequest=%v, want %v", i, glimit.Value(), tt.wlimit, grequest.Value(), tt.wrequest)
}
case LinuxCapabilitiesSet:
var s LinuxCapabilitiesSet = v
if !reflect.DeepEqual(s.Set(), tt.wset) {
t.Errorf("#%d: gset=%v, want %v", i, s.Set(), tt.wset)
}
default:
panic("unexecpected type")
}
}
}
func TestIsolatorUnrecognized(t *testing.T) {
msg := `
[{
"name": "resource/network-bandwidth",
"value": {"default": true, "limit": "1G"}
},
{
"name": "resource/network-ponies",
"value": 0
}]`
ex := Isolators{
{Name: "resource/network-ponies"},
}
is := Isolators{}
if err := json.Unmarshal([]byte(msg), &is); err != nil {
t.Fatalf("failed to unmarshal isolators: %v", err)
}
u := is.Unrecognized()
if len(u) != len(ex) {
t.Errorf("unrecognized isolator list is wrong len: want %v, got %v", len(ex), len(u))
}
for i, e := range ex {
if e.Name != u[i].Name {
t.Errorf("unrecognized isolator list mismatch: want %v, got %v", e.Name, u[i].Name)
}
}
}
package types
import (
"encoding/json"
"fmt"
"sort"
)
var ValidOSArch = map[string][]string{
"linux": {"amd64", "i386", "aarch64", "armv7l", "armv7b"},
"freebsd": {"amd64", "i386", "arm"},
"darwin": {"x86_64", "i386"},
}
type Labels []Label
type labels Labels
type Label struct {
Name ACName `json:"name"`
Value string `json:"value"`
}
func (l Labels) assertValid() error {
seen := map[ACName]string{}
for _, lbl := range l {
if lbl.Name == "name" {
return fmt.Errorf(`invalid label name: "name"`)
}
_, ok := seen[lbl.Name]
if ok {
return fmt.Errorf(`duplicate labels of name %q`, lbl.Name)
}
seen[lbl.Name] = lbl.Value
}
if os, ok := seen["os"]; ok {
if validArchs, ok := ValidOSArch[os]; !ok {
// Not a whitelisted OS. TODO: how to warn rather than fail?
validOses := make([]string, 0, len(ValidOSArch))
for validOs := range ValidOSArch {
validOses = append(validOses, validOs)
}
sort.Strings(validOses)
return fmt.Errorf(`bad os %#v (must be one of: %v)`, os, validOses)
} else {
// Whitelisted OS. We check arch here, as arch makes sense only
// when os is defined.
if arch, ok := seen["arch"]; ok {
found := false
for _, validArch := range validArchs {
if arch == validArch {
found = true
break
}
}
if !found {
return fmt.Errorf(`bad arch %#v for %v (must be one of: %v)`, arch, os, validArchs)
}
}
}
}
return nil
}
func (l Labels) MarshalJSON() ([]byte, error) {
if err := l.assertValid(); err != nil {
return nil, err
}
return json.Marshal(labels(l))
}
func (l *Labels) UnmarshalJSON(data []byte) error {
var jl labels
if err := json.Unmarshal(data, &jl); err != nil {
return err
}
nl := Labels(jl)
if err := nl.assertValid(); err != nil {
return err
}
*l = nl
return nil
}
// Get retrieves the value of the label by the given name from Labels, if it exists
func (l Labels) Get(name string) (val string, ok bool) {
for _, lbl := range l {
if lbl.Name.String() == name {
return lbl.Value, true
}
}
return "", false
}
// ToMap creates a map[ACName]string.
func (l Labels) ToMap() map[ACName]string {
labelsMap := make(map[ACName]string)
for _, lbl := range l {
labelsMap[lbl.Name] = lbl.Value
}
return labelsMap
}
// LabelsFromMap creates Labels from a map[ACName]string
func LabelsFromMap(labelsMap map[ACName]string) (Labels, error) {
labels := Labels{}
for n, v := range labelsMap {
labels = append(labels, Label{Name: n, Value: v})
}
if err := labels.assertValid(); err != nil {
return nil, err
}
return labels, nil
}
package types
import (
"encoding/json"
"strings"
"testing"
)
func TestLabels(t *testing.T) {
tests := []struct {
in string
errPrefix string
}{
{
`[{"name": "os", "value": "linux"}, {"name": "arch", "value": "amd64"}]`,
"",
},
{
`[{"name": "os", "value": "linux"}, {"name": "arch", "value": "aarch64"}]`,
"",
},
{
`[{"name": "os", "value": "linux"}, {"name": "arch", "value": "armv7l"}]`,
"",
},
{
`[{"name": "os", "value": "linux"}, {"name": "arch", "value": "armv7b"}]`,
"",
},
{
`[{"name": "os", "value": "freebsd"}, {"name": "arch", "value": "amd64"}]`,
"",
},
{
`[{"name": "os", "value": "OS/360"}, {"name": "arch", "value": "S/360"}]`,
`bad os "OS/360"`,
},
{
`[{"name": "os", "value": "freebsd"}, {"name": "arch", "value": "armv7b"}]`,
`bad arch "armv7b" for freebsd`,
},
{
`[{"name": "os", "value": "linux"}, {"name": "arch", "value": "arm"}]`,
`bad arch "arm" for linux`,
},
{
`[{"name": "name"}]`,
`invalid label name: "name"`,
},
{
`[{"name": "os", "value": "linux"}, {"name": "os", "value": "freebsd"}]`,
`duplicate labels of name "os"`,
},
{
`[{"name": "arch", "value": "amd64"}, {"name": "os", "value": "freebsd"}, {"name": "arch", "value": "x86_64"}]`,
`duplicate labels of name "arch"`,
},
{
`[]`,
"",
},
}
for i, tt := range tests {
var l Labels
if err := json.Unmarshal([]byte(tt.in), &l); err != nil {
if tt.errPrefix == "" {
t.Errorf("#%d: got err=%v, expected no error", i, err)
} else if !strings.HasPrefix(err.Error(), tt.errPrefix) {
t.Errorf("#%d: got err=%v, expected prefix %#v", i, err, tt.errPrefix)
}
} else {
t.Log(l)
if tt.errPrefix != "" {
t.Errorf("#%d: got no err, expected prefix %#v", i, tt.errPrefix)
}
}
}
}
package types
import (
"errors"
"fmt"
"net/url"
"strconv"
"strings"
)
type MountPoint struct {
Name ACName `json:"name"`
Path string `json:"path"`
ReadOnly bool `json:"readOnly,omitempty"`
}
func (mount MountPoint) assertValid() error {
if mount.Name.Empty() {
return errors.New("name must be set")
}
if len(mount.Path) == 0 {
return errors.New("path must be set")
}
return nil
}
// MountPointFromString takes a command line mountpoint parameter and returns a mountpoint
//
// It is useful for actool patch-manifest --mounts
//
// Example mountpoint parameters:
// database,path=/tmp,readOnly=true
func MountPointFromString(mp string) (*MountPoint, error) {
var mount MountPoint
mp = "name=" + mp
v, err := url.ParseQuery(strings.Replace(mp, ",", "&", -1))
if err != nil {
return nil, err
}
for key, val := range v {
if len(val) > 1 {
return nil, fmt.Errorf("label %s with multiple values %q", key, val)
}
// TOOD(philips): make this less hardcoded
switch key {
case "name":
acn, err := NewACName(val[0])
if err != nil {
return nil, err
}
mount.Name = *acn
case "path":
mount.Path = val[0]
case "readOnly":
ro, err := strconv.ParseBool(val[0])
if err != nil {
return nil, err
}
mount.ReadOnly = ro
default:
return nil, fmt.Errorf("unknown mountpoint parameter %q", key)
}
}
err = mount.assertValid()
if err != nil {
return nil, err
}
return &mount, nil
}
package types
import (
"reflect"
"testing"
)
func TestMountPointFromString(t *testing.T) {
tests := []struct {
s string
mount MountPoint
}{
{
"foobar,path=/tmp",
MountPoint{
Name: "foobar",
Path: "/tmp",
ReadOnly: false,
},
},
{
"foobar,path=/tmp,readOnly=false",
MountPoint{
Name: "foobar",
Path: "/tmp",
ReadOnly: false,
},
},
{
"foobar,path=/tmp,readOnly=true",
MountPoint{
Name: "foobar",
Path: "/tmp",
ReadOnly: true,
},
},
}
for i, tt := range tests {
mount, err := MountPointFromString(tt.s)
if err != nil {
t.Errorf("#%d: got err=%v, want nil", i, err)
}
if !reflect.DeepEqual(*mount, tt.mount) {
t.Errorf("#%d: mount=%v, want %v", i, *mount, tt.mount)
}
}
}
func TestMountPointFromStringBad(t *testing.T) {
tests := []string{
"#foobar,path=/tmp",
"foobar,path=/tmp,readOnly=true,asdf=asdf",
"foobar,path=/tmp,readOnly=maybe",
"foobar,path=/tmp,readOnly=",
"foobar,path=",
"foobar",
"",
",path=/",
}
for i, in := range tests {
l, err := MountPointFromString(in)
if l != nil {
t.Errorf("#%d: got l=%v, want nil", i, l)
}
if err == nil {
t.Errorf("#%d: got err=nil, want non-nil", i)
}
}
}
package types
type Port struct {
Name ACName `json:"name"`
Protocol string `json:"protocol"`
Port uint `json:"port"`
SocketActivated bool `json:"socketActivated"`
}
type ExposedPort struct {
Name ACName `json:"name"`
HostPort uint `json:"hostPort"`
}
package types
import (
"encoding/json"
"github.com/coreos/go-semver/semver"
)
var (
ErrNoZeroSemVer = ACVersionError("SemVer cannot be zero")
ErrBadSemVer = ACVersionError("SemVer is bad")
)
// SemVer implements the Unmarshaler interface to define a field that must be
// a semantic version string
// TODO(jonboulle): extend upstream instead of wrapping?
type SemVer semver.Version
// NewSemVer generates a new SemVer from a string. If the given string does
// not represent a valid SemVer, nil and an error are returned.
func NewSemVer(s string) (*SemVer, error) {
nsv, err := semver.NewVersion(s)
if err != nil {
return nil, ErrBadSemVer
}
v := SemVer(*nsv)
if v.Empty() {
return nil, ErrNoZeroSemVer
}
return &v, nil
}
func (sv SemVer) String() string {
s := semver.Version(sv)
return s.String()
}
func (sv SemVer) Empty() bool {
return semver.Version(sv) == semver.Version{}
}
// UnmarshalJSON implements the json.Unmarshaler interface
func (sv *SemVer) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
v, err := NewSemVer(s)
if err != nil {
return err
}
*sv = *v
return nil
}
// MarshalJSON implements the json.Marshaler interface
func (sv SemVer) MarshalJSON() ([]byte, error) {
if sv.Empty() {
return nil, ErrNoZeroSemVer
}
return json.Marshal(sv.String())
}
package types
import (
"encoding/json"
"reflect"
"testing"
"github.com/coreos/go-semver/semver"
)
func TestMarshalSemver(t *testing.T) {
tests := []struct {
sv SemVer
wd []byte
}{
{
SemVer(semver.Version{Major: 1}),
[]byte(`"1.0.0"`),
},
{
SemVer(semver.Version{Major: 3, Minor: 2, Patch: 1}),
[]byte(`"3.2.1"`),
},
{
SemVer(semver.Version{Major: 3, Minor: 2, Patch: 1, PreRelease: "foo"}),
[]byte(`"3.2.1-foo"`),
},
{
SemVer(semver.Version{Major: 1, Minor: 2, Patch: 3, PreRelease: "alpha", Metadata: "git"}),
[]byte(`"1.2.3-alpha+git"`),
},
}
for i, tt := range tests {
d, err := json.Marshal(tt.sv)
if !reflect.DeepEqual(d, tt.wd) {
t.Errorf("#%d: d=%v, want %v", i, string(d), string(tt.wd))
}
if err != nil {
t.Errorf("#%d: err=%v, want nil", i, err)
}
}
}
func TestUnmarshalSemver(t *testing.T) {
tests := []struct {
d []byte
wsv SemVer
werr bool
}{
{
[]byte(`"1.0.0"`),
SemVer(semver.Version{Major: 1}),
false,
},
{
[]byte(`"3.2.1"`),
SemVer(semver.Version{Major: 3, Minor: 2, Patch: 1}),
false,
},
{
[]byte(`"3.2.1-foo"`),
SemVer(semver.Version{Major: 3, Minor: 2, Patch: 1, PreRelease: "foo"}),
false,
},
{
[]byte(`"1.2.3-alpha+git"`),
SemVer(semver.Version{Major: 1, Minor: 2, Patch: 3, PreRelease: "alpha", Metadata: "git"}),
false,
},
{
[]byte(`"1"`),
SemVer{},
true,
},
{
[]byte(`"1.2.3.4"`),
SemVer{},
true,
},
{
[]byte(`1.2.3`),
SemVer{},
true,
},
{
[]byte(`"v1.2.3"`),
SemVer{},
true,
},
}
for i, tt := range tests {
var sv SemVer
err := json.Unmarshal(tt.d, &sv)
if !reflect.DeepEqual(sv, tt.wsv) {
t.Errorf("#%d: semver=%#v, want %#v", i, sv, tt.wsv)
}
if gerr := (err != nil); gerr != tt.werr {
t.Errorf("#%d: err==%v, want errstate %t", i, err, tt.werr)
}
}
}
package types
import (
"encoding/json"
"fmt"
"net/url"
)
// URL wraps url.URL to marshal/unmarshal to/from JSON strings and enforce
// that the scheme is HTTP/HTTPS only
type URL url.URL
func NewURL(s string) (*URL, error) {
uu, err := url.Parse(s)
if err != nil {
return nil, fmt.Errorf("bad URL: %v", err)
}
nu := URL(*uu)
if err := nu.assertValidScheme(); err != nil {
return nil, err
}
return &nu, nil
}
func (u URL) String() string {
uu := url.URL(u)
return uu.String()
}
func (u URL) assertValidScheme() error {
switch u.Scheme {
case "http", "https":
return nil
default:
return fmt.Errorf("bad URL scheme, must be http/https")
}
}
func (u *URL) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
nu, err := NewURL(s)
if err != nil {
return err
}
*u = *nu
return nil
}
func (u URL) MarshalJSON() ([]byte, error) {
if err := u.assertValidScheme(); err != nil {
return nil, err
}
return json.Marshal(u.String())
}
package types
import (
"encoding/json"
"net/url"
"reflect"
"testing"
)
func mustParseURL(t *testing.T, s string) url.URL {
u, err := url.Parse(s)
if err != nil {
t.Fatalf("error parsing URL: %v", err)
}
return *u
}
func TestMarshalURL(t *testing.T) {
tests := []struct {
u url.URL
w string
}{
{
mustParseURL(t, "http://foo.com"),
`"http://foo.com"`,
},
{
mustParseURL(t, "http://foo.com/huh/what?is=this"),
`"http://foo.com/huh/what?is=this"`,
},
{
mustParseURL(t, "https://example.com/bar"),
`"https://example.com/bar"`,
},
}
for i, tt := range tests {
u := URL(tt.u)
b, err := json.Marshal(u)
if g := string(b); g != tt.w {
t.Errorf("#%d: got %q, want %q", i, g, tt.w)
}
if err != nil {
t.Errorf("#%d: err=%v, want nil", i, err)
}
}
}
func TestMarshalURLBad(t *testing.T) {
tests := []url.URL{
mustParseURL(t, "ftp://foo.com"),
mustParseURL(t, "unix:///hello"),
}
for i, tt := range tests {
u := URL(tt)
b, err := json.Marshal(u)
if b != nil {
t.Errorf("#%d: got %v, want nil", i, b)
}
if err == nil {
t.Errorf("#%d: got unexpected err=nil", i)
}
}
}
func TestUnmarshalURL(t *testing.T) {
tests := []struct {
in string
w URL
}{
{
`"http://foo.com"`,
URL(mustParseURL(t, "http://foo.com")),
},
{
`"http://yis.com/hello?goodbye=yes"`,
URL(mustParseURL(t, "http://yis.com/hello?goodbye=yes")),
},
{
`"https://ohai.net"`,
URL(mustParseURL(t, "https://ohai.net")),
},
}
for i, tt := range tests {
var g URL
err := json.Unmarshal([]byte(tt.in), &g)
if err != nil {
t.Errorf("#%d: want err=nil, got %v", i, err)
}
if !reflect.DeepEqual(g, tt.w) {
t.Errorf("#%d: got url=%v, want %v", i, g, tt.w)
}
}
}
func TestUnmarshalURLBad(t *testing.T) {
var empty = URL{}
tests := []string{
"badjson",
"http://google.com",
`"ftp://example.com"`,
`"unix://file.net"`,
`"not a url"`,
}
for i, tt := range tests {
var g URL
err := json.Unmarshal([]byte(tt), &g)
if err == nil {
t.Errorf("#%d: want err, got nil", i)
}
if !reflect.DeepEqual(g, empty) {
t.Errorf("#%d: got %v, want %v", i, g, empty)
}
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment