Browse Source

Imported Jetpaks Pod definition

layerset
Till Wegmüller 6 years ago
parent
commit
59e0a9cef7
  1. 216
      aciutil/flags.go
  2. 18
      aciutil/labels.go
  3. 23
      aciutil/utils.go
  4. 9
      aciutil/values.go
  5. 13
      cmd/imageadm/create.go
  6. 103
      drain/drain.go
  7. 119
      drain/drain_test.go
  8. 130
      image/discovery.go
  9. 76
      image/fetch.go
  10. 151
      keystore/data_for_test.go
  11. 42
      keystore/entity.go
  12. 71
      keystore/keyring.go
  13. 153
      keystore/keystore.go
  14. 273
      keystore/keystore_test.go
  15. 112
      keystore/utils.go
  16. 119
      pod/app.go
  17. 7
      pod/errors.go
  18. 26
      pod/global.go
  19. 551
      pod/host.go
  20. 105
      pod/host_mds.go
  21. 322
      pod/image.go
  22. 48
      pod/interface.go
  23. 1
      pod/main.go
  24. 102
      pod/mds.go
  25. 481
      pod/pod.go
  26. 68
      pod/utils.go
  27. 135
      run/run.go
  28. 32
      run/shell.go
  29. 53
      run/shell_test.go
  30. 20
      zfs/clone.go
  31. 2
      zfs/common.go
  32. 4
      zfs/create.go
  33. 48
      zfs/dataset.go
  34. 11
      zfs/destroy.go
  35. 9
      zfs/snap.go

216
aciutil/flags.go

@ -0,0 +1,216 @@
package aciutil
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
"os"
"path"
"runtime"
"strconv"
"strings"
"github.com/appc/spec/discovery"
"github.com/appc/spec/schema"
"github.com/appc/spec/schema/types"
)
type AnnotationsFlag types.Annotations
func (afl *AnnotationsFlag) String() string {
vv := make([]string, len(*afl))
for i, ann := range *afl {
vv[i] = fmt.Sprintf("%v=%#v", ann.Name, ann.Value)
}
return fmt.Sprintf("[%v]", strings.Join(vv, ","))
}
func (afl *AnnotationsFlag) Set(val string) error {
pieces := strings.SplitN(val, "=", 2)
if len(pieces) != 2 {
return errors.New("Annotations must be provided in NAME=VALUE format")
} else if name, err := types.NewACIdentifier(pieces[0]); err != nil {
return err
} else {
(*types.Annotations)(afl).Set(*name, pieces[1])
return nil
}
}
type ExposedPortsFlag []types.ExposedPort
func (epfl *ExposedPortsFlag) String() string {
vv := make([]string, len(*epfl))
for i, ep := range *epfl {
if ep.HostPort == 0 {
vv[i] = ep.Name.String()
} else {
vv[i] = fmt.Sprintf("%v=%v", ep.Name, ep.HostPort)
}
}
return fmt.Sprintf("[%v]", strings.Join(vv, ","))
}
func (epfl *ExposedPortsFlag) Set(val string) error {
ep := types.ExposedPort{}
pieces := strings.SplitN(val, "=", 2)
if name, err := types.NewACName(pieces[0]); err != nil {
return err
} else {
ep.Name = *name
}
if len(pieces) == 2 {
if hp, err := strconv.ParseUint(pieces[1], 10, 0); err != nil {
return err
} else {
ep.HostPort = uint(hp)
}
}
// TODO: check for duplicates? Or do we validate that later (by
// serializing & reparsing JSON)?
*epfl = append(*epfl, ep)
return nil
}
type VolumesFlag []types.Volume
func (vfl *VolumesFlag) String() string {
return fmt.Sprint(([]types.Volume)(*vfl))
}
func (vfl *VolumesFlag) Set(val string) error {
if !strings.ContainsRune(val, ',') {
if pieces := strings.SplitN(val, ":", 2); len(pieces) == 1 {
val += ",kind=empty"
} else {
val = fmt.Sprintf("%v,kind=host,source=%v", pieces[0], pieces[1])
}
}
if val[0] == '-' {
val = val[1:] + ",readOnly=true"
}
if v, err := types.VolumeFromString(val); err != nil {
return err
} else {
// TODO: check for duplicates?
*vfl = append(*vfl, *v)
return nil
}
}
type PodManifestJSONFlag schema.PodManifest
func (pmjf *PodManifestJSONFlag) String() string {
return "[PATH]"
}
func readFileOrStdin(path string) ([]byte, error) {
if path == "-" {
return ioutil.ReadAll(os.Stdin)
}
return ioutil.ReadFile(path)
}
func (pmjf *PodManifestJSONFlag) Set(val string) error {
if bb, err := readFileOrStdin(val); err != nil {
return err
} else if err := json.Unmarshal(bb, (*schema.PodManifest)(pmjf)); err != nil {
return err
} else {
return nil
}
}
type MountsFlag []schema.Mount
func (mf *MountsFlag) String() string {
return fmt.Sprint(*mf)
}
func (mf *MountsFlag) Set(val string) error {
mnt := schema.Mount{}
pieces := strings.SplitN(val, ":", 2)
if name, err := types.NewACName(pieces[0]); err != nil {
return err
} else {
mnt.Volume = *name
}
if len(pieces) == 1 {
mnt.Path = mnt.Volume.String()
} else {
mnt.Path = pieces[1]
}
*mf = append(*mf, mnt)
return nil
}
func PodManifestFlags(fl *flag.FlagSet, pm *schema.PodManifest) {
fl.Var((*PodManifestJSONFlag)(pm), "f", "Read JSON pod manifest file")
fl.Var((*AnnotationsFlag)(&pm.Annotations), "a", "Add annotation (NAME=VALUE)")
fl.Var((*ExposedPortsFlag)(&pm.Ports), "p", "Expose port (NAME[=HOST_PORT])")
fl.Var((*VolumesFlag)(&pm.Volumes), "v", "Define volume")
}
func ParseImageName(name string) (types.ACIdentifier, types.Labels, error) {
app, err := discovery.NewAppFromString(name)
if err != nil {
return "", nil, err
}
if app.Labels["os"] == "" {
app.Labels["os"] = runtime.GOOS
}
if app.Labels["arch"] == "" {
app.Labels["arch"] = runtime.GOARCH
}
labels, err := types.LabelsFromMap(app.Labels)
if err != nil {
return "", nil, err
}
return app.Name, labels, nil
}
func parseApp(args []string) ([]string, *schema.RuntimeApp, error) {
if len(args) == 0 {
return nil, nil, nil
}
rtapp := schema.RuntimeApp{}
// Parse first argument (image name)
if h, err := types.NewHash(args[0]); err == nil {
rtapp.Image.ID = *h
rtapp.Name.Set(h.String()) // won't err
} else if name, labels, err := ParseImageName(args[0]); err == nil {
rtapp.Image.Name = &name
rtapp.Name.Set(path.Base(name.String())) // won't err here
rtapp.Image.Labels = labels
} else {
return args, nil, err
}
fl := flag.NewFlagSet(args[0], flag.ExitOnError)
fl.Var(&rtapp.Name, "name", "App name")
fl.Var((*AnnotationsFlag)(&rtapp.Annotations), "a", "Add annotation (NAME=VALUE)")
fl.Var((*MountsFlag)(&rtapp.Mounts), "m", "Mount volume (VOLUME[:MOUNTPOINT])")
// TODO: app override
fl.Parse(args[1:])
return fl.Args(), &rtapp, nil
}
func ParseApps(pm *schema.PodManifest, args []string) error {
for len(args) > 0 {
if rest, rtapp, err := parseApp(args); err != nil {
return err
} else {
pm.Apps = append(pm.Apps, *rtapp)
args = rest
}
}
return nil
}

