Merge pull request #7410 from yifan-gu/rkt_godep

Godep: Add godep for rkt.
This commit is contained in:
Victor Marmol 2015-04-28 08:04:38 -07:00
commit 17e612324e
379 changed files with 109317 additions and 0 deletions

116
Godeps/Godeps.json generated
View File

@ -48,30 +48,130 @@
"ImportPath": "github.com/abbot/go-http-auth",
"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",
"Rev": "b965b613227fddccbfffe13eae360ed3fa822f8d"
},
{
"ImportPath": "github.com/camlistore/lock",
"Rev": "ae27720f340952636b826119b58130b9c1a847a0"
},
{
"ImportPath": "github.com/codegangsta/negroni",
"Comment": "v0.1-62-g8d75e11",
"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",
"Comment": "v2.0.0-3-g0424b5f",
"Rev": "0424b5f86ef0ca57a5309c599f74bbb3e97ecd9d"
},
{
"ImportPath": "github.com/coreos/go-semver/semver",
"Rev": "6fe83ccda8fb9b7549c9ab4ba47f47858bc950aa"
},
{
"ImportPath": "github.com/coreos/go-systemd/dbus",
"Comment": "v2-27-g97e243d",
"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",
"Comment": "v1.0.2-5-g2831f11",
"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",
"Rev": "3e6e67c4dcea3ac2f25fd4731abc0e1deaf36216"
@ -331,6 +431,14 @@
"Comment": "v1.0-28-g8adf9e1730c5",
"Rev": "8adf9e1730c55cdc590de7d49766cb2acc88d8f2"
},
{
"ImportPath": "github.com/petar/GoLLRB/llrb",
"Rev": "53be0d36a84c2a886ca057d34b6aa4468df9ccb4"
},
{
"ImportPath": "github.com/peterbourgon/diskv",
"Rev": "508f5671a72eeaef05cf8c24abe7fbc1c07faf69"
},
{
"ImportPath": "github.com/prometheus/client_golang/model",
"Comment": "0.4.0-1-g692492e",
@ -411,6 +519,14 @@
"Comment": "v1.0-13-g5292687",
"Rev": "5292687f5379e01054407da44d7c4590a61fd3de"
},
{
"ImportPath": "golang.org/x/crypto/cast5",
"Rev": "a7ead6ddf06233883deca151dffaef2effbf498f"
},
{
"ImportPath": "golang.org/x/crypto/openpgp",
"Rev": "a7ead6ddf06233883deca151dffaef2effbf498f"
},
{
"ImportPath": "golang.org/x/net/context",
"Rev": "cbcac7bb8415db9b6cb4d1ebab1dc9afbd688b97"

View File

@ -0,0 +1,81 @@
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
}
}

View File

@ -0,0 +1,2 @@
// Package aci contains various functions for working with App Container Images.
package aci

194
Godeps/_workspace/src/github.com/appc/spec/aci/file.go generated vendored Normal file
View File

@ -0,0 +1,194 @@
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
}

View File

@ -0,0 +1,136 @@
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)
}
}

View File

@ -0,0 +1,159 @@
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
}

View File

@ -0,0 +1,62 @@
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)
}
}

View File

@ -0,0 +1,84 @@
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()
}

View File

@ -0,0 +1,223 @@
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
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,74 @@
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
}

View File

@ -0,0 +1,91 @@
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)
}

View File

@ -0,0 +1,3 @@
// Package tarheader contains a simple abstraction to accurately create
// tar.Headers on different operating systems.
package tarheader

View File

@ -0,0 +1,25 @@
//+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
}

View File

@ -0,0 +1,23 @@
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
}

View File

@ -0,0 +1,51 @@
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
}
}

View File

@ -0,0 +1,61 @@
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)
}
}

View File

@ -0,0 +1,14 @@
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)
}
}

View File

@ -0,0 +1,11 @@
// 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

View File

@ -0,0 +1,81 @@
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)
}

View File

@ -0,0 +1,48 @@
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)
}
}

View File

@ -0,0 +1,28 @@
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))
}

View File

@ -0,0 +1,146 @@
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"`
}

View File

@ -0,0 +1,59 @@
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")
}
}

View File

@ -0,0 +1,53 @@
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
}

View File

@ -0,0 +1,79 @@
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)
}
}
}

View File

@ -0,0 +1,131 @@
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
}

View File

@ -0,0 +1,252 @@
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)
}
}
}

View File

@ -0,0 +1,92 @@
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)
}

View File

@ -0,0 +1,217 @@
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)
}
}

View File

@ -0,0 +1,75 @@
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
}

View File

@ -0,0 +1,216 @@
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)
}
}
}

View File

@ -0,0 +1,46 @@
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())
}

View File

@ -0,0 +1,66 @@
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)
}
}
}

View File

@ -0,0 +1,43 @@
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
}

View File

@ -0,0 +1,26 @@
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)
}
}

View File

@ -0,0 +1,4 @@
// 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

View File

@ -0,0 +1,96 @@
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)
}

View File

@ -0,0 +1,62 @@
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)
}
}
}

View File

@ -0,0 +1,28 @@
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)
}

View File

@ -0,0 +1,47 @@
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
}

View File

@ -0,0 +1,42 @@
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
}

View File

@ -0,0 +1,28 @@
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)
}
}
}

View File

@ -0,0 +1,104 @@
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
}

View File

@ -0,0 +1,82 @@
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)
}
}
}

View File

@ -0,0 +1,89 @@
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
}

View File

@ -0,0 +1,69 @@
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
}

View File

@ -0,0 +1,144 @@
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
}

View File

@ -0,0 +1,236 @@
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)
}
}
}

View File

@ -0,0 +1,114 @@
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
}

View File

@ -0,0 +1,78 @@
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)
}
}
}
}

View File

@ -0,0 +1,72 @@
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
}

View File

@ -0,0 +1,69 @@
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)
}
}
}

View File

@ -0,0 +1,13 @@
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"`
}

View File

@ -0,0 +1,62 @@
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())
}

View File

@ -0,0 +1,115 @@
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)
}
}
}

View File

@ -0,0 +1,57 @@
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())
}

View File

@ -0,0 +1,123 @@
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)
}
}
}

View File

@ -0,0 +1,78 @@
package types
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
)
var (
ErrNoEmptyUUID = errors.New("UUID cannot be empty")
)
// UUID encodes an RFC4122-compliant UUID, marshaled to/from a string
// TODO(jonboulle): vendor a package for this?
// TODO(jonboulle): consider more flexibility in input string formats.
// Right now, we only accept:
// "6733C088-A507-4694-AABF-EDBE4FC5266F"
// "6733C088A5074694AABFEDBE4FC5266F"
type UUID [16]byte
func (u UUID) String() string {
return fmt.Sprintf("%x-%x-%x-%x-%x", u[0:4], u[4:6], u[6:8], u[8:10], u[10:16])
}
func (u *UUID) Set(s string) error {
nu, err := NewUUID(s)
if err == nil {
*u = *nu
}
return err
}
// NewUUID generates a new UUID from the given string. If the string does not
// represent a valid UUID, nil and an error are returned.
func NewUUID(s string) (*UUID, error) {
s = strings.Replace(s, "-", "", -1)
if len(s) != 32 {
return nil, errors.New("bad UUID length != 32")
}
dec, err := hex.DecodeString(s)
if err != nil {
return nil, err
}
var u UUID
for i, b := range dec {
u[i] = b
}
return &u, nil
}
func (u UUID) Empty() bool {
return reflect.DeepEqual(u, UUID{})
}
func (u *UUID) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
uu, err := NewUUID(s)
if uu.Empty() {
return ErrNoEmptyUUID
}
if err == nil {
*u = *uu
}
return err
}
func (u UUID) MarshalJSON() ([]byte, error) {
if u.Empty() {
return nil, ErrNoEmptyUUID
}
return json.Marshal(u.String())
}

View File

@ -0,0 +1,59 @@
package types
import "testing"
func TestNewUUID(t *testing.T) {
tests := []struct {
in string
ws string
}{
{
"6733C088-A507-4694-AABF-EDBE4FC5266F",
"6733c088-a507-4694-aabf-edbe4fc5266f",
},
{
"6733C088A5074694AABFEDBE4FC5266F",
"6733c088-a507-4694-aabf-edbe4fc5266f",
},
{
"0aaf0a79-1a39-4d59-abbf-1bebca8209d2",
"0aaf0a79-1a39-4d59-abbf-1bebca8209d2",
},
{
"0aaf0a791a394d59abbf1bebca8209d2",
"0aaf0a79-1a39-4d59-abbf-1bebca8209d2",
},
}
for i, tt := range tests {
gu, err := NewUUID(tt.in)
if err != nil {
t.Errorf("#%d: err=%v, want %v", i, err, nil)
}
if gs := gu.String(); gs != tt.ws {
t.Errorf("#%d: String()=%v, want %v", i, gs, tt.ws)
}
}
}
func TestNewUUIDBad(t *testing.T) {
tests := []string{
"asdf",
"0AAF0A79-1A39-4D59-ABBF-1BEBCA8209D2ABC",
"",
}
for i, tt := range tests {
g, err := NewUUID(tt)
if err == nil {
t.Errorf("#%d: err=nil, want non-nil", i)
}
if g != nil {
t.Errorf("#%d: err=%v, want %v", i, g, nil)
}
}
}

View File

@ -0,0 +1,124 @@
package types
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"path/filepath"
"strconv"
"strings"
)
// Volume encapsulates a volume which should be mounted into the filesystem
// of all apps in a PodManifest
type Volume struct {
Name ACName `json:"name"`
Kind string `json:"kind"`
// currently used only by "host"
// TODO(jonboulle): factor out?
Source string `json:"source,omitempty"`
ReadOnly *bool `json:"readOnly,omitempty"`
}
type volume Volume
func (v Volume) assertValid() error {
if v.Name.Empty() {
return errors.New("name must be set")
}
switch v.Kind {
case "empty":
if v.Source != "" {
return errors.New("source for empty volume must be empty")
}
return nil
case "host":
if v.Source == "" {
return errors.New("source for host volume cannot be empty")
}
if !filepath.IsAbs(v.Source) {
return errors.New("source for host volume must be absolute path")
}
return nil
default:
return errors.New(`unrecognized volume kind: should be one of "empty", "host"`)
}
}
func (v *Volume) UnmarshalJSON(data []byte) error {
var vv volume
if err := json.Unmarshal(data, &vv); err != nil {
return err
}
nv := Volume(vv)
if err := nv.assertValid(); err != nil {
return err
}
*v = nv
return nil
}
func (v Volume) MarshalJSON() ([]byte, error) {
if err := v.assertValid(); err != nil {
return nil, err
}
return json.Marshal(volume(v))
}
func (v Volume) String() string {
s := fmt.Sprintf("%s,kind=%s,readOnly=%t", v.Name, v.Kind, *v.ReadOnly)
if v.Source != "" {
s = s + fmt.Sprintf("source=%s", v.Source)
}
return s
}
// VolumeFromString takes a command line volume parameter and returns a volume
//
// Example volume parameters:
// database,kind=host,source=/tmp,readOnly=true
func VolumeFromString(vp string) (*Volume, error) {
var vol Volume
vp = "name=" + vp
v, err := url.ParseQuery(strings.Replace(vp, ",", "&", -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
}
vol.Name = *acn
case "kind":
vol.Kind = val[0]
case "source":
vol.Source = val[0]
case "readOnly":
ro, err := strconv.ParseBool(val[0])
if err != nil {
return nil, err
}
vol.ReadOnly = &ro
default:
return nil, fmt.Errorf("unknown volume parameter %q", key)
}
}
err = vol.assertValid()
if err != nil {
return nil, err
}
return &vol, nil
}

