35 changed files with 3641 additions and 12 deletions
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
@ -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("") |
@ -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 |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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] } |
@ -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] |
||||
} |
@ -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 |
||||
} |
@ -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 { |
||||