18
aciutil/labels.go

@ -0,0 +1,18 @@
package aciutil
import "github.com/appc/spec/schema/types"
// True if all labels in `labels` are present in `candidate` and have
// the same value
func MatchLabels(labels, candidate types.Labels) bool {
if len(labels) == 0 {
return true
}
cmap := candidate.ToMap()
for _, label := range labels {
if v, ok := cmap[label.Name]; !ok || v != label.Value {
return false
}
}
return true
}

23
aciutil/utils.go

@ -0,0 +1,23 @@
package aciutil
import (
"crypto/sha512"
"github.com/appc/spec/schema"
"github.com/appc/spec/schema/types"
)
func IsHashPartial(hash *types.Hash) bool {
// We assume that hash.typ == "sha512". The field is not exported
// (WHY?), so we can't double-check that.
return len(hash.Val) < sha512.Size*2
}
func IsPodManifestEmpty(pm *schema.PodManifest) bool {
return pm == nil ||
(len(pm.Apps) == 0 &&
len(pm.Volumes) == 0 &&
len(pm.Isolators) == 0 &&
len(pm.Annotations) == 0 &&
len(pm.Ports) == 0)
}

9
aciutil/values.go

@ -0,0 +1,9 @@
package aciutil
import "github.com/appc/spec/schema/types"
// Empty ACName
const ACNoName = types.ACName("")
// Empty ACIdentifier
const ACNoIdentifier = types.ACIdentifier("")

13
cmd/imageadm/create.go