View File

@ -0,0 +1,85 @@
package types
import (
"reflect"
"testing"
)
func TestVolumeFromString(t *testing.T) {
trueVar := true
falseVar := false
tests := []struct {
s string
v Volume
}{
{
"foobar,kind=host,source=/tmp",
Volume{
Name: "foobar",
Kind: "host",
Source: "/tmp",
ReadOnly: nil,
},
},
{
"foobar,kind=host,source=/tmp,readOnly=false",
Volume{
Name: "foobar",
Kind: "host",
Source: "/tmp",
ReadOnly: &falseVar,
},
},
{
"foobar,kind=host,source=/tmp,readOnly=true",
Volume{
Name: "foobar",
Kind: "host",
Source: "/tmp",
ReadOnly: &trueVar,
},
},
{
"foobar,kind=empty",
Volume{
Name: "foobar",
Kind: "empty",
ReadOnly: nil,
},
},
{
"foobar,kind=empty,readOnly=true",
Volume{
Name: "foobar",
Kind: "empty",
ReadOnly: &trueVar,
},
},
}
for i, tt := range tests {
v, err := VolumeFromString(tt.s)
if err != nil {
t.Errorf("#%d: got err=%v, want nil", i, err)
}
if !reflect.DeepEqual(*v, tt.v) {
t.Errorf("#%d: v=%v, want %v", i, *v, tt.v)
}
}
}
func TestVolumeFromStringBad(t *testing.T) {
tests := []string{
"#foobar,kind=host,source=/tmp",
"foobar,kind=host,source=/tmp,readOnly=true,asdf=asdf",
"foobar,kind=empty,source=/tmp",
}
for i, in := range tests {
l, err := VolumeFromString(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)
}
}
}

View File

@ -0,0 +1,25 @@
package schema
import (
"github.com/appc/spec/schema/types"
)
const (
// version represents the canonical version of the appc spec and tooling.
// For now, the schema and tooling is coupled with the spec itself, so
// this must be kept in sync with the VERSION file in the root of the repo.
version string = "0.5.1+git"
)
var (
// AppContainerVersion is the SemVer representation of version
AppContainerVersion types.SemVer
)
func init() {
v, err := types.NewSemVer(version)
if err != nil {
panic(err)
}
AppContainerVersion = *v
}

View File

@ -0,0 +1 @@
*~

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,3 @@
File locking library.
See http://godoc.org/github.com/camlistore/lock

View File

@ -0,0 +1,158 @@
/*
Copyright 2013 The Go Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package lock
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"sync"
)
// Lock locks the given file, creating the file if necessary. If the
// file already exists, it must have zero size or an error is returned.
// The lock is an exclusive lock (a write lock), but locked files
// should neither be read from nor written to. Such files should have
// zero size and only exist to co-ordinate ownership across processes.
//
// A nil Closer is returned if an error occurred. Otherwise, close that
// Closer to release the lock.
//
// On Linux, FreeBSD and OSX, a lock has the same semantics as fcntl(2)'s
// advisory locks. In particular, closing any other file descriptor for the
// same file will release the lock prematurely.
//
// Attempting to lock a file that is already locked by the current process
// has undefined behavior.
//
// On other operating systems, lock will fallback to using the presence and
// content of a file named name + '.lock' to implement locking behavior.
func Lock(name string) (io.Closer, error) {
return lockFn(name)
}
var lockFn = lockPortable
// Portable version not using fcntl. Doesn't handle crashes as gracefully,
// since it can leave stale lock files.
// TODO: write pid of owner to lock file and on race see if pid is
// still alive?
func lockPortable(name string) (io.Closer, error) {
absName, err := filepath.Abs(name)
if err != nil {
return nil, fmt.Errorf("can't Lock file %q: can't find abs path: %v", name, err)
}
fi, err := os.Stat(absName)
if err == nil && fi.Size() > 0 {
if isStaleLock(absName) {
os.Remove(absName)
} else {
return nil, fmt.Errorf("can't Lock file %q: has non-zero size", name)
}
}
f, err := os.OpenFile(absName, os.O_RDWR|os.O_CREATE|os.O_TRUNC|os.O_EXCL, 0666)
if err != nil {
return nil, fmt.Errorf("failed to create lock file %s %v", absName, err)
}
if err := json.NewEncoder(f).Encode(&pidLockMeta{OwnerPID: os.Getpid()}); err != nil {
return nil, err
}
return &lockCloser{f: f, abs: absName}, nil
}
type pidLockMeta struct {
OwnerPID int
}
func isStaleLock(path string) bool {
f, err := os.Open(path)
if err != nil {
return false
}
defer f.Close()
var meta pidLockMeta
if json.NewDecoder(f).Decode(&meta) != nil {
return false
}
if meta.OwnerPID == 0 {
return false
}
p, err := os.FindProcess(meta.OwnerPID)
if err != nil {
// e.g. on Windows
return true
}
// On unix, os.FindProcess always is true, so we have to send
// it a signal to see if it's alive.
if signalZero != nil {
if p.Signal(signalZero) != nil {
return true
}
}
return false
}
var signalZero os.Signal // nil or set by lock_sigzero.go
type lockCloser struct {
f *os.File
abs string
once sync.Once
err error
}
func (lc *lockCloser) Close() error {
lc.once.Do(lc.close)
return lc.err
}
func (lc *lockCloser) close() {
if err := lc.f.Close(); err != nil {
lc.err = err
}
if err := os.Remove(lc.abs); err != nil {
lc.err = err
}
}
var (
lockmu sync.Mutex
locked = map[string]bool{} // abs path -> true
)
// unlocker is used by the darwin and linux implementations with fcntl
// advisory locks.
type unlocker struct {
f *os.File
abs string
}
func (u *unlocker) Close() error {
lockmu.Lock()
// Remove is not necessary but it's nice for us to clean up.
// If we do do this, though, it needs to be before the
// u.f.Close below.
os.Remove(u.abs)
if err := u.f.Close(); err != nil {
return err
}
delete(locked, u.abs)
lockmu.Unlock()
return nil
}

View File

@ -0,0 +1,32 @@
// +build appengine
/*
Copyright 2013 The Go Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package lock
import (
"errors"
"io"
)
func init() {
lockFn = lockAppEngine
}
func lockAppEngine(name string) (io.Closer, error) {
return nil, errors.New("Lock not available on App Engine")
}

View File

@ -0,0 +1,80 @@
// +build darwin,amd64
// +build !appengine
/*
Copyright 2013 The Go Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package lock
import (
"fmt"
"io"
"os"
"path/filepath"
"syscall"
"unsafe"
)
func init() {
lockFn = lockFcntl
}
func lockFcntl(name string) (io.Closer, error) {
abs, err := filepath.Abs(name)
if err != nil {
return nil, err
}
lockmu.Lock()
if locked[abs] {
lockmu.Unlock()
return nil, fmt.Errorf("file %q already locked", abs)
}
locked[abs] = true
lockmu.Unlock()
fi, err := os.Stat(name)
if err == nil && fi.Size() > 0 {
return nil, fmt.Errorf("can't Lock file %q: has non-zero size", name)
}
f, err := os.Create(name)
if err != nil {
return nil, fmt.Errorf("Lock Create of %s (abs: %s) failed: %v", name, abs, err)
}
// This type matches C's "struct flock" defined in /usr/include/sys/fcntl.h.
// TODO: move this into the standard syscall package.
k := struct {
Start uint64 // sizeof(off_t): 8
Len uint64 // sizeof(off_t): 8
Pid uint32 // sizeof(pid_t): 4
Type uint16 // sizeof(short): 2
Whence uint16 // sizeof(short): 2
}{
Type: syscall.F_WRLCK,
Whence: uint16(os.SEEK_SET),
Start: 0,
Len: 0, // 0 means to lock the entire file.
Pid: uint32(os.Getpid()),
}
_, _, errno := syscall.Syscall(syscall.SYS_FCNTL, f.Fd(), uintptr(syscall.F_SETLK), uintptr(unsafe.Pointer(&k)))
if errno != 0 {
f.Close()
return nil, errno
}
return &unlocker{f, abs}, nil
}

View File

@ -0,0 +1,79 @@
/*
Copyright 2013 The Go Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package lock
import (
"fmt"
"io"
"os"
"path/filepath"
"syscall"
"unsafe"
)
func init() {
lockFn = lockFcntl
}
func lockFcntl(name string) (io.Closer, error) {
abs, err := filepath.Abs(name)
if err != nil {
return nil, err
}
lockmu.Lock()
if locked[abs] {
lockmu.Unlock()
return nil, fmt.Errorf("file %q already locked", abs)
}
locked[abs] = true
lockmu.Unlock()
fi, err := os.Stat(name)
if err == nil && fi.Size() > 0 {
return nil, fmt.Errorf("can't Lock file %q: has non-zero size", name)
}
f, err := os.Create(name)
if err != nil {
return nil, err
}
// This type matches C's "struct flock" defined in /usr/include/fcntl.h.
// TODO: move this into the standard syscall package.
k := struct {
Start int64 /* off_t starting offset */
Len int64 /* off_t len = 0 means until end of file */
Pid int32 /* pid_t lock owner */
Type int16 /* short lock type: read/write, etc. */
Whence int16 /* short type of l_start */
Sysid int32 /* int remote system id or zero for local */
}{
Start: 0,
Len: 0, // 0 means to lock the entire file.
Pid: int32(os.Getpid()),
Type: syscall.F_WRLCK,
Whence: int16(os.SEEK_SET),
Sysid: 0,
}
_, _, errno := syscall.Syscall(syscall.SYS_FCNTL, f.Fd(), uintptr(syscall.F_SETLK), uintptr(unsafe.Pointer(&k)))
if errno != 0 {
f.Close()
return nil, errno
}
return &unlocker{f, abs}, nil
}

View File