@ -10,10 +10,10 @@ import (
)
var (
createImgtype string
createImgdir string
createSets []string
createNotresolve bool
createImgtype string
createImgdir string
createSets []string
createResolve bool
)
// createCmd represents the create command
@ -22,6 +22,7 @@ var createCmd = &cobra.Command{
Short: "Create an image with the following options",
Long: `Instructs imageadm to create an image as specified.
Use -t to specify a type of image to create Supported Types are Chroot|ZFS|UFS|ACI
This Will not make an Image file but rather the Files required to build one.
`,
Run: createCmdrun,
Args: cobra.MinimumNArgs(1),
@ -33,7 +34,7 @@ func init() {
createCmd.Flags().StringVarP(&createImgtype, "type", "t", "chroot", "Set this to Chroot, ZFS or UFS to specify which image to be created.")
createCmd.Flags().StringVarP(&createImgdir, "dir", "D", ".", "Which directory to create the image in defaults to ")
createCmd.Flags().StringArrayVarP(&createSets, "set", "s", []string{}, "Which Filesets to include in the final Image. use config to show the sets")
createCmd.Flags().BoolVarP(&createNotresolve, "resolve", "n", false, "If the Files should be resolved from the sets. Defaults to true")
createCmd.Flags().BoolVarP(&createResolve, "resolve", "n", true, "If the Files should be resolved from the sets. Defaults to true")
}
func createCmdrun(cmd *cobra.Command, args []string) {
@ -63,7 +64,7 @@ func createCmdrun(cmd *cobra.Command, args []string) {
profile.Devices = append(profile.Devices, config.GetAllFromSection(&section, "devices")...)
}
}
if createNotresolve == false {
if createResolve {
profile.ResolveFiles(&config)
}
err = profile.Save(imgp)

103
drain/drain.go

@ -0,0 +1,103 @@
package drain
import (
"bytes"
"errors"
"time"
)
var ErrClosed = errors.New("closed")
type Line struct {
Writer *Writer
Text string
Timestamp time.Time
}
type Drain chan Line
type Writer struct {
drain Drain
pending []byte
ts time.Time
}
func (drain Drain) NewWriter() *Writer {
return &Writer{drain: drain}
}
func (drain Drain) Lines() []Line {
var rv []Line
for line := range drain {
rv = append(rv, line)
}
return rv
}
func (w *Writer) emit(line []byte, timestamp time.Time) {
// fmt.Fprintf(os.Stderr, " EMIT: %v %#v\n", timestamp, string(line))
w.drain <- Line{w, string(line), timestamp}
}
func (w *Writer) Write(p_ []byte) (n int, err error) {
if w.drain == nil {
return 0, ErrClosed
}
ts := time.Now()
// Writer is not allowed to modify or retain the original slice. We
// may optimize it later on, but for now we just make ourselves a
// copy.
p := make([]byte, len(p_))
copy(p, p_)
// fmt.Fprintf(os.Stderr, "WRITE: %v %d:[%#v]%#v\n", ts, len(p), string(w.pending), string(p))
pending := len(w.pending) > 0
if pending {
p = append(w.pending, p...)
}
if lines := bytes.Split(p, []byte("\n")); len(lines) == 1 {
// no newline => save as pending
if !pending {
// no previously pending partial line, set timestamp
w.ts = ts
}
// If we are continuing a previously pending line, we leave the timestamp unchanged
w.pending = p
// fmt.Fprintf(os.Stderr, "-PEND: %v %#v\n", w.ts, string(w.pending))
} else {
// at least 2 lines
if pending {
// There was a pending line, emit first line with its timestamp
w.emit(lines[0], w.ts)
lines = lines[1:]
}
// at least 1 line now
for _, ln := range lines[:len(lines)-1] {
w.emit(ln, ts)
}
w.pending = lines[len(lines)-1]
w.ts = ts
// fmt.Fprintf(os.Stderr, " PEND: %v %#v\n", w.ts, string(w.pending))
}
return len(p_), nil
}
func (w *Writer) Flush() error {
if w.drain == nil {
return ErrClosed
}
if len(w.pending) > 0 {
w.emit(w.pending, w.ts)
w.pending = nil
}
return nil
}
func (w *Writer) Close() error {
w.Flush()
w.drain = nil
return nil
}

119
drain/drain_test.go

@ -0,0 +1,119 @@
package drain
import (
"math/rand"
"strings"
"sync"
"testing"
"time"
)
var jabberwocky = []byte(`Twas brillig, and the slithy toves
Did gyre and gimble in the wabe:
All mimsy were the borogoves,
And the mome raths outgrabe.
Beware the Jabberwock, my son!
The jaws that bite, the claws that catch!
Beware the Jubjub bird, and shun
The frumious Bandersnatch!
He took his vorpal sword in hand;
Long time the manxome foe he sought
So rested he by the Tumtum tree
And stood awhile in thought.
And, as in uffish thought he stood,
The Jabberwock, with eyes of flame,
Came whiffling through the tulgey wood,
And burbled as it came!
One, two! One, two! And through and through
The vorpal blade went snicker-snack!
He left it dead, and with its head
He went galumphing back.
And hast thou slain the Jabberwock?
Come to my arms, my beamish boy!
O frabjous day! Callooh! Callay!
He chortled in his joy.
Twas brillig, and the slithy toves
Did gyre and gimble in the wabe:
All mimsy were the borogoves,
And the mome raths outgrabe.
`)
func TestDrainWithOneWriter(t *testing.T) {
d := make(Drain, 128) // enough buffer to avoid having to use goroutines
w := d.NewWriter()
for i := 0; i < len(jabberwocky); i += 128 {
j := i + 128
if j > len(jabberwocky) {
j = len(jabberwocky)
}
w.Write(jabberwocky[i:j])
}
w.Close()
close(d)
ll := d.Lines()
if len(ll) != 34 {
t.Fatal("Wrong number of lines: expected 34, got", len(ll))
}
}
func TestDrainWithManyWriters(t *testing.T) {
bufferSizes := []int{
3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61,
67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131,
137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197,
199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271,
277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353,
359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433,
439, 443, 449, 457, 461, 463, 467,
}
d := make(Drain)
wg := sync.WaitGroup{}
for _, bs := range bufferSizes {
wg.Add(1)
go func(bs int) {
defer wg.Done()
w := d.NewWriter()
for i := 0; i < len(jabberwocky); i += bs {
j := i + bs
if j > len(jabberwocky) {
j = len(jabberwocky)
}
time.Sleep(time.Duration(rand.Int()%bs) * time.Millisecond)
w.Write(jabberwocky[i:j])
}
w.Close()
}(bs)
}
var ll []Line
gotLines := make(chan int)
go func() {
ll = d.Lines()
gotLines <- 1
}()
wg.Wait()
close(d)
<-gotLines
res := make(map[*Writer][]string)
for _, ln := range ll {
res[ln.Writer] = append(res[ln.Writer], ln.Text)
}
if len(res) != len(bufferSizes) {
t.Errorf("Wrong # of writers: expected %d, got %d", len(res), len(bufferSizes))
}
for _, single := range res {
if len(single) != 34 {
t.Errorf("Length of one of results not 34, but %d\n", len(single))
}
if glued := strings.Join(single, "\n") + "\n"; glued != string(jabberwocky) {
t.Errorf("One of the results is not Jabberwocky: %#v\n", glued)
}
}
}

130
image/discovery.go

@ -0,0 +1,130 @@
package image
import (
"os"
"runtime"
"github.com/appc/spec/discovery"
"github.com/appc/spec/schema/types"
"github.com/hashicorp/go-multierror"
)
func tryAppFromString(location string) *discovery.App {
if app, err := discovery.NewAppFromString(location); err != nil {
return nil
} else {
if app.Labels["os"] == "" {
app.Labels["os"] = runtime.GOOS
}
if app.Labels["arch"] == "" {
app.Labels["arch"] = runtime.GOARCH
}
return app
}
}
func OpenPubKey(location string) (types.ACIdentifier, *os.File, error) {
if app := tryAppFromString(location); app != nil {
// Proper ACIdentifier given, let's do the discovery
// TODO: hostHeaders, insecure
if pks, _, err := discovery.DiscoverPublicKeys(*app, nil, 0, 0); err != nil {
return app.Name, nil, err
} else {
// We assume multiple returned keys are alternatives, not
// multiple different valid keychains.
var err error
for _, keyurl := range pks {
if keyf, er1 := OpenLocation(keyurl); er1 != nil {
err = multierror.Append(err, er1)
} else {
return app.Name, keyf, nil
}
}
// All keys erred
return app.Name, nil, err
}
} else {
// Not an ACIdentifier, let's open as raw location
f, err := OpenLocation(location)
return "", f, err
}
}
func DiscoverACI(app discovery.App) (*os.File, *os.File, error) {
return discoverACI(app, nil)
}
func discoverACI(app discovery.App, asc *os.File) (*os.File, *os.File, error) {
var aci *os.File
// TODO: hostHeaders, insecure
if eps, _, err := discovery.DiscoverACIEndpoints(app, nil, 0, 0); err != nil {
return nil, nil, err
} else {
var err error
if asc == nil {
err = nil
for _, ep := range eps {
if af, er1 := OpenLocation(ep.ASC); er1 != nil {
err = multierror.Append(err, er1)
} else {
asc = af
break
}
}
if err != nil {
return nil, nil, err
}
}
err = nil
for _, ep := range eps {
if af, er1 := OpenLocation(ep.ACI); er1 != nil {
err = multierror.Append(err, er1)
} else {
aci = af
break
}
if aci == nil {
if asc != nil {
asc.Close()
}
return nil, nil, err
}
}
return aci, asc, nil
}
}
func OpenACI(location, sigLocation string) (types.ACIdentifier, *os.File, *os.File, error) {
var asc *os.File
// Signature override
if sigLocation != "" {
if sf, err := OpenLocation(sigLocation); err != nil {
return "", nil, nil, err
} else {
asc = sf
}
}
if app := tryAppFromString(location); app != nil {
// Proper ACIdentifier given, let's do discovery
if aci, asc, err := discoverACI(*app, asc); err != nil {
return app.Name, nil, nil, err
} else {
return app.Name, aci, asc, nil
}
} else {
if aci, err := OpenLocation(location); err != nil {
if asc != nil {
asc.Close()
}
return "", nil, nil, err
} else {
return "", aci, asc, nil
}
}
}

76
image/fetch.go

@ -0,0 +1,76 @@
package image
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
)
func OpenURL(url string) (_ *os.File, erv error) {
tf, err := ioutil.TempFile("", "jetpack.fetch.")
if err != nil {
return nil, err
}
os.Remove(tf.Name()) // no need to keep the tempfile around
defer func() {
if erv != nil {
tf.Close()
}
}()
res, err := http.Get(url)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad HTTP status code: %d", res.StatusCode)
}
fmt.Println("Downloading", url, "...")
if _, err := io.Copy(tf, res.Body); err != nil {
return nil, err
}
tf.Seek(0, io.SeekStart)
return tf, nil
}
const flagAllowHTTP = false // TODO: make the flag
func OpenLocation(location string) (_ *os.File, erv error) {
if location == "-" {
return os.Stdin, nil
}
u, err := url.Parse(location)
if err != nil {
return nil, err
}
switch u.Scheme {
case "":
return os.Open(location)
case "file":
return os.Open(u.Path)
case "http":
if !flagAllowHTTP {
return nil, fmt.Errorf("-insecure-allow-http required for http URLs")
}
fallthrough
case "https":
return OpenURL(u.String())
default:
return nil, fmt.Errorf("unsupported scheme: %v\n", u.Scheme)
}
}

151
keystore/data_for_test.go

@ -0,0 +1,151 @@
package keystore
import (
"fmt"
"io/ioutil"
"os"
)
var sampleKeys = []string{
// 3ofcoins.net provisional key
`-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v2
mQENBFVnXGIBCADovl1Yw/Ftjz7QZ9qVEUmJ9ztduws50yk+RbY+R1UJQvLhWU10
izORyN1qrXTYrQKoiJDJ9eTc7WHqrJGsNrgZWGc5SvMTdfVtiRdfaKP5QrIgXI5/
EZwZjRgi3ty/hsq2RvpNkvC5cXp4sSYf63dEFFLD4Ps3G+Lc4adKyNn8gZSieHjv
0+aizg7DW+mqNxUy3NK5wkRo876EYbhVpGCZRxPq39p88wqb6j1Rkt68jNtMOWkA
euoCxfeXR9jbU7ArNFxmhg6/ND7Zq60PAC5aImBLolXXMAK62cBPJBTZulNXSNxF
iZZBtFl5COyQNHSwbZUOv4jdrOiwE894qC8NABEBAAG0MkFDSSAzb2Zjb2lucy5u
ZXQgKHByb3Zpc2lvbmFsKSA8aW5mb0Azb2Zjb2lucy5uZXQ+iQE3BBMBCAAhBQJV
Z1xiAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheAAAoJEFPMwtY6FiZkYXEIAJiM
bGvBOIrjIKhW0y6syRIGuypkJw7dhp9EvMwzsvMLM86ze7iX2gNaBOGKWMdIRAYH
gKLN+f65ZOviOY5yy6fMG5A9ySlSWIKiLPDX6X/pXwMhmSUBkwhWKUWXIHd0+3d4
lGUWzzimTFfjWNlPWbHb08HMepMuNtCxtKkxvRgpH0yaWNCHQe3NsWAh6nYLsXpJ
A/2lomQ9K/qhHyZT18eYD1lHCpEOPksqifv5BKAFpXekGsFjTbYIKj6bgspaMg+d
GHMg3KUnCulgzhhbo0bMyMqrHm8waSF/JabTiIqp/ePFiUWqeirBdZ8yje6TA9eS
7UMOx+veKUT6Y5axbPaJARsEEAEIAAYFAlVnXLYACgkQRjqg8fR27d7nsAf3cxHv
Q+ljIyTqzxHDCgMmMLFPd8zzZ6cBTUYgb8R2G8EjYVg5Qu9zwv3FOZWaY19hBYDP
AzXjQtjkdQiwGvOppgui4CmnjJpNlyPd5J2A06fyeg2F28WJacrxUdTre1/UwN+c
9UUJo9o3OluVbaq1o9rXJnau79HOqWDoh8VdTzZAd3VO4SXuI7GUP49j6NC1t8FO
I7Qe0TLHzkZDeiZmVlK5OhfwhzMBQ5Qjv/9iT47Od91ryXKrHNAICyT32K4zawc0
/xs0oAQb5MOE4HrxW46e8UCT+2u+GrtS5ftWQqAMqnBF9oiRpSfXis3XgFyP5nU6
+ZaUESTObMoZyZ67uQENBFVnXGIBCADE19yLRCcO0NNBvDxWVFdFLsDKzl7vAQWm
cjaWBPJi0pWbi+vppECSjf8S5NNUXqx8MObG4i+ivBR4dqPbVls7Lpzn9wt3MwPZ
RbCLPLvBPJtlYtIt76JlqcjuJws3kd5hmX/1gny8Vy9RoNWt4y1ldde4+mJXGHoW
OY3CHj1vagkRE445czYT+ST8xeSxLKAyAEHis3U6Gb80uHbRknt/bUaogbibrl9N
3MyRX56cctVqpY8bJTi1PDKpSNcUHT+LMQ6rpPc2anKVqoUHT9Ye6hLSvNeBwJHs
LLWEWNRfmjjZvHs4hfzzaUVT0bP2IMkMBpxMCAOEargaJYhD1olFABEBAAGJAR8E
GAEIAAkFAlVnXGICGwwACgkQU8zC1joWJmTROAgA4RBWZww5CaBZ2ZbBijNRK8sa
swUrdxxcAkRzki2Z5KpBXESZEe6iPZt/rsGNw0F2XaORP+DRrzTGg+DSQlh5nT2z
LFVkNyeW3RJQjP2K2avikjRgoprryr/WVUOTpk4jK/MpBxUU0Htpvks2Jly2vYJD
Z9R2yW31RLEJXcVxOR/LcW6UfkjhOsH/RbKbcMx+HraxKk7jmrKZEglz8cO0BMGc
gemHpMb04W4YXOdKjqppm0kS+TDTEm85xnfAe14olr3ZsHn/ey8nc3JEb3aCBeuC
PjkMkIAW7BYX1ZKyV9z5EQVkqkz7zwyZYO1/tmdjQQH/x+mD0MwpeIF0E78YhA==
=m9io
-----END PGP PUBLIC KEY BLOCK-----
`,
// quay.io signing key
`-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v2
mQENBFTT6doBCACkVncI+t4HASQdnByRlXCYkwjsPqGOlgTCgenop5I6vgTqFWhQ
PMNhtSaFdFECMt2WKQT4QGVbfVOmIH9CLV+Muqvk4iJIAn3Nh3qp/kfMhwjGaS6m
fWN2ARFCq4RIs9tboCNQOouaD5C26/FsQtIsoqyYcdX+YFaU1a+R1kp0fc2CABDI
k6Iq8oEJO+FOYvqQYIJNfd3c0NHICilMu2jO3yIsw80qzWoFAAblyb0zVq/hudWB
4vdVzPmJe1f4Ymk8l1R413bN65LcbCiOax3hmFWovJoxlkL7WoGTTMfaeb2QmaPL
qcu4Q94v1KG87gyxbkIo5uZdvMLdswQI7yQ7ABEBAAG0RFF1YXkuaW8gQUNJIENv
bnZlcnRlciAoQUNJIGNvbnZlcnNpb24gc2lnbmluZyBrZXkpIDxzdXBwb3J0QHF1
YXkuaW8+iQE5BBMBAgAjBQJU0+naAhsDBwsJCAcDAgEGFQgCCQoLBBYCAwECHgEC
F4AACgkQcqv19nmdM7zKzggAjGFqy7Hcx6TCFXn53/inl5iyKrTu8cuF4K547XuZ
12Dt8b6PgJ+b3z6UnMMTd0wXKGcfOmNeQ2R71xmVnviuo7xB5ZkZIBxHI4M/5uhK
I6GZKr84WJS2ec7ssH2ofFQ5u1l+es9jUwW0KbAoNmES0IcdDy28xfmJpkfOn3oI
P2Bzz4rGlIqJXEjq28Wk+qQu64kJRKYuPNXqiHncPDm+i5jMXUUN1D+pkDukp26x
oLbpol42/jIcM3fe2AFZnflittBCHYLIHjJ51NlpSHJZmf2pQZbdyeKElN2SCNe7
nDcol24zYIC+SX0K23w/LrLzlff4mzbO99ePt1bB9zAiVA==
=SBoV
-----END PGP PUBLIC KEY BLOCK-----
`,
// coreos.com/etcd
`-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1
mQINBFTCnMQBEAC/49bGbStCpa3peej+/42mobfuGbTcdmcGGwYZmigP0Kl0TPZK
zcIxhVZOr3ITYuAx8T1WWJVb7/r/y4roUogDrUSTm2nAbLP8xp8Qn/N1zaXFyEtJ
WTaLuPI2mq643x8g7fAiJY19JRjFbYVVZLr5OMOvHrOdtYVN31ARZeSxmqP5yFNW
9DgoXG0/w80EOXIsWWoJgjaKLye15LHI86MjPthXyT5K212efxPffSk1hca04Dmk
I5vCMHC1Q2DbrlilhS0DTf+lSK2YgkaHPWiNSZb3XvjwbU8qQMzyfnQcQrQlfm4/
1fHLj2bWyASzNG/MOJCQ2JyEyIzbS2M4jKcfFxaKKuJA5PwdfjbRTkvxAKbFcdc5
ER7D3QoEOxgRDMppHaihKNI/T4dPIuqyUczq3ia9fGfrQFROAIxdAnBqzzBVaetv
FYFVjJlAhGsWWEhuO3P7qGwwR7CtPkWvvsMT8CYdHP2h7uOrOZioGCQ+09YBGpJ9
LzwCKHiV2s4/aBVfLhjttGqXG+PW/Kzg5rtwAjSdeooThKQLQk/ok1TtFydgNVNH
kSPNdhgiTWlNmK8Qj3C1zqZmcPzv+c6y6f79GTL0+Hz6gqnMGdIpFJtmbusU79/2
MkDqumBAslvwm7h85s0ccKwZCG1VelyhGLawVyxin0UhLWlzYd6SL5IuuQARAQAB
tCdDb3JlT1MgQUNJIEJ1aWxkZXIgPHJlbGVhc2VAY29yZW9zLmNvbT6JAjgEEwEC
ACIFAlTCnMQCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEFIQvYiIGCGQ
nxYP+wcVClXD1t5oT7mwvYZPfF9/+itOSHvN6++T4CCFkRVdIN7/G/Ou1wRb/fV+
P27Rc7gnK+jbQJqUa8aEsNSWZT/1A8VaQ51orQdV80ZROzrJPLBB0w4fkEsSESO+
Uuz9ZsiEOhZf0ATkafrF1jfepGXmoHJxLJ+bKS+KlAEhtceB1jiWAafGPy99XUAf
MBJ905F6bXvDqQov+9U3vyUGnwA6ymCCqKIoCVx3GdKOh5UaqC8pHaF7oI7xkv5b
5WMajzuXwKBy6KoduHTnW7Y5g1aoGwW5FDFoEq4LsBvcxeUI6OMVEWCVe502E+4S
lWx2gEvFa33wy77kYp2ZvHToY5tSjiIi8QocR0IgfLqE3P1ZMPe9YXk5EveEZH6Q
VtQ8z1ktuZCVQqrtTEeernEdSFsTVFSoWUsNJV1FMlgisZZ0ljoFJrH7/7GkYhK9
DT7OcrZnyZDUkEIiVaqWwjWw5Ing4IHExAq+PlXDwrA0QcH2UW9IurDCvPiXHpxi
D0V21oEbANUdGtYcOSDRBbZlsonINJZQ1Ad2XrY8kfZ2sZXAZuBZbH0+dEx6zmua
C0IGN3SLFseScqJZ5G1joYYqOKOUweErkzA/62Kaj31SVoQDpZyMqtwTjjZaFT8N
fMkBtaM3knaFonHZc19BD1FOishRThCCq2Ty8HUoN2Fk7w0l
=bYl7
-----END PGP PUBLIC KEY BLOCK-----`,
}
var sampleKeyFingerprints = []string{
"4706dc5d5c214bc3ad127c6d53ccc2d63a162664",
"bff313cdaa560b16a8987b8f72abf5f6799d33bc",
"8b86de38890ddb7291867b025210bd8888182190",
}
func asFile(args ...interface{}) (_ *os.File, erv error) {
if f, err := ioutil.TempFile("", "jetpack.test.keystore."); err != nil {
return nil, err
} else {
defer func() {
if erv != nil {
f.Close()
}
}()
if err := os.Remove(f.Name()); err != nil {
return nil, err
}
if _, err := fmt.Fprint(f, args...); err != nil {
return nil, err
}
if _, err := f.Seek(0, 0); err != nil {
return nil, err
}
return f, nil
}
}
var openedSampleKeys = make(map[int]*os.File)
func openSampleKey(i int) *os.File {
if f := openedSampleKeys[i]; f != nil {
if _, err := f.Seek(0, 0); err != nil {
panic(err)
}
return f
}
if f, err := asFile(sampleKeys[i]); err != nil {
panic(err)
} else {
openedSampleKeys[i] = f
return f
}
}
func closeSampleKeys() {
for i, f := range openedSampleKeys {
f.Close()
delete(openedSampleKeys, i)
}
}