@ -0,0 +1,80 @@
// +build linux,amd64
// +build !appengine
/*
Copyright 2013 The Go Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package lock
import (
"fmt"
"io"
"os"
"path/filepath"
"syscall"
"unsafe"
)
func init() {
lockFn = lockFcntl
}
func lockFcntl(name string) (io.Closer, error) {
abs, err := filepath.Abs(name)
if err != nil {
return nil, err
}
lockmu.Lock()
if locked[abs] {
lockmu.Unlock()
return nil, fmt.Errorf("file %q already locked", abs)
}
locked[abs] = true
lockmu.Unlock()
fi, err := os.Stat(name)
if err == nil && fi.Size() > 0 {
return nil, fmt.Errorf("can't Lock file %q: has non-zero size", name)
}
f, err := os.Create(name)
if err != nil {
return nil, err
}
// This type matches C's "struct flock" defined in /usr/include/bits/fcntl.h.
// TODO: move this into the standard syscall package.
k := struct {
Type uint32
Whence uint32
Start uint64
Len uint64
Pid uint32
}{
Type: syscall.F_WRLCK,
Whence: uint32(os.SEEK_SET),
Start: 0,
Len: 0, // 0 means to lock the entire file.
Pid: uint32(os.Getpid()),
}
_, _, errno := syscall.Syscall(syscall.SYS_FCNTL, f.Fd(), uintptr(syscall.F_SETLK), uintptr(unsafe.Pointer(&k)))
if errno != 0 {
f.Close()
return nil, errno
}
return &unlocker{f, abs}, nil
}

View File

@ -0,0 +1,81 @@
// +build linux,arm
// +build !appengine
/*
Copyright 2013 The Go Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package lock
import (
"fmt"
"io"
"os"
"path/filepath"
"syscall"
"unsafe"
)
func init() {
lockFn = lockFcntl
}
func lockFcntl(name string) (io.Closer, error) {
abs, err := filepath.Abs(name)
if err != nil {
return nil, err
}
lockmu.Lock()
if locked[abs] {
lockmu.Unlock()
return nil, fmt.Errorf("file %q already locked", abs)
}
locked[abs] = true
lockmu.Unlock()
fi, err := os.Stat(name)
if err == nil && fi.Size() > 0 {
return nil, fmt.Errorf("can't Lock file %q: has non-zero size", name)
}
f, err := os.Create(name)
if err != nil {
return nil, err
}
// This type matches C's "struct flock" defined in /usr/include/bits/fcntl.h.
// TODO: move this into the standard syscall package.
k := struct {
Type uint16
Whence uint16
Start uint32
Len uint32
Pid uint32
}{
Type: syscall.F_WRLCK,
Whence: uint16(os.SEEK_SET),
Start: 0,
Len: 0, // 0 means to lock the entire file.
Pid: uint32(os.Getpid()),
}
const F_SETLK = 6 // actual value. syscall package is wrong: golang.org/issue/7059
_, _, errno := syscall.Syscall(syscall.SYS_FCNTL, f.Fd(), uintptr(F_SETLK), uintptr(unsafe.Pointer(&k)))
if errno != 0 {
f.Close()
return nil, errno
}
return &unlocker{f, abs}, nil
}

View File

@ -0,0 +1,55 @@
/*
Copyright 2013 The Go Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package lock
import (
"fmt"
"io"
"os"
"path/filepath"
)
func init() {
lockFn = lockPlan9
}
func lockPlan9(name string) (io.Closer, error) {
var f *os.File
abs, err := filepath.Abs(name)
if err != nil {
return nil, err
}
lockmu.Lock()
if locked[abs] {
lockmu.Unlock()
return nil, fmt.Errorf("file %q already locked", abs)
}
locked[abs] = true
lockmu.Unlock()
fi, err := os.Stat(name)
if err == nil && fi.Size() > 0 {
return nil, fmt.Errorf("can't Lock file %q: has non-zero size", name)
}
f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE, os.ModeExclusive|0644)
if err != nil {
return nil, fmt.Errorf("Lock Create of %s (abs: %s) failed: %v", name, abs, err)
}
return &unlocker{f, abs}, nil
}

View File

@ -0,0 +1,26 @@
// +build !appengine
// +build linux darwin freebsd openbsd netbsd dragonfly
/*
Copyright 2013 The Go Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package lock
import "syscall"
func init() {
signalZero = syscall.Signal(0)
}

View File

@ -0,0 +1,131 @@
/*
Copyright 2013 The Go Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package lock
import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
"testing"
)
func TestLock(t *testing.T) {
testLock(t, false)
}
func TestLockPortable(t *testing.T) {
testLock(t, true)
}
func TestLockInChild(t *testing.T) {
f := os.Getenv("TEST_LOCK_FILE")
if f == "" {
// not child
return
}
lock := Lock
if v, _ := strconv.ParseBool(os.Getenv("TEST_LOCK_PORTABLE")); v {
lock = lockPortable
}
lk, err := lock(f)
if err != nil {
log.Fatalf("Lock failed: %v", err)
}
if v, _ := strconv.ParseBool(os.Getenv("TEST_LOCK_CRASH")); v {
// Simulate a crash, or at least not unlocking the
// lock. We still exit 0 just to simplify the parent
// process exec code.
os.Exit(0)
}
lk.Close()
}
func testLock(t *testing.T, portable bool) {
lock := Lock
if portable {
lock = lockPortable
}
td, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(td)
path := filepath.Join(td, "foo.lock")
childLock := func(crash bool) error {
cmd := exec.Command(os.Args[0], "-test.run=LockInChild$")
cmd.Env = []string{"TEST_LOCK_FILE=" + path}
if portable {
cmd.Env = append(cmd.Env, "TEST_LOCK_PORTABLE=1")
}
if crash {
cmd.Env = append(cmd.Env, "TEST_LOCK_CRASH=1")
}
out, err := cmd.CombinedOutput()
t.Logf("Child output: %q (err %v)", out, err)
if err != nil {
return fmt.Errorf("Child Process lock of %s failed: %v %s", path, err, out)
}
return nil
}
t.Logf("Locking in crashing child...")
if err := childLock(true); err != nil {
t.Fatalf("first lock in child process: %v", err)
}
t.Logf("Locking+unlocking in child...")
if err := childLock(false); err != nil {
t.Fatalf("lock in child process after crashing child: %v", err)
}
t.Logf("Locking in parent...")
lk1, err := lock(path)
if err != nil {
t.Fatal(err)
}
t.Logf("Again in parent...")
_, err = lock(path)
if err == nil {
t.Fatal("expected second lock to fail")
}
t.Logf("Locking in child...")
if childLock(false) == nil {
t.Fatalf("expected lock in child process to fail")
}
t.Logf("Unlocking lock in parent")
if err := lk1.Close(); err != nil {
t.Fatal(err)
}
lk3, err := lock(path)
if err != nil {
t.Fatal(err)
}
lk3.Close()
}

View File

@ -0,0 +1,19 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package httptypes defines how etcd's HTTP API entities are serialized to and deserialized from JSON.
*/
package httptypes

View File

@ -0,0 +1,49 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httptypes
import (
"encoding/json"
"log"
"net/http"
)
type HTTPError struct {
Message string `json:"message"`
// HTTP return code
Code int `json:"-"`
}
func (e HTTPError) Error() string {
return e.Message
}
// TODO(xiangli): handle http write errors
func (e HTTPError) WriteTo(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(e.Code)
b, err := json.Marshal(e)
if err != nil {
log.Panicf("marshal HTTPError should never fail: %v", err)
}
w.Write(b)
}
func NewHTTPError(code int, m string) *HTTPError {
return &HTTPError{
Message: m,
Code: code,
}
}

View File

@ -0,0 +1,47 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httptypes
import (
"net/http"
"net/http/httptest"
"reflect"
"testing"
)
func TestHTTPErrorWriteTo(t *testing.T) {
err := NewHTTPError(http.StatusBadRequest, "what a bad request you made!")
rr := httptest.NewRecorder()
err.WriteTo(rr)
wcode := http.StatusBadRequest
wheader := http.Header(map[string][]string{
"Content-Type": []string{"application/json"},
})
wbody := `{"message":"what a bad request you made!"}`
if wcode != rr.Code {
t.Errorf("HTTP status code %d, want %d", rr.Code, wcode)
}
if !reflect.DeepEqual(wheader, rr.HeaderMap) {
t.Errorf("HTTP headers %v, want %v", rr.HeaderMap, wheader)
}
gbody := rr.Body.String()
if wbody != gbody {
t.Errorf("HTTP body %q, want %q", gbody, wbody)
}
}

View File

@ -0,0 +1,67 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httptypes
import (
"encoding/json"
"github.com/coreos/etcd/pkg/types"
)
type Member struct {
ID string `json:"id"`
Name string `json:"name"`
PeerURLs []string `json:"peerURLs"`
ClientURLs []string `json:"clientURLs"`
}
type MemberCreateRequest struct {
PeerURLs types.URLs
}
type MemberUpdateRequest struct {
MemberCreateRequest
}
func (m *MemberCreateRequest) UnmarshalJSON(data []byte) error {
s := struct {
PeerURLs []string `json:"peerURLs"`
}{}
err := json.Unmarshal(data, &s)
if err != nil {
return err
}
urls, err := types.NewURLs(s.PeerURLs)
if err != nil {
return err
}
m.PeerURLs = urls
return nil
}
type MemberCollection []Member
func (c *MemberCollection) MarshalJSON() ([]byte, error) {
d := struct {
Members []Member `json:"members"`
}{
Members: []Member(*c),
}
return json.Marshal(d)
}

View File

@ -0,0 +1,135 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httptypes
import (
"encoding/json"
"net/url"
"reflect"
"testing"
"github.com/coreos/etcd/pkg/types"
)
func TestMemberUnmarshal(t *testing.T) {
tests := []struct {
body []byte
wantMember Member
wantError bool
}{
// no URLs, just check ID & Name
{
body: []byte(`{"id": "c", "name": "dungarees"}`),
wantMember: Member{ID: "c", Name: "dungarees", PeerURLs: nil, ClientURLs: nil},
},
// both client and peer URLs
{
body: []byte(`{"peerURLs": ["http://127.0.0.1:4001"], "clientURLs": ["http://127.0.0.1:4001"]}`),
wantMember: Member{
PeerURLs: []string{
"http://127.0.0.1:4001",
},
ClientURLs: []string{
"http://127.0.0.1:4001",
},
},
},
// multiple peer URLs
{
body: []byte(`{"peerURLs": ["http://127.0.0.1:4001", "https://example.com"]}`),
wantMember: Member{
PeerURLs: []string{
"http://127.0.0.1:4001",
"https://example.com",
},
ClientURLs: nil,
},
},
// multiple client URLs
{
body: []byte(`{"clientURLs": ["http://127.0.0.1:4001", "https://example.com"]}`),
wantMember: Member{
PeerURLs: nil,
ClientURLs: []string{
"http://127.0.0.1:4001",
"https://example.com",
},
},
},
// invalid JSON
{
body: []byte(`{"peerU`),
wantError: true,
},
}
for i, tt := range tests {
got := Member{}
err := json.Unmarshal(tt.body, &got)
if tt.wantError != (err != nil) {
t.Errorf("#%d: want error %t, got %v", i, tt.wantError, err)
continue
}
if !reflect.DeepEqual(tt.wantMember, got) {
t.Errorf("#%d: incorrect output: want=%#v, got=%#v", i, tt.wantMember, got)
}
}
}
func TestMemberCreateRequestUnmarshal(t *testing.T) {
body := []byte(`{"peerURLs": ["http://127.0.0.1:8081", "https://127.0.0.1:8080"]}`)
want := MemberCreateRequest{
PeerURLs: types.URLs([]url.URL{
url.URL{Scheme: "http", Host: "127.0.0.1:8081"},
url.URL{Scheme: "https", Host: "127.0.0.1:8080"},
}),
}
var req MemberCreateRequest
if err := json.Unmarshal(body, &req); err != nil {
t.Fatalf("Unmarshal returned unexpected err=%v", err)
}
if !reflect.DeepEqual(want, req) {
t.Fatalf("Failed to unmarshal MemberCreateRequest: want=%#v, got=%#v", want, req)
}
}
func TestMemberCreateRequestUnmarshalFail(t *testing.T) {
tests := [][]byte{
// invalid JSON
[]byte(``),
[]byte(`{`),
// spot-check validation done in types.NewURLs
[]byte(`{"peerURLs": "foo"}`),
[]byte(`{"peerURLs": ["."]}`),
[]byte(`{"peerURLs": []}`),
[]byte(`{"peerURLs": ["http://127.0.0.1:4001/foo"]}`),
[]byte(`{"peerURLs": ["http://127.0.0.1"]}`),
}
for i, tt := range tests {
var req MemberCreateRequest
if err := json.Unmarshal(tt, &req); err == nil {
t.Errorf("#%d: expected err, got nil", i)
}
}
}