42
keystore/entity.go

@ -0,0 +1,42 @@
package keystore
import (
"fmt"
"path/filepath"
"sort"
"strings"
"github.com/appc/spec/schema/types"
"golang.org/x/crypto/openpgp"
)
type Entity struct {
*openpgp.Entity
Path string
Prefix types.ACIdentifier
}
func (e *Entity) Fingerprint() string {
return filepath.Base(e.Path)
}
func (e *Entity) String() string {
identities := make([]string, 0, len(e.Entity.Identities))
for name := range e.Entity.Identities {
identities = append(identities, name)
}
sort.Strings(identities)
return fmt.Sprintf("%v\t%v\t%v", e.Prefix, e.Fingerprint(), strings.Join(identities, "; "))
}
type EntityList []Entity
// sort.Interface
func (ee EntityList) Len() int { return len(ee) }
func (ee EntityList) Less(i, j int) bool {
if ee[i].Prefix == ee[j].Prefix {
return ee[i].Path < ee[j].Path
}
return ee[i].Prefix.String() < ee[j].Prefix.String()
}
func (ee EntityList) Swap(i, j int) { ee[i], ee[j] = ee[j], ee[i] }

71
keystore/keyring.go

@ -0,0 +1,71 @@
package keystore
import (
"fmt"
"os"
"path/filepath"
"github.com/appc/spec/schema/types"
"golang.org/x/crypto/openpgp"
)
type Keyring struct {
openpgp.EntityList
paths []string
prefixes []types.ACIdentifier
}
func (kr *Keyring) loadFile(path string) error {
trustedKey, err := os.Open(path)
if err != nil {
return err
}
defer trustedKey.Close()
entityList, err := openpgp.ReadArmoredKeyRing(trustedKey)
if err != nil {
return err
}
if len(entityList) < 1 {
return fmt.Errorf("missing opengpg entity")
}
fingerprint := fingerprintToFilename(entityList[0].PrimaryKey.Fingerprint)
keyFile := filepath.Base(trustedKey.Name())
if fingerprint != keyFile {
return fmt.Errorf("fingerprint mismatch: %q:%q", keyFile, fingerprint)
}
prefix, err := pathToACIdentifier(path)
if err != nil {
return err
}
kr.EntityList = append(kr.EntityList, entityList[0])
kr.paths = append(kr.paths, path)
kr.prefixes = append(kr.prefixes, prefix)
return nil
}
func (kr *Keyring) Entities() EntityList {
rv := make(EntityList, len(kr.EntityList))
for i, e := range kr.EntityList {
rv[i] = Entity{e, kr.paths[i], kr.prefixes[i]}
}
return rv
}
// sort.Interface - sort by prefix, then by path
func (kr *Keyring) Len() int { return len(kr.EntityList) }
func (kr *Keyring) Less(i, j int) bool {
if kr.prefixes[i] == kr.prefixes[j] {
return kr.paths[i] < kr.paths[j]
}
return kr.prefixes[i].String() < kr.prefixes[j].String()
}
func (kr *Keyring) Swap(i, j int) {
kr.EntityList[i], kr.EntityList[j] = kr.EntityList[j], kr.EntityList[i]
kr.paths[i], kr.paths[j] = kr.paths[j], kr.paths[i]
kr.prefixes[i], kr.prefixes[j] = kr.prefixes[j], kr.prefixes[i]
}