View File

@ -0,0 +1,41 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
import (
"strconv"
)
// ID represents a generic identifier which is canonically
// stored as a uint64 but is typically represented as a
// base-16 string for input/output
type ID uint64
func (i ID) String() string {
return strconv.FormatUint(uint64(i), 16)
}
// IDFromString attempts to create an ID from a base-16 string.
func IDFromString(s string) (ID, error) {
i, err := strconv.ParseUint(s, 16, 64)
return ID(i), err
}
// IDSlice implements the sort interface
type IDSlice []ID
func (p IDSlice) Len() int { return len(p) }
func (p IDSlice) Less(i, j int) bool { return uint64(p[i]) < uint64(p[j]) }
func (p IDSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

View File

@ -0,0 +1,95 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
import (
"reflect"
"sort"
"testing"
)
func TestIDString(t *testing.T) {
tests := []struct {
input ID
want string
}{
{
input: 12,
want: "c",
},
{
input: 4918257920282737594,
want: "444129853c343bba",
},
}
for i, tt := range tests {
got := tt.input.String()
if tt.want != got {
t.Errorf("#%d: ID.String failure: want=%v, got=%v", i, tt.want, got)
}
}
}
func TestIDFromString(t *testing.T) {
tests := []struct {
input string
want ID
}{
{
input: "17",
want: 23,
},
{
input: "612840dae127353",
want: 437557308098245459,
},
}
for i, tt := range tests {
got, err := IDFromString(tt.input)
if err != nil {
t.Errorf("#%d: IDFromString failure: err=%v", i, err)
continue
}
if tt.want != got {
t.Errorf("#%d: IDFromString failure: want=%v, got=%v", i, tt.want, got)
}
}
}
func TestIDFromStringFail(t *testing.T) {
tests := []string{
"",
"XXX",
"612840dae127353612840dae127353",
}
for i, tt := range tests {
_, err := IDFromString(tt)
if err == nil {
t.Fatalf("#%d: IDFromString expected error, but err=nil", i)
}
}
}
func TestIDSlice(t *testing.T) {
g := []ID{10, 500, 5, 1, 100, 25}
w := []ID{1, 5, 10, 25, 100, 500}
sort.Sort(IDSlice(g))
if !reflect.DeepEqual(g, w) {
t.Errorf("slice after sort = %#v, want %#v", g, w)
}
}

View File

@ -0,0 +1,178 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
import (
"reflect"
"sort"
"sync"
)
type Set interface {
Add(string)
Remove(string)
Contains(string) bool
Equals(Set) bool
Length() int
Values() []string
Copy() Set
Sub(Set) Set
}
func NewUnsafeSet(values ...string) *unsafeSet {
set := &unsafeSet{make(map[string]struct{})}
for _, v := range values {
set.Add(v)
}
return set
}
func NewThreadsafeSet(values ...string) *tsafeSet {
us := NewUnsafeSet(values...)
return &tsafeSet{us, sync.RWMutex{}}
}
type unsafeSet struct {
d map[string]struct{}
}
// Add adds a new value to the set (no-op if the value is already present)
func (us *unsafeSet) Add(value string) {
us.d[value] = struct{}{}
}
// Remove removes the given value from the set
func (us *unsafeSet) Remove(value string) {
delete(us.d, value)
}
// Contains returns whether the set contains the given value
func (us *unsafeSet) Contains(value string) (exists bool) {
_, exists = us.d[value]
return
}
// ContainsAll returns whether the set contains all given values
func (us *unsafeSet) ContainsAll(values []string) bool {
for _, s := range values {
if !us.Contains(s) {
return false
}
}
return true
}
// Equals returns whether the contents of two sets are identical
func (us *unsafeSet) Equals(other Set) bool {
v1 := sort.StringSlice(us.Values())
v2 := sort.StringSlice(other.Values())
v1.Sort()
v2.Sort()
return reflect.DeepEqual(v1, v2)
}
// Length returns the number of elements in the set
func (us *unsafeSet) Length() int {
return len(us.d)
}
// Values returns the values of the Set in an unspecified order.
func (us *unsafeSet) Values() (values []string) {
values = make([]string, 0)
for val, _ := range us.d {
values = append(values, val)
}
return
}
// Copy creates a new Set containing the values of the first
func (us *unsafeSet) Copy() Set {
cp := NewUnsafeSet()
for val, _ := range us.d {
cp.Add(val)
}
return cp
}
// Sub removes all elements in other from the set
func (us *unsafeSet) Sub(other Set) Set {
oValues := other.Values()
result := us.Copy().(*unsafeSet)
for _, val := range oValues {
if _, ok := result.d[val]; !ok {
continue
}
delete(result.d, val)
}
return result
}
type tsafeSet struct {
us *unsafeSet
m sync.RWMutex
}
func (ts *tsafeSet) Add(value string) {
ts.m.Lock()
defer ts.m.Unlock()
ts.us.Add(value)
}
func (ts *tsafeSet) Remove(value string) {
ts.m.Lock()
defer ts.m.Unlock()
ts.us.Remove(value)
}
func (ts *tsafeSet) Contains(value string) (exists bool) {
ts.m.RLock()
defer ts.m.RUnlock()
return ts.us.Contains(value)
}
func (ts *tsafeSet) Equals(other Set) bool {
ts.m.RLock()
defer ts.m.RUnlock()
return ts.us.Equals(other)
}
func (ts *tsafeSet) Length() int {
ts.m.RLock()
defer ts.m.RUnlock()
return ts.us.Length()
}
func (ts *tsafeSet) Values() (values []string) {
ts.m.RLock()
defer ts.m.RUnlock()
return ts.us.Values()
}
func (ts *tsafeSet) Copy() Set {
ts.m.RLock()
defer ts.m.RUnlock()
usResult := ts.us.Copy().(*unsafeSet)
return &tsafeSet{usResult, sync.RWMutex{}}
}
func (ts *tsafeSet) Sub(other Set) Set {
ts.m.RLock()
defer ts.m.RUnlock()
usResult := ts.us.Sub(other).(*unsafeSet)
return &tsafeSet{usResult, sync.RWMutex{}}
}

View File

@ -0,0 +1,186 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
import (
"reflect"
"sort"
"testing"
)
func TestUnsafeSet(t *testing.T) {
driveSetTests(t, NewUnsafeSet())
}
func TestThreadsafeSet(t *testing.T) {
driveSetTests(t, NewThreadsafeSet())
}
// Check that two slices contents are equal; order is irrelevant
func equal(a, b []string) bool {
as := sort.StringSlice(a)
bs := sort.StringSlice(b)
as.Sort()
bs.Sort()
return reflect.DeepEqual(as, bs)
}
func driveSetTests(t *testing.T, s Set) {
// Verify operations on an empty set
eValues := []string{}
values := s.Values()
if !reflect.DeepEqual(values, eValues) {
t.Fatalf("Expect values=%v got %v", eValues, values)
}
if l := s.Length(); l != 0 {
t.Fatalf("Expected length=0, got %d", l)
}
for _, v := range []string{"foo", "bar", "baz"} {
if s.Contains(v) {
t.Fatalf("Expect s.Contains(%q) to be fale, got true", v)
}
}
// Add three items, ensure they show up
s.Add("foo")
s.Add("bar")
s.Add("baz")
eValues = []string{"foo", "bar", "baz"}
values = s.Values()
if !equal(values, eValues) {
t.Fatalf("Expect values=%v got %v", eValues, values)
}
for _, v := range eValues {
if !s.Contains(v) {
t.Fatalf("Expect s.Contains(%q) to be true, got false", v)
}
}
if l := s.Length(); l != 3 {
t.Fatalf("Expected length=3, got %d", l)
}
// Add the same item a second time, ensuring it is not duplicated
s.Add("foo")
values = s.Values()
if !equal(values, eValues) {
t.Fatalf("Expect values=%v got %v", eValues, values)
}
if l := s.Length(); l != 3 {
t.Fatalf("Expected length=3, got %d", l)
}
// Remove all items, ensure they are gone
s.Remove("foo")
s.Remove("bar")
s.Remove("baz")
eValues = []string{}
values = s.Values()
if !equal(values, eValues) {
t.Fatalf("Expect values=%v got %v", eValues, values)
}
if l := s.Length(); l != 0 {
t.Fatalf("Expected length=0, got %d", l)
}
// Create new copies of the set, and ensure they are unlinked to the
// original Set by making modifications
s.Add("foo")
s.Add("bar")
cp1 := s.Copy()
cp2 := s.Copy()
s.Remove("foo")
cp3 := s.Copy()
cp1.Add("baz")
for i, tt := range []struct {
want []string
got []string
}{
{[]string{"bar"}, s.Values()},
{[]string{"foo", "bar", "baz"}, cp1.Values()},
{[]string{"foo", "bar"}, cp2.Values()},
{[]string{"bar"}, cp3.Values()},
} {
if !equal(tt.want, tt.got) {
t.Fatalf("case %d: expect values=%v got %v", i, tt.want, tt.got)
}
}
for i, tt := range []struct {
want bool
got bool
}{
{true, s.Equals(cp3)},
{true, cp3.Equals(s)},
{false, s.Equals(cp2)},
{false, s.Equals(cp1)},
{false, cp1.Equals(s)},
{false, cp2.Equals(s)},
{false, cp2.Equals(cp1)},
} {
if tt.got != tt.want {
t.Fatalf("case %d: want %t, got %t", i, tt.want, tt.got)
}
}
// Subtract values from a Set, ensuring a new Set is created and
// the original Sets are unmodified
sub1 := cp1.Sub(s)
sub2 := cp2.Sub(cp1)
for i, tt := range []struct {
want []string
got []string
}{
{[]string{"foo", "bar", "baz"}, cp1.Values()},
{[]string{"foo", "bar"}, cp2.Values()},
{[]string{"bar"}, s.Values()},
{[]string{"foo", "baz"}, sub1.Values()},
{[]string{}, sub2.Values()},
} {
if !equal(tt.want, tt.got) {
t.Fatalf("case %d: expect values=%v got %v", i, tt.want, tt.got)
}
}
}
func TestUnsafeSetContainsAll(t *testing.T) {
vals := []string{"foo", "bar", "baz"}
s := NewUnsafeSet(vals...)
tests := []struct {
strs []string
wcontain bool
}{
{[]string{}, true},
{vals[:1], true},
{vals[:2], true},
{vals, true},
{[]string{"cuz"}, false},
{[]string{vals[0], "cuz"}, false},
}
for i, tt := range tests {
if g := s.ContainsAll(tt.strs); g != tt.wcontain {
t.Errorf("#%d: ok = %v, want %v", i, g, tt.wcontain)
}
}
}

View File

@ -0,0 +1,22 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
// Uint64Slice implements sort interface
type Uint64Slice []uint64
func (p Uint64Slice) Len() int { return len(p) }
func (p Uint64Slice) Less(i, j int) bool { return p[i] < p[j] }
func (p Uint64Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

View File

@ -0,0 +1,30 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
import (
"reflect"
"sort"
"testing"
)
func TestUint64Slice(t *testing.T) {
g := Uint64Slice{10, 500, 5, 1, 100, 25}
w := Uint64Slice{1, 5, 10, 25, 100, 500}
sort.Sort(g)
if !reflect.DeepEqual(g, w) {
t.Errorf("slice after sort = %#v, want %#v", g, w)
}
}

View File

@ -0,0 +1,74 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
import (
"errors"
"fmt"
"net"
"net/url"
"sort"
"strings"
)
type URLs []url.URL
func NewURLs(strs []string) (URLs, error) {
all := make([]url.URL, len(strs))
if len(all) == 0 {
return nil, errors.New("no valid URLs given")
}
for i, in := range strs {
in = strings.TrimSpace(in)
u, err := url.Parse(in)
if err != nil {
return nil, err
}
if u.Scheme != "http" && u.Scheme != "https" {
return nil, fmt.Errorf("URL scheme must be http or https: %s", in)
}
if _, _, err := net.SplitHostPort(u.Host); err != nil {
return nil, fmt.Errorf(`URL address does not have the form "host:port": %s`, in)
}
if u.Path != "" {
return nil, fmt.Errorf("URL must not contain a path: %s", in)
}
all[i] = *u
}
us := URLs(all)
us.Sort()
return us, nil
}
func (us URLs) String() string {
return strings.Join(us.StringSlice(), ",")
}
func (us *URLs) Sort() {
sort.Sort(us)
}
func (us URLs) Len() int { return len(us) }
func (us URLs) Less(i, j int) bool { return us[i].String() < us[j].String() }
func (us URLs) Swap(i, j int) { us[i], us[j] = us[j], us[i] }
func (us URLs) StringSlice() []string {
out := make([]string, len(us))
for i := range us {
out[i] = us[i].String()
}
return out
}

View File

@ -0,0 +1,169 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
import (
"reflect"
"testing"
"github.com/coreos/etcd/pkg/testutil"
)
func TestNewURLs(t *testing.T) {
tests := []struct {
strs []string
wurls URLs
}{
{
[]string{"http://127.0.0.1:4001"},
testutil.MustNewURLs(t, []string{"http://127.0.0.1:4001"}),
},
// it can trim space
{
[]string{" http://127.0.0.1:4001 "},
testutil.MustNewURLs(t, []string{"http://127.0.0.1:4001"}),
},
// it does sort
{
[]string{
"http://127.0.0.2:4001",
"http://127.0.0.1:4001",
},
testutil.MustNewURLs(t, []string{
"http://127.0.0.1:4001",
"http://127.0.0.2:4001",
}),
},
}
for i, tt := range tests {
urls, _ := NewURLs(tt.strs)
if !reflect.DeepEqual(urls, tt.wurls) {
t.Errorf("#%d: urls = %+v, want %+v", i, urls, tt.wurls)
}
}
}
func TestURLsString(t *testing.T) {
tests := []struct {
us URLs
wstr string
}{
{
URLs{},
"",
},
{
testutil.MustNewURLs(t, []string{"http://127.0.0.1:4001"}),
"http://127.0.0.1:4001",
},
{
testutil.MustNewURLs(t, []string{
"http://127.0.0.1:4001",
"http://127.0.0.2:4001",
}),
"http://127.0.0.1:4001,http://127.0.0.2:4001",
},
{
testutil.MustNewURLs(t, []string{
"http://127.0.0.2:4001",
"http://127.0.0.1:4001",
}),
"http://127.0.0.2:4001,http://127.0.0.1:4001",
},
}
for i, tt := range tests {
g := tt.us.String()
if g != tt.wstr {
t.Errorf("#%d: string = %s, want %s", i, g, tt.wstr)
}
}
}
func TestURLsSort(t *testing.T) {
g := testutil.MustNewURLs(t, []string{
"http://127.0.0.4:4001",
"http://127.0.0.2:4001",
"http://127.0.0.1:4001",
"http://127.0.0.3:4001",
})
w := testutil.MustNewURLs(t, []string{
"http://127.0.0.1:4001",
"http://127.0.0.2:4001",
"http://127.0.0.3:4001",
"http://127.0.0.4:4001",
})
gurls := URLs(g)
gurls.Sort()
if !reflect.DeepEqual(g, w) {
t.Errorf("URLs after sort = %#v, want %#v", g, w)
}
}
func TestURLsStringSlice(t *testing.T) {
tests := []struct {
us URLs
wstr []string
}{
{
URLs{},
[]string{},
},
{
testutil.MustNewURLs(t, []string{"http://127.0.0.1:4001"}),
[]string{"http://127.0.0.1:4001"},
},
{
testutil.MustNewURLs(t, []string{
"http://127.0.0.1:4001",
"http://127.0.0.2:4001",
}),
[]string{"http://127.0.0.1:4001", "http://127.0.0.2:4001"},
},
{
testutil.MustNewURLs(t, []string{
"http://127.0.0.2:4001",
"http://127.0.0.1:4001",
}),
[]string{"http://127.0.0.2:4001", "http://127.0.0.1:4001"},
},
}
for i, tt := range tests {
g := tt.us.StringSlice()
if !reflect.DeepEqual(g, tt.wstr) {
t.Errorf("#%d: string slice = %+v, want %+v", i, g, tt.wstr)
}
}
}
func TestNewURLsFail(t *testing.T) {
tests := [][]string{
// no urls given
{},
// missing protocol scheme
{"://127.0.0.1:4001"},
// unsupported scheme
{"mailto://127.0.0.1:4001"},
// not conform to host:port
{"http://127.0.0.1"},
// contain a path
{"http://127.0.0.1:4001/path"},
}
for i, tt := range tests {
_, err := NewURLs(tt)
if err == nil {
t.Errorf("#%d: err = nil, but error", i)
}
}
}

View File

@ -0,0 +1,202 @@
package semver
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
)
type Version struct {
Major int64
Minor int64
Patch int64
PreRelease PreRelease
Metadata string
}
type PreRelease string
func splitOff(input *string, delim string) (val string) {
parts := strings.SplitN(*input, delim, 2)
if len(parts) == 2 {
*input = parts[0]
val = parts[1]
}
return val
}
func NewVersion(version string) (*Version, error) {
v := Version{}
dotParts := strings.SplitN(version, ".", 3)
if len(dotParts) != 3 {
return nil, errors.New(fmt.Sprintf("%s is not in dotted-tri format", version))
}
v.Metadata = splitOff(&dotParts[2], "+")
v.PreRelease = PreRelease(splitOff(&dotParts[2], "-"))
parsed := make([]int64, 3, 3)
for i, v := range dotParts[:3] {
val, err := strconv.ParseInt(v, 10, 64)
parsed[i] = val
if err != nil {
return nil, err
}
}
v.Major = parsed[0]
v.Minor = parsed[1]
v.Patch = parsed[2]
return &v, nil
}
func (v *Version) String() string {
var buffer bytes.Buffer
base := fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
buffer.WriteString(base)
if v.PreRelease != "" {
buffer.WriteString(fmt.Sprintf("-%s", v.PreRelease))
}
if v.Metadata != "" {
buffer.WriteString(fmt.Sprintf("+%s", v.Metadata))
}
return buffer.String()
}
func (v *Version) LessThan(versionB Version) bool {
versionA := *v
cmp := recursiveCompare(versionA.Slice(), versionB.Slice())
if cmp == 0 {
cmp = preReleaseCompare(versionA, versionB)
}
if cmp == -1 {
return true
}
return false
}
/* Slice converts the comparable parts of the semver into a slice of strings */
func (v *Version) Slice() []int64 {
return []int64{v.Major, v.Minor, v.Patch}
}
func (p *PreRelease) Slice() []string {
preRelease := string(*p)
return strings.Split(preRelease, ".")
}
func preReleaseCompare(versionA Version, versionB Version) int {
a := versionA.PreRelease
b := versionB.PreRelease
/* Handle the case where if two versions are otherwise equal it is the
* one without a PreRelease that is greater */
if len(a) == 0 && (len(b) > 0) {
return 1
} else if len(b) == 0 && (len(a) > 0) {
return -1
}
// If there is a prelease, check and compare each part.
return recursivePreReleaseCompare(a.Slice(), b.Slice())
}
func recursiveCompare(versionA []int64, versionB []int64) int {
if len(versionA) == 0 {
return 0
}
a := versionA[0]
b := versionB[0]
if a > b {
return 1
} else if a < b {
return -1
}
return recursiveCompare(versionA[1:], versionB[1:])
}
func recursivePreReleaseCompare(versionA []string, versionB []string) int {
// Handle slice length disparity.
if len(versionA) == 0 {
// Nothing to compare too, so we return 0
return 0
} else if len(versionB) == 0 {
// We're longer than versionB so return 1.
return 1
}
a := versionA[0]
b := versionB[0]
aInt := false; bInt := false
aI, err := strconv.Atoi(versionA[0])
if err == nil {
aInt = true
}
bI, err := strconv.Atoi(versionB[0])
if err == nil {
bInt = true
}
// Handle Integer Comparison
if aInt && bInt {
if aI > bI {
return 1
} else if aI < bI {
return -1
}
}
// Handle String Comparison
if a > b {
return 1
} else if a < b {
return -1
}
return recursivePreReleaseCompare(versionA[1:], versionB[1:])
}
// BumpMajor increments the Major field by 1 and resets all other fields to their default values
func (v *Version) BumpMajor() {
v.Major += 1
v.Minor = 0
v.Patch = 0
v.PreRelease = PreRelease("")
v.Metadata = ""
}
// BumpMinor increments the Minor field by 1 and resets all other fields to their default values
func (v *Version) BumpMinor() {
v.Minor += 1
v.Patch = 0
v.PreRelease = PreRelease("")
v.Metadata = ""
}
// BumpPatch increments the Patch field by 1 and resets all other fields to their default values
func (v *Version) BumpPatch() {
v.Patch += 1
v.PreRelease = PreRelease("")
v.Metadata = ""
}

View File

@ -0,0 +1,187 @@
package semver
import (
"math/rand"
"testing"
"time"
)
type fixture struct {
greaterVersion string
lesserVersion string
}
var fixtures = []fixture{
fixture{"0.0.0", "0.0.0-foo"},
fixture{"0.0.1", "0.0.0"},
fixture{"1.0.0", "0.9.9"},
fixture{"0.10.0", "0.9.0"},
fixture{"0.99.0", "0.10.0"},
fixture{"2.0.0", "1.2.3"},
fixture{"0.0.0", "0.0.0-foo"},
fixture{"0.0.1", "0.0.0"},
fixture{"1.0.0", "0.9.9"},
fixture{"0.10.0", "0.9.0"},
fixture{"0.99.0", "0.10.0"},
fixture{"2.0.0", "1.2.3"},
fixture{"0.0.0", "0.0.0-foo"},
fixture{"0.0.1", "0.0.0"},
fixture{"1.0.0", "0.9.9"},
fixture{"0.10.0", "0.9.0"},
fixture{"0.99.0", "0.10.0"},
fixture{"2.0.0", "1.2.3"},
fixture{"1.2.3", "1.2.3-asdf"},
fixture{"1.2.3", "1.2.3-4"},
fixture{"1.2.3", "1.2.3-4-foo"},
fixture{"1.2.3-5-foo", "1.2.3-5"},
fixture{"1.2.3-5", "1.2.3-4"},
fixture{"1.2.3-5-foo", "1.2.3-5-Foo"},
fixture{"3.0.0", "2.7.2+asdf"},
fixture{"3.0.0+foobar", "2.7.2"},
fixture{"1.2.3-a.10", "1.2.3-a.5"},
fixture{"1.2.3-a.b", "1.2.3-a.5"},
fixture{"1.2.3-a.b", "1.2.3-a"},
fixture{"1.2.3-a.b.c.10.d.5", "1.2.3-a.b.c.5.d.100"},
fixture{"1.0.0", "1.0.0-rc.1"},
fixture{"1.0.0-rc.2", "1.0.0-rc.1"},
fixture{"1.0.0-rc.1", "1.0.0-beta.11"},
fixture{"1.0.0-beta.11", "1.0.0-beta.2"},
fixture{"1.0.0-beta.2", "1.0.0-beta"},
fixture{"1.0.0-beta", "1.0.0-alpha.beta"},
fixture{"1.0.0-alpha.beta", "1.0.0-alpha.1"},
fixture{"1.0.0-alpha.1", "1.0.0-alpha"},
}
func TestCompare(t *testing.T) {
for _, v := range fixtures {
gt, err := NewVersion(v.greaterVersion)
if err != nil {
t.Error(err)
}
lt, err := NewVersion(v.lesserVersion)
if err != nil {
t.Error(err)
}
if gt.LessThan(*lt) == true {
t.Errorf("%s should not be less than %s", gt, lt)
}
}
}
func testString(t *testing.T, orig string, version *Version) {
if orig != version.String() {
t.Errorf("%s != %s", orig, version)
}
}
func TestString(t *testing.T) {
for _, v := range fixtures {
gt, err := NewVersion(v.greaterVersion)
if err != nil {
t.Error(err)
}
testString(t, v.greaterVersion, gt)
lt, err := NewVersion(v.lesserVersion)
if err != nil {
t.Error(err)
}
testString(t, v.lesserVersion, lt)
}
}
func shuffleStringSlice(src []string) []string {
dest := make([]string, len(src))
rand.Seed(time.Now().Unix())
perm := rand.Perm(len(src))
for i, v := range perm {
dest[v] = src[i]
}
return dest
}
func TestSort(t *testing.T) {
sortedVersions := []string{"1.0.0", "1.0.2", "1.2.0", "3.1.1"}
unsortedVersions := shuffleStringSlice(sortedVersions)
semvers := []*Version{}
for _, v := range unsortedVersions {
sv, err := NewVersion(v)
if err != nil {
t.Fatal(err)
}
semvers = append(semvers, sv)
}
Sort(semvers)
for idx, sv := range semvers {
if sv.String() != sortedVersions[idx] {
t.Fatalf("incorrect sort at index %v", idx)
}
}
}
func TestBumpMajor(t *testing.T) {
version, _ := NewVersion("1.0.0")
version.BumpMajor()
if version.Major != 2 {
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
}
version, _ = NewVersion("1.5.2")
version.BumpMajor()
if version.Minor != 0 && version.Patch != 0 {
t.Fatalf("bumping major on 1.5.2 resulted in %v", version)
}
version, _ = NewVersion("1.0.0+build.1-alpha.1")
version.BumpMajor()
if version.PreRelease != "" && version.PreRelease != "" {
t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version)
}
}
func TestBumpMinor(t *testing.T) {
version, _ := NewVersion("1.0.0")
version.BumpMinor()
if version.Major != 1 {
t.Fatalf("bumping minor on 1.0.0 resulted in %v", version)
}
if version.Minor != 1 {
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
}
version, _ = NewVersion("1.0.0+build.1-alpha.1")
version.BumpMinor()
if version.PreRelease != "" && version.PreRelease != "" {
t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version)
}
}
func TestBumpPatch(t *testing.T) {
version, _ := NewVersion("1.0.0")
version.BumpPatch()
if version.Major != 1 {
t.Fatalf("bumping minor on 1.0.0 resulted in %v", version)
}
if version.Minor != 0 {
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
}
if version.Patch != 1 {
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
}
version, _ = NewVersion("1.0.0+build.1-alpha.1")
version.BumpPatch()
if version.PreRelease != "" && version.PreRelease != "" {
t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version)
}
}