153
keystore/keystore.go

@ -0,0 +1,153 @@
package keystore
// Heavily based on https://github.com/coreos/rkt/blob/master/pkg/keystore/keystore.go
// We don't use rkt's keystore, because we want to escape ACIdentifier to
// avoid path traversal issues and not to worry about prefix
// collisions.
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"golang.org/x/crypto/openpgp"
"github.com/appc/spec/schema/types"
)
// Intentionally invalid ACIdentifier to mark root key
const Root = types.ACIdentifier("@")
type Keystore struct {
Path string
}
func New(path string) *Keystore {
return &Keystore{path}
}
func (ks *Keystore) prefixPath(prefix types.ACIdentifier) string {
if prefix.Empty() {
panic("Empty prefix!")
}
return filepath.Join(ks.Path, strings.Replace(string(prefix), "/", ",", -1))
}
func (ks *Keystore) StoreTrustedKey(prefix types.ACIdentifier, key *os.File, acceptFingerprint string) (string, error) {
if prefix.Empty() {
panic("Empty prefix!")
}
if accepted, err := reviewKey(prefix, key, acceptFingerprint); err != nil {
return "", err
} else if !accepted {
return "", nil
}
pubkeyBytes, err := ioutil.ReadAll(key)
if err != nil {
return "", err
}
dir := ks.prefixPath(prefix)
if err := os.MkdirAll(dir, 0750); err != nil {
return "", err
}
entityList, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(pubkeyBytes))
if err != nil {
return "", err
}
// FIXME: cargo cult from rkt
// FIXME: can we import more than one key here, and note only one?
// Maybe we should split and re-armor the entityList?
trustedKeyPath := filepath.Join(dir, fingerprintToFilename(entityList[0].PrimaryKey.Fingerprint))
if err := ioutil.WriteFile(trustedKeyPath, pubkeyBytes, 0640); err != nil {
return "", err
}
return trustedKeyPath, nil
}
func (ks *Keystore) UntrustKey(fingerprint string) (removed []types.ACIdentifier, err error) {
err = ks.walk("", func(prefix types.ACIdentifier, path string) error {
if filepath.Base(path) == fingerprint {
if err := os.Remove(path); err != nil {
return err
}
removed = append(removed, prefix)
}
return nil
})
return
}
func (ks *Keystore) walk(name types.ACIdentifier, fn func(prefix types.ACIdentifier, path string) error) error {
var namePath string
if !name.Empty() {
namePath = ks.prefixPath(name)
}
return filepath.Walk(ks.Path, func(path string, fi os.FileInfo, err error) error {
if err != nil && !os.IsNotExist(err) {
return err
}
if fi == nil {
return nil
}
if fi.IsDir() {
if namePath == "" || strings.HasPrefix(namePath, path) || fi.Name() == "@" {
return nil
} else {
return filepath.SkipDir
}
}
if prefix, err := pathToACIdentifier(path); err != nil {
return err
} else {
return fn(prefix, path)
}
})
}
func walkLoaderFn(kr *Keyring) func(types.ACIdentifier, string) error {
return func(_ types.ACIdentifier, path string) error {
return kr.loadFile(path)
}
}
func (ks *Keystore) GetAllKeys() (*Keyring, error) {
kr := &Keyring{}
if err := ks.walk("", walkLoaderFn(kr)); err != nil {
return nil, err
}
return kr, nil
}
func (ks *Keystore) GetKeysFor(name types.ACIdentifier) (*Keyring, error) {
kr := &Keyring{}
if err := ks.walk(name, walkLoaderFn(kr)); err != nil {
return nil, err
}
return kr, nil
}
func (ks *Keystore) CheckSignature(name types.ACIdentifier, signed, signature io.Reader) (*openpgp.Entity, error) {
kr, err := ks.GetKeysFor(name)
if err != nil {
return nil, err
}
entities, err := openpgp.CheckArmoredDetachedSignature(kr, signed, signature)
if err == io.EOF {
err = fmt.Errorf("no signatures found")
}
return entities, err
}