View File

@ -0,0 +1,24 @@
package semver
import (
"sort"
)
type Versions []*Version
func (s Versions) Len() int {
return len(s)
}
func (s Versions) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s Versions) Less(i, j int) bool {
return s[i].LessThan(*s[j])
}
// Sort sorts the given slice of Version
func Sort(versions []*Version) {
sort.Sort(Versions(versions))
}

View File

@ -0,0 +1,213 @@
package unit
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"strings"
"unicode"
)
// Deserialize parses a systemd unit file into a list of UnitOption objects.
func Deserialize(f io.Reader) (opts []*UnitOption, err error) {
lexer, optchan, errchan := newLexer(f)
go lexer.lex()
for opt := range optchan {
opts = append(opts, &(*opt))
}
err = <-errchan
return opts, err
}
func newLexer(f io.Reader) (*lexer, <-chan *UnitOption, <-chan error) {
optchan := make(chan *UnitOption)
errchan := make(chan error, 1)
buf := bufio.NewReader(f)
return &lexer{buf, optchan, errchan, ""}, optchan, errchan
}
type lexer struct {
buf *bufio.Reader
optchan chan *UnitOption
errchan chan error
section string
}
func (l *lexer) lex() {
var err error
next := l.lexNextSection
for next != nil {
next, err = next()
if err != nil {
l.errchan <- err
break
}
}
close(l.optchan)
close(l.errchan)
}
type lexStep func() (lexStep, error)
func (l *lexer) lexSectionName() (lexStep, error) {
sec, err := l.buf.ReadBytes(']')
if err != nil {
return nil, errors.New("unable to find end of section")
}
return l.lexSectionSuffixFunc(string(sec[:len(sec)-1])), nil
}
func (l *lexer) lexSectionSuffixFunc(section string) lexStep {
return func() (lexStep, error) {
garbage, err := l.toEOL()
if err != nil {
return nil, err
}
garbage = bytes.TrimSpace(garbage)
if len(garbage) > 0 {
return nil, fmt.Errorf("found garbage after section name %s: %v", l.section, garbage)
}
return l.lexNextSectionOrOptionFunc(section), nil
}
}
func (l *lexer) ignoreLineFunc(next lexStep) lexStep {
return func() (lexStep, error) {
for {
line, err := l.toEOL()
if err != nil {
return nil, err
}
line = bytes.TrimSuffix(line, []byte{' '})
// lack of continuation means this line has been exhausted
if !bytes.HasSuffix(line, []byte{'\\'}) {
break
}
}
// reached end of buffer, safe to exit
return next, nil
}
}
func (l *lexer) lexNextSection() (lexStep, error) {
r, _, err := l.buf.ReadRune()
if err != nil {
if err == io.EOF {
err = nil
}
return nil, err
}
if r == '[' {
return l.lexSectionName, nil
} else if isComment(r) {
return l.ignoreLineFunc(l.lexNextSection), nil
}
return l.lexNextSection, nil
}
func (l *lexer) lexNextSectionOrOptionFunc(section string) lexStep {
return func() (lexStep, error) {
r, _, err := l.buf.ReadRune()
if err != nil {
if err == io.EOF {
err = nil
}
return nil, err
}
if unicode.IsSpace(r) {
return l.lexNextSectionOrOptionFunc(section), nil
} else if r == '[' {
return l.lexSectionName, nil
} else if isComment(r) {
return l.ignoreLineFunc(l.lexNextSectionOrOptionFunc(section)), nil
}
l.buf.UnreadRune()
return l.lexOptionNameFunc(section), nil
}
}
func (l *lexer) lexOptionNameFunc(section string) lexStep {
return func() (lexStep, error) {
var partial bytes.Buffer
for {
r, _, err := l.buf.ReadRune()
if err != nil {
return nil, err
}
if r == '\n' || r == '\r' {
return nil, errors.New("unexpected newline encountered while parsing option name")
}
if r == '=' {
break
}
partial.WriteRune(r)
}
name := strings.TrimSpace(partial.String())
return l.lexOptionValueFunc(section, name), nil
}
}
func (l *lexer) lexOptionValueFunc(section, name string) lexStep {
return func() (lexStep, error) {
var partial bytes.Buffer
for {
line, err := l.toEOL()
if err != nil {
return nil, err
}
// lack of continuation means this value has been exhausted
idx := bytes.LastIndex(line, []byte{'\\'})
if idx == -1 || idx != (len(line)-1) {
partial.Write(line)
break
}
partial.Write(line[0:idx])
partial.WriteRune(' ')
}
val := strings.TrimSpace(partial.String())
l.optchan <- &UnitOption{Section: section, Name: name, Value: val}
return l.lexNextSectionOrOptionFunc(section), nil
}
}
func (l *lexer) toEOL() ([]byte, error) {
line, err := l.buf.ReadBytes('\n')
// ignore EOF here since it's roughly equivalent to EOL
if err != nil && err != io.EOF {
return nil, err
}
line = bytes.TrimSuffix(line, []byte{'\r'})
line = bytes.TrimSuffix(line, []byte{'\n'})
return line, nil
}
func isComment(r rune) bool {
return r == '#' || r == ';'
}