273
keystore/keystore_test.go

@ -0,0 +1,273 @@
package keystore
import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"sort"
"testing"
"github.com/appc/spec/schema/types"
)
func newStore() *Keystore {
storePath, err := ioutil.TempDir(".", "test.store.")
if err != nil {
panic(err)
}
return New(storePath)
}
func testImport(t *testing.T, prefix types.ACIdentifier, subdir string) {
// Redirect stdout (how to DRY?)
origStdout := os.Stdout
defer func() { os.Stdout = origStdout }()
if devnull, err := os.Create("/dev/null"); err != nil {
panic(err)
} else {
os.Stdout = devnull
defer devnull.Close()
}
defer closeSampleKeys()
ks := newStore()
defer os.RemoveAll(ks.Path)
for i, key := range sampleKeys {
fingerprint := sampleKeyFingerprints[i]
keyPath, err := ks.StoreTrustedKey(prefix, openSampleKey(i), fingerprint)
if err != nil {
t.Errorf("Error storing key #%d: %v\n", i, err)
}
expectedPath := filepath.Join(ks.Path, subdir, fingerprint)
if keyPath != expectedPath {
t.Errorf("Unexpected key path: %v, expected %v (key %d, store %v, prefix %v, fingerprint %v)\n",
keyPath, expectedPath, i, ks.Path, prefix, fingerprint)
}
if keyBytes, err := ioutil.ReadFile(keyPath); err != nil {
t.Errorf("Error reading back saved key %d: %v", i, err)
} else if string(keyBytes) != key {
t.Errorf("Saved key %d different than original", i)
}
}
}
func TestImportRoot(t *testing.T) {
testImport(t, Root, "@")
}
func TestImportPrefix(t *testing.T) {
testImport(t, types.ACIdentifier("example.com"), "example.com")
}
func TestImportPrefixEscaped(t *testing.T) {
testImport(t, types.ACIdentifier("example.com/foo"), "example.com,foo")
}
func checkKeyCount(t *testing.T, ks *Keystore, expected map[types.ACIdentifier]int) {
for name, expectedKeys := range expected {
if kr, err := ks.GetKeysFor(name); err != nil {
t.Errorf("Error getting keyring for %v: %v\n", name, err)
} else if actualKeys := kr.Len(); actualKeys != expectedKeys {
t.Errorf("Expected %d keys for %v, got %d instead\n", expectedKeys, name, actualKeys)
}
}
}
func TestGetKeyring(t *testing.T) {
// Redirect stdout (how to DRY?)
origStdout := os.Stdout
defer func() { os.Stdout = origStdout }()
if devnull, err := os.Create("/dev/null"); err != nil {
panic(err)
} else {
os.Stdout = devnull
defer devnull.Close()
}
ks := newStore()
defer os.RemoveAll(ks.Path)
defer closeSampleKeys()
if _, err := ks.StoreTrustedKey(types.ACIdentifier("example.com/foo"), openSampleKey(0), sampleKeyFingerprints[0]); err != nil {
t.Errorf("Error storing key: %v\n", err)
}
if _, err := ks.StoreTrustedKey(types.ACIdentifier("example.com/foo/bar"), openSampleKey(1), sampleKeyFingerprints[1]); err != nil {
t.Errorf("Error storing key: %v\n", err)
}
checkKeyCount(t, ks, map[types.ACIdentifier]int{
types.ACIdentifier("eggsample.com"): 0,
types.ACIdentifier("eggsample.com/foo"): 0,
types.ACIdentifier("eggsample.com/foo/bar"): 0,
types.ACIdentifier("example.com"): 0,
types.ACIdentifier("example.com/foo"): 1,
types.ACIdentifier("example.com/foo/baz"): 1,
types.ACIdentifier("example.com/foo/bar"): 2,
types.ACIdentifier("example.com/foo/bar/baz"): 2,
types.ACIdentifier("example.com/foobar"): 1,
types.ACIdentifier("example.com/baz"): 0,
})
if _, err := ks.StoreTrustedKey(Root, openSampleKey(2), sampleKeyFingerprints[2]); err != nil {
t.Errorf("Error storing key: %v\n", err)
}
checkKeyCount(t, ks, map[types.ACIdentifier]int{
types.ACIdentifier("eggsample.com"): 1,
types.ACIdentifier("eggsample.com/foo"): 1,
types.ACIdentifier("eggsample.com/foo/bar"): 1,
types.ACIdentifier("example.com"): 1,
types.ACIdentifier("example.com/foo"): 2,
types.ACIdentifier("example.com/foo/baz"): 2,
types.ACIdentifier("example.com/foo/bar"): 3,
types.ACIdentifier("example.com/foo/bar/baz"): 3,
types.ACIdentifier("example.com/foobar"): 2,
types.ACIdentifier("example.com/baz"): 1,
})
}
func countKeys(kr *Keyring) map[types.ACIdentifier]int {
rv := make(map[types.ACIdentifier]int)
for _, prefix := range kr.prefixes {
rv[prefix] = rv[prefix] + 1
}
return rv
}
func TestGetAllKeyrings(t *testing.T) {
// Redirect stdout (how to DRY?)
origStdout := os.Stdout
defer func() { os.Stdout = origStdout }()
if devnull, err := os.Create("/dev/null"); err != nil {
panic(err)
} else {
os.Stdout = devnull
defer devnull.Close()
}
ks := newStore()
defer os.RemoveAll(ks.Path)
defer closeSampleKeys()
prefix := types.ACIdentifier("example.com/foo")
if _, err := ks.StoreTrustedKey(prefix, openSampleKey(0), sampleKeyFingerprints[0]); err != nil {
t.Errorf("Error storing key: %v\n", err)
}
if _, err := ks.StoreTrustedKey(prefix, openSampleKey(1), sampleKeyFingerprints[1]); err != nil {
t.Errorf("Error storing key: %v\n", err)
}
if _, err := ks.StoreTrustedKey(Root, openSampleKey(2), sampleKeyFingerprints[2]); err != nil {
t.Errorf("Error storing key: %v\n", err)
}
kr, err := ks.GetAllKeys()
if err != nil {
t.Errorf("Error getting all keyrings: %v\n", err)
t.FailNow()
}
kc := countKeys(kr)
if len(kc) != 2 {
t.Errorf("Got %d keyrings, expected 2: %v\n", len(kc), kc)
}
if rkc, ok := kc[Root]; !ok {
t.Error("No root keyring")
} else if rkc != 1 {
t.Error("Root keyring %d long, expected 1\n", rkc)
}
if pkc, ok := kc[prefix]; !ok {
t.Error("No prefix keyring")
} else if pkc != 2 {
t.Error("Prefix keyring %d long, expected 2\n", kc)
}
}
type acNames []types.ACIdentifier
// sort.Interface
func (acn acNames) Len() int { return len(acn) }
func (acn acNames) Less(i, j int) bool { return acn[i].String() < acn[j].String() }
func (acn acNames) Swap(i, j int) { acn[i], acn[j] = acn[j], acn[i] }
func TestUntrustKey(t *testing.T) {
// Redirect stdout (how to DRY?)
origStdout := os.Stdout
defer func() { os.Stdout = origStdout }()
if devnull, err := os.Create("/dev/null"); err != nil {