View File

@ -0,0 +1,301 @@
package unit
import (
"bytes"
"fmt"
"reflect"
"testing"
)
func TestDeserialize(t *testing.T) {
tests := []struct {
input []byte
output []*UnitOption
}{
// multiple options underneath a section
{
[]byte(`[Unit]
Description=Foo
Description=Bar
Requires=baz.service
After=baz.service
`),
[]*UnitOption{
&UnitOption{"Unit", "Description", "Foo"},
&UnitOption{"Unit", "Description", "Bar"},
&UnitOption{"Unit", "Requires", "baz.service"},
&UnitOption{"Unit", "After", "baz.service"},
},
},
// multiple sections
{
[]byte(`[Unit]
Description=Foo
[Service]
ExecStart=/usr/bin/sleep infinity
[X-Third-Party]
Pants=on
`),
[]*UnitOption{
&UnitOption{"Unit", "Description", "Foo"},
&UnitOption{"Service", "ExecStart", "/usr/bin/sleep infinity"},
&UnitOption{"X-Third-Party", "Pants", "on"},
},
},
// multiple sections with no options
{
[]byte(`[Unit]
[Service]
[X-Third-Party]
`),
[]*UnitOption{},
},
// multiple values not special-cased
{
[]byte(`[Service]
Environment= "FOO=BAR" "BAZ=QUX"
`),
[]*UnitOption{
&UnitOption{"Service", "Environment", "\"FOO=BAR\" \"BAZ=QUX\""},
},
},
// line continuations respected
{
[]byte(`[Unit]
Description= Unnecessarily wrapped \
words here
`),
[]*UnitOption{
&UnitOption{"Unit", "Description", "Unnecessarily wrapped words here"},
},
},
// comments ignored
{
[]byte(`; comment alpha
# comment bravo
[Unit]
; comment charlie
# comment delta
#Description=Foo
Description=Bar
; comment echo
# comment foxtrot
`),
[]*UnitOption{
&UnitOption{"Unit", "Description", "Bar"},
},
},
// apparent comment lines inside of line continuations not ignored
{
[]byte(`[Unit]
Description=Bar\
# comment alpha
Description=Bar\
# comment bravo \
Baz
`),
[]*UnitOption{
&UnitOption{"Unit", "Description", "Bar # comment alpha"},
&UnitOption{"Unit", "Description", "Bar # comment bravo Baz"},
},
},
// options outside of sections are ignored
{
[]byte(`Description=Foo
[Unit]
Description=Bar
`),
[]*UnitOption{
&UnitOption{"Unit", "Description", "Bar"},
},
},
// garbage outside of sections are ignored
{
[]byte(`<<<<<<<<
[Unit]
Description=Bar
`),
[]*UnitOption{
&UnitOption{"Unit", "Description", "Bar"},
},
},
// garbage used as unit option
{
[]byte(`[Unit]
<<<<<<<<=Bar
`),
[]*UnitOption{
&UnitOption{"Unit", "<<<<<<<<", "Bar"},
},
},
// option name with spaces are valid
{
[]byte(`[Unit]
Some Thing = Bar
`),
[]*UnitOption{
&UnitOption{"Unit", "Some Thing", "Bar"},
},
},
// lack of trailing newline doesn't cause problem for non-continued file
{
[]byte(`[Unit]
Description=Bar`),
[]*UnitOption{
&UnitOption{"Unit", "Description", "Bar"},
},
},
// unit file with continuation but no following line is ok, too
{
[]byte(`[Unit]
Description=Bar \`),
[]*UnitOption{
&UnitOption{"Unit", "Description", "Bar"},
},
},
// Assert utf8 characters are preserved
{
[]byte(`[©]
µ=ÇôrèÕ$`),
[]*UnitOption{
&UnitOption{"©", "µ☃", "ÇôrèÕ$"},
},
},
// whitespace removed around option name
{
[]byte(`[Unit]
Description =words here
`),
[]*UnitOption{
&UnitOption{"Unit", "Description", "words here"},
},
},
// whitespace around option value stripped
{
[]byte(`[Unit]
Description= words here `),
[]*UnitOption{
&UnitOption{"Unit", "Description", "words here"},
},
},
// whitespace around option value stripped, regardless of continuation
{
[]byte(`[Unit]
Description= words here \
`),
[]*UnitOption{
&UnitOption{"Unit", "Description", "words here"},
},
},
// backslash not considered continuation if followed by text
{
[]byte(`[Service]
ExecStart=/bin/bash -c "while true; do echo \"ping\"; sleep 1; done"
`),
[]*UnitOption{
&UnitOption{"Service", "ExecStart", `/bin/bash -c "while true; do echo \"ping\"; sleep 1; done"`},
},
},
// backslash not considered continuation if followed by whitespace, but still trimmed
{
[]byte(`[Service]
ExecStart=/bin/bash echo poof \ `),
[]*UnitOption{
&UnitOption{"Service", "ExecStart", `/bin/bash echo poof \`},
},
},
}
assert := func(expect, output []*UnitOption) error {
if len(expect) != len(output) {
return fmt.Errorf("expected %d items, got %d", len(expect), len(output))
}
for i, _ := range expect {
if !reflect.DeepEqual(expect[i], output[i]) {
return fmt.Errorf("item %d: expected %v, got %v", i, expect[i], output[i])
}
}
return nil
}
for i, tt := range tests {
output, err := Deserialize(bytes.NewReader(tt.input))
if err != nil {
t.Errorf("case %d: unexpected error parsing unit: %v", i, err)
continue
}
err = assert(tt.output, output)
if err != nil {
t.Errorf("case %d: %v", i, err)
t.Log("Expected options:")
logUnitOptionSlice(t, tt.output)
t.Log("Actual options:")
logUnitOptionSlice(t, output)
}
}
}
func TestDeserializeFail(t *testing.T) {
tests := [][]byte{
// malformed section header
[]byte(`[Unit
Description=Foo
`),
// garbage following section header
[]byte(`[Unit] pants
Description=Foo
`),
// option without value
[]byte(`[Unit]
Description
`),
// garbage inside of section
[]byte(`[Unit]
<<<<<<
Description=Foo
`),
}
for i, tt := range tests {
output, err := Deserialize(bytes.NewReader(tt))
if err == nil {
t.Errorf("case %d: unexpected non-nil error, received nil", i)
t.Log("Output:")
logUnitOptionSlice(t, output)
}
}
}
func logUnitOptionSlice(t *testing.T, opts []*UnitOption) {
for idx, opt := range opts {
t.Logf("%d: %v", idx, opt)
}
}

View File

@ -0,0 +1,36 @@
package unit
import (
"fmt"
)
type UnitOption struct {
Section string
Name string
Value string
}
func (uo *UnitOption) String() string {
return fmt.Sprintf("{Section: %q, Name: %q, Value: %q}", uo.Section, uo.Name, uo.Value)
}
func (uo *UnitOption) Match(other *UnitOption) bool {
return uo.Section == other.Section &&
uo.Name == other.Name &&
uo.Value == other.Value
}
func AllMatch(u1 []*UnitOption, u2 []*UnitOption) bool {
length := len(u1)
if length != len(u2) {
return false
}
for i := 0; i < length; i++ {
if !u1[i].Match(u2[i]) {
return false
}
}
return true
}

View File

@ -0,0 +1,200 @@
package unit
import (
"testing"
)
func TestAllMatch(t *testing.T) {
tests := []struct {
u1 []*UnitOption
u2 []*UnitOption
match bool
}{
// empty lists match
{
u1: []*UnitOption{},
u2: []*UnitOption{},
match: true,
},
// simple match of a single option
{
u1: []*UnitOption{
{Section: "Unit", Name: "Description", Value: "FOO"},
},
u2: []*UnitOption{
{Section: "Unit", Name: "Description", Value: "FOO"},
},
match: true,
},
// single option mismatched
{
u1: []*UnitOption{
{Section: "Unit", Name: "Description", Value: "FOO"},
},
u2: []*UnitOption{
{Section: "Unit", Name: "Description", Value: "BAR"},
},
match: false,
},
// multiple options match
{
u1: []*UnitOption{
{Section: "Unit", Name: "Description", Value: "FOO"},
{Section: "Unit", Name: "BindsTo", Value: "bar.service"},
{Section: "Service", Name: "ExecStart", Value: "/bin/true"},
},
u2: []*UnitOption{
{Section: "Unit", Name: "Description", Value: "FOO"},
{Section: "Unit", Name: "BindsTo", Value: "bar.service"},
{Section: "Service", Name: "ExecStart", Value: "/bin/true"},
},
match: true,
},
// mismatch length
{
u1: []*UnitOption{
{Section: "Unit", Name: "Description", Value: "FOO"},
{Section: "Unit", Name: "BindsTo", Value: "bar.service"},
},
u2: []*UnitOption{
{Section: "Unit", Name: "Description", Value: "FOO"},
{Section: "Unit", Name: "BindsTo", Value: "bar.service"},
{Section: "Service", Name: "ExecStart", Value: "/bin/true"},
},
match: false,
},
// multiple options misordered
{
u1: []*UnitOption{
{Section: "Unit", Name: "Description", Value: "FOO"},
{Section: "Service", Name: "ExecStart", Value: "/bin/true"},
},
u2: []*UnitOption{
{Section: "Service", Name: "ExecStart", Value: "/bin/true"},
{Section: "Unit", Name: "Description", Value: "FOO"},
},
match: false,
},
// interleaved sections mismatch
{
u1: []*UnitOption{
{Section: "Unit", Name: "Description", Value: "FOO"},
{Section: "Unit", Name: "BindsTo", Value: "bar.service"},
{Section: "Service", Name: "ExecStart", Value: "/bin/true"},
{Section: "Service", Name: "ExecStop", Value: "/bin/true"},
},
u2: []*UnitOption{
{Section: "Unit", Name: "Description", Value: "FOO"},
{Section: "Service", Name: "ExecStart", Value: "/bin/true"},
{Section: "Unit", Name: "BindsTo", Value: "bar.service"},
{Section: "Service", Name: "ExecStop", Value: "/bin/true"},
},
match: false,
},
}
for i, tt := range tests {
match := AllMatch(tt.u1, tt.u2)
if match != tt.match {
t.Errorf("case %d: failed comparing u1 to u2 - expected match=%t, got %t", i, tt.match, match)
}
match = AllMatch(tt.u2, tt.u1)
if match != tt.match {
t.Errorf("case %d: failed comparing u2 to u1 - expected match=%t, got %t", i, tt.match, match)
}
}
}
func TestMatch(t *testing.T) {
tests := []struct {
o1 *UnitOption
o2 *UnitOption
match bool
}{
// empty options match
{
o1: &UnitOption{},
o2: &UnitOption{},
match: true,
},
// all fields match
{
o1: &UnitOption{
Section: "Unit",
Name: "Description",
Value: "FOO",
},
o2: &UnitOption{
Section: "Unit",
Name: "Description",
Value: "FOO",
},
match: true,
},
// Section mismatch
{
o1: &UnitOption{
Section: "Unit",
Name: "Description",
Value: "FOO",
},
o2: &UnitOption{
Section: "X-Other",
Name: "Description",
Value: "FOO",
},
match: false,
},
// Name mismatch
{
o1: &UnitOption{
Section: "Unit",
Name: "Description",
Value: "FOO",
},
o2: &UnitOption{
Section: "Unit",
Name: "BindsTo",
Value: "FOO",
},
match: false,
},
// Value mismatch
{
o1: &UnitOption{
Section: "Unit",
Name: "Description",
Value: "FOO",
},
o2: &UnitOption{
Section: "Unit",
Name: "Description",
Value: "BAR",
},
match: false,
},
}
for i, tt := range tests {
match := tt.o1.Match(tt.o2)
if match != tt.match {
t.Errorf("case %d: failed comparing o1 to o2 - expected match=%t, got %t", i, tt.match, match)
}
match = tt.o2.Match(tt.o1)
if match != tt.match {
t.Errorf("case %d: failed comparing o2 to o1 - expected match=%t, got %t", i, tt.match, match)
}
}
}

View File

@ -0,0 +1,51 @@
package unit
import (
"bytes"
"io"
)
// Serialize encodes all of the given UnitOption objects into a unit file
func Serialize(opts []*UnitOption) io.Reader {
var buf bytes.Buffer
if len(opts) == 0 {
return &buf
}
curSection := opts[0].Section
writeSectionHeader(&buf, curSection)
writeNewline(&buf)
for _, opt := range opts {
if opt.Section != curSection {
curSection = opt.Section
writeNewline(&buf)
writeSectionHeader(&buf, curSection)
writeNewline(&buf)
}
writeOption(&buf, opt)
writeNewline(&buf)
}
return &buf
}
func writeNewline(buf *bytes.Buffer) {
buf.WriteRune('\n')
}
func writeSectionHeader(buf *bytes.Buffer, section string) {
buf.WriteRune('[')
buf.WriteString(section)
buf.WriteRune(']')
}
func writeOption(buf *bytes.Buffer, opt *UnitOption) {
buf.WriteString(opt.Name)
buf.WriteRune('=')
buf.WriteString(opt.Value)
}

View File

@ -0,0 +1,134 @@
package unit
import (
"io/ioutil"
"testing"
)
func TestSerialize(t *testing.T) {
tests := []struct {
input []*UnitOption
output string
}{
// no options results in empty file
{
[]*UnitOption{},
``,
},
// options with same section share the header
{
[]*UnitOption{
&UnitOption{"Unit", "Description", "Foo"},
&UnitOption{"Unit", "BindsTo", "bar.service"},
},
`[Unit]
Description=Foo
BindsTo=bar.service
`,
},
// options with same name are not combined
{
[]*UnitOption{
&UnitOption{"Unit", "Description", "Foo"},
&UnitOption{"Unit", "Description", "Bar"},
},
`[Unit]
Description=Foo
Description=Bar
`,
},
// multiple options printed under different section headers
{
[]*UnitOption{
&UnitOption{"Unit", "Description", "Foo"},
&UnitOption{"Service", "ExecStart", "/usr/bin/sleep infinity"},
},
`[Unit]
Description=Foo
[Service]
ExecStart=/usr/bin/sleep infinity
`,
},
// no optimization for unsorted options
{
[]*UnitOption{
&UnitOption{"Unit", "Description", "Foo"},
&UnitOption{"Service", "ExecStart", "/usr/bin/sleep infinity"},
&UnitOption{"Unit", "BindsTo", "bar.service"},
},
`[Unit]
Description=Foo
[Service]
ExecStart=/usr/bin/sleep infinity
[Unit]
BindsTo=bar.service
`,
},
// utf8 characters are not a problem
{
[]*UnitOption{
&UnitOption{"©", "µ☃", "ÇôrèÕ$"},
},
`[©]
µ=ÇôrèÕ$
`,
},
// no verification is done on section names
{
[]*UnitOption{
&UnitOption{"Un\nit", "Description", "Foo"},
},
`[Un
it]
Description=Foo
`,
},
// no verification is done on option names
{
[]*UnitOption{
&UnitOption{"Unit", "Desc\nription", "Foo"},
},
`[Unit]
Desc
ription=Foo
`,
},
// no verification is done on option values
{
[]*UnitOption{
&UnitOption{"Unit", "Description", "Fo\no"},
},
`[Unit]
Description=Fo
o
`,
},
}
for i, tt := range tests {
outReader := Serialize(tt.input)
outBytes, err := ioutil.ReadAll(outReader)
if err != nil {
t.Errorf("case %d: encountered error while reading output: %v", i, err)
continue
}
output := string(outBytes)
if tt.output != output {
t.Errorf("case %d: incorrect output")
t.Logf("Expected:\n%s", tt.output)
t.Logf("Actual:\n%s", output)
}
}
}

View File

@ -0,0 +1,170 @@
// Copyright 2014 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package aci implements helper functions for working with ACIs
package aci
import (
"archive/tar"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"time"
"github.com/appc/spec/aci"
"github.com/appc/spec/schema"
"golang.org/x/crypto/openpgp"
)
type ACIEntry struct {
Header *tar.Header
Contents string
}
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
// TODO(sgotti) this is a copy of appc/spec/aci.imageArchiveWriter with
// addFileNow changed to create the file with the current user. needed for
// testing as non root user.
func NewImageWriter(am schema.ImageManifest, w *tar.Writer) aci.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: os.Getuid(),
Gid: os.Getgid(),
Size: int64(buf.Len()),
ModTime: now,
Typeflag: tar.TypeReg,
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(aci.ManifestFile, aw.am); err != nil {
return err
}
return aw.Writer.Close()
}
// NewBasicACI creates a new ACI in the given directory with the given name.
// Used for testing.
func NewBasicACI(dir string, name string) (*os.File, error) {
manifest := fmt.Sprintf(`{"acKind":"ImageManifest","acVersion":"0.5.4","name":"%s"}`, name)
return NewACI(dir, manifest, nil)
}
// NewACI creates a new ACI in the given directory with the given image
// manifest and entries.
// Used for testing.
func NewACI(dir string, manifest string, entries []*ACIEntry) (*os.File, error) {
var im schema.ImageManifest
if err := im.UnmarshalJSON([]byte(manifest)); err != nil {
return nil, err
}
tf, err := ioutil.TempFile(dir, "")
if err != nil {
return nil, err
}
defer os.Remove(tf.Name())
tw := tar.NewWriter(tf)
aw := NewImageWriter(im, tw)
for _, entry := range entries {
// Add default mode
if entry.Header.Mode == 0 {
if entry.Header.Typeflag == tar.TypeDir {
entry.Header.Mode = 0755
} else {
entry.Header.Mode = 0644
}
}
// Add calling user uid and gid or tests will fail
entry.Header.Uid = os.Getuid()
entry.Header.Gid = os.Getgid()
sr := strings.NewReader(entry.Contents)
if err := aw.AddFile(entry.Header, sr); err != nil {
return nil, err
}
}
if err := aw.Close(); err != nil {
return nil, err
}
return tf, nil
}
// NewDetachedSignature creates a new openpgp armored detached signature for the given ACI
// signed with armoredPrivateKey.
func NewDetachedSignature(armoredPrivateKey string, aci io.Reader) (io.Reader, error) {
entityList, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(armoredPrivateKey))
if err != nil {
return nil, err
}
if len(entityList) < 1 {
return nil, errors.New("empty entity list")
}
signature := &bytes.Buffer{}
if err := openpgp.ArmoredDetachSign(signature, entityList[0], aci, nil); err != nil {
return nil, err
}
return signature, nil
}

View File

@ -0,0 +1,61 @@
package aci
import (
"archive/tar"
"fmt"
"github.com/appc/spec/pkg/acirenderer"
"github.com/appc/spec/schema/types"
ptar "github.com/coreos/rkt/pkg/tar"
)
// Given an imageID, start with the matching image available in the store,
// build its dependency list and render it inside dir
func RenderACIWithImageID(imageID types.Hash, dir string, ap acirenderer.ACIRegistry) error {
renderedACI, err := acirenderer.GetRenderedACIWithImageID(imageID, ap)
if err != nil {
return err
}
return renderImage(renderedACI, dir, ap)
}
// Given an image app name and optional labels, get the best matching image
// available in the store, build its dependency list and render it inside dir
func RenderACI(name types.ACName, labels types.Labels, dir string, ap acirenderer.ACIRegistry) error {
renderedACI, err := acirenderer.GetRenderedACI(name, labels, ap)
if err != nil {
return err
}
return renderImage(renderedACI, dir, ap)
}
// Given an already populated dependency list, it will extract, under the provided
// directory, the rendered ACI
func RenderACIFromList(imgs acirenderer.Images, dir string, ap acirenderer.ACIProvider) error {
renderedACI, err := acirenderer.GetRenderedACIFromList(imgs, ap)
if err != nil {
return err
}
return renderImage(renderedACI, dir, ap)
}
// Given a RenderedACI, it will extract, under the provided directory, the
// needed files from the right source ACI.
// The manifest will be extracted from the upper ACI.
// No file overwriting is done as it should usually be called
// providing an empty directory.
func renderImage(renderedACI acirenderer.RenderedACI, dir string, ap acirenderer.ACIProvider) error {
for _, ra := range renderedACI {
rs, err := ap.ReadStream(ra.Key)
if err != nil {
return err
}
defer rs.Close()
// Overwrite is not needed. If a file needs to be overwritten then the renderedACI builder has a bug
if err := ptar.ExtractTar(tar.NewReader(rs), dir, false, ra.FileMap); err != nil {
return fmt.Errorf("error extracting ACI: %v", err)
}
}
return nil
}

View File

@ -0,0 +1,190 @@
// Copyright 2014 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package lock implements simple locking primitives on a
// regular file or directory using flock
package lock
import (
"errors"
"syscall"
)
var (
ErrLocked = errors.New("file already locked")
ErrNotExist = errors.New("file does not exist")
ErrPermission = errors.New("permission denied")
ErrNotRegular = errors.New("not a regular file")
)
// FileLock represents a lock on a regular file or a directory
type FileLock struct {
path string
fd int
}
type LockType int
const (
Dir LockType = iota
RegFile
)
// TryExclusiveLock takes an exclusive lock without blocking.
// This is idempotent when the Lock already represents an exclusive lock,
// and tries promote a shared lock to exclusive atomically.
// It will return ErrLocked if any lock is already held.
func (l *FileLock) TryExclusiveLock() error {
err := syscall.Flock(l.fd, syscall.LOCK_EX|syscall.LOCK_NB)
if err == syscall.EWOULDBLOCK {
err = ErrLocked
}
return err
}
// TryExclusiveLock takes an exclusive lock on a file/directory without blocking.
// It will return ErrLocked if any lock is already held on the file/directory.
func TryExclusiveLock(path string, lockType LockType) (*FileLock, error) {
l, err := NewLock(path, lockType)
if err != nil {
return nil, err
}
err = l.TryExclusiveLock()
if err != nil {
return nil, err
}
return l, err
}
// ExclusiveLock takes an exclusive lock.
// This is idempotent when the Lock already represents an exclusive lock,
// and promotes a shared lock to exclusive atomically.
// It will block if an exclusive lock is already held.
func (l *FileLock) ExclusiveLock() error {
return syscall.Flock(l.fd, syscall.LOCK_EX)
}
// ExclusiveLock takes an exclusive lock on a file/directory.
// It will block if an exclusive lock is already held on the file/directory.
func ExclusiveLock(path string, lockType LockType) (*FileLock, error) {
l, err := NewLock(path, lockType)
if err == nil {
err = l.ExclusiveLock()
}
if err != nil {
return nil, err
}
return l, nil
}
// TrySharedLock takes a co-operative (shared) lock without blocking.
// This is idempotent when the Lock already represents a shared lock,
// and tries demote an exclusive lock to shared atomically.
// It will return ErrLocked if an exclusive lock already exists.
func (l *FileLock) TrySharedLock() error {
err := syscall.Flock(l.fd, syscall.LOCK_SH|syscall.LOCK_NB)
if err == syscall.EWOULDBLOCK {
err = ErrLocked
}
return err
}
// TrySharedLock takes a co-operative (shared) lock on a file/directory without blocking.
// It will return ErrLocked if an exclusive lock already exists on the file/directory.
func TrySharedLock(path string, lockType LockType) (*FileLock, error) {
l, err := NewLock(path, lockType)
if err != nil {
return nil, err
}
err = l.TrySharedLock()
if err != nil {
return nil, err
}
return l, nil
}
// SharedLock takes a co-operative (shared) lock on.
// This is idempotent when the Lock already represents a shared lock,
// and demotes an exclusive lock to shared atomically.
// It will block if an exclusive lock is already held.
func (l *FileLock) SharedLock() error {
return syscall.Flock(l.fd, syscall.LOCK_SH)
}
// SharedLock takes a co-operative (shared) lock on a file/directory.
// It will block if an exclusive lock is already held on the file/directory.
func SharedLock(path string, lockType LockType) (*FileLock, error) {
l, err := NewLock(path, lockType)
if err != nil {
return nil, err
}
err = l.SharedLock()
if err != nil {
return nil, err
}
return l, nil
}
// Unlock unlocks the lock
func (l *FileLock) Unlock() error {
return syscall.Flock(l.fd, syscall.LOCK_UN)
}
// Fd returns the lock's file descriptor, or an error if the lock is closed
func (l *FileLock) Fd() (int, error) {
var err error
if l.fd == -1 {
err = errors.New("lock closed")
}
return l.fd, err
}
// Close closes the lock which implicitly unlocks it as well
func (l *FileLock) Close() error {
fd := l.fd
l.fd = -1
return syscall.Close(fd)
}
// NewLock opens a new lock on a file without acquisition
func NewLock(path string, lockType LockType) (*FileLock, error) {
l := &FileLock{path: path, fd: -1}
mode := syscall.O_RDONLY | syscall.O_CLOEXEC
if lockType == Dir {
mode |= syscall.O_DIRECTORY
}
lfd, err := syscall.Open(l.path, mode, 0)
if err != nil {
if err == syscall.ENOENT {
err = ErrNotExist
} else if err == syscall.EACCES {
err = ErrPermission
}
return nil, err
}
l.fd = lfd
var stat syscall.Stat_t
err = syscall.Fstat(lfd, &stat)
if err != nil {
return nil, err
}
// Check if the file is a regular file
if lockType == RegFile && !(stat.Mode&syscall.S_IFMT == syscall.S_IFREG) {
return nil, ErrNotRegular
}
return l, nil
}

Some files were not shown because too many files have changed in this diff Show More