diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index bbf84e6969..436fd280a9 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,6 +1,6 @@ { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes", - "GoVersion": "go1.3", + "GoVersion": "go1.3.1", "Packages": [ "./..." ], @@ -81,6 +81,10 @@ "ImportPath": "github.com/google/gofuzz", "Rev": "aef70dacbc78771e35beb261bb3a72986adf7906" }, + { + "ImportPath": "github.com/kr/text", + "Rev": "6807e777504f54ad073ecef66747de158294b639" + }, { "ImportPath": "github.com/mitchellh/goamz/aws", "Rev": "9cad7da945e699385c1a3e115aa255211921c9bb" @@ -89,6 +93,20 @@ "ImportPath": "github.com/mitchellh/goamz/ec2", "Rev": "9cad7da945e699385c1a3e115aa255211921c9bb" }, + { + "ImportPath": "github.com/mitchellh/mapstructure", + "Rev": "740c764bc6149d3f1806231418adb9f52c11bcbf" + }, + { + "ImportPath": "github.com/racker/perigee", + "Comment": "v0.0.0-18-g0c00cb0", + "Rev": "0c00cb0a026b71034ebc8205263c77dad3577db5" + }, + { + "ImportPath": "github.com/rackspace/gophercloud", + "Comment": "v0.1.0-31-ge13cda2", + "Rev": "e13cda260ce48d63ce816f4fa72b6c6cd096596d" + }, { "ImportPath": "github.com/skratchdot/open-golang/open", "Rev": "ba570a111973b539baf23c918213059543b5bb6e" @@ -105,6 +123,11 @@ "ImportPath": "github.com/stretchr/testify/mock", "Rev": "37614ac27794505bf7867ca93aac883cadb6a5f7" }, + { + "ImportPath": "github.com/tonnerre/golang-pretty", + "Comment": "debian/0.0_git20130613-1-1-ge7fccc0", + "Rev": "e7fccc03e91bad289b96c21aa3312a220689bdd7" + }, { "ImportPath": "github.com/vaughan0/go-ini", "Rev": "a98ad7ee00ec53921f08832bc06ecf7fd600e6a1" diff --git a/Godeps/_workspace/src/github.com/kr/text/License b/Godeps/_workspace/src/github.com/kr/text/License new file mode 100644 index 0000000000..480a328059 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/License @@ -0,0 +1,19 @@ +Copyright 2012 Keith Rarick + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/kr/text/Readme b/Godeps/_workspace/src/github.com/kr/text/Readme new file mode 100644 index 0000000000..7e6e7c0687 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/Readme @@ -0,0 +1,3 @@ +This is a Go package for manipulating paragraphs of text. + +See http://go.pkgdoc.org/github.com/kr/text for full documentation. diff --git a/Godeps/_workspace/src/github.com/kr/text/colwriter/Readme b/Godeps/_workspace/src/github.com/kr/text/colwriter/Readme new file mode 100644 index 0000000000..1c1f4e6839 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/colwriter/Readme @@ -0,0 +1,5 @@ +Package colwriter provides a write filter that formats +input lines in multiple columns. + +The package is a straightforward translation from +/src/cmd/draw/mc.c in Plan 9 from User Space. diff --git a/Godeps/_workspace/src/github.com/kr/text/colwriter/column.go b/Godeps/_workspace/src/github.com/kr/text/colwriter/column.go new file mode 100644 index 0000000000..7302ce9f7a --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/colwriter/column.go @@ -0,0 +1,147 @@ +// Package colwriter provides a write filter that formats +// input lines in multiple columns. +// +// The package is a straightforward translation from +// /src/cmd/draw/mc.c in Plan 9 from User Space. +package colwriter + +import ( + "bytes" + "io" + "unicode/utf8" +) + +const ( + tab = 4 +) + +const ( + // Print each input line ending in a colon ':' separately. + BreakOnColon uint = 1 << iota +) + +// A Writer is a filter that arranges input lines in as many columns as will +// fit in its width. Tab '\t' chars in the input are translated to sequences +// of spaces ending at multiples of 4 positions. +// +// If BreakOnColon is set, each input line ending in a colon ':' is written +// separately. +// +// The Writer assumes that all Unicode code points have the same width; this +// may not be true in some fonts. +type Writer struct { + w io.Writer + buf []byte + width int + flag uint +} + +// NewWriter allocates and initializes a new Writer writing to w. +// Parameter width controls the total number of characters on each line +// across all columns. +func NewWriter(w io.Writer, width int, flag uint) *Writer { + return &Writer{ + w: w, + width: width, + flag: flag, + } +} + +// Write writes p to the writer w. The only errors returned are ones +// encountered while writing to the underlying output stream. +func (w *Writer) Write(p []byte) (n int, err error) { + var linelen int + var lastWasColon bool + for i, c := range p { + w.buf = append(w.buf, c) + linelen++ + if c == '\t' { + w.buf[len(w.buf)-1] = ' ' + for linelen%tab != 0 { + w.buf = append(w.buf, ' ') + linelen++ + } + } + if w.flag&BreakOnColon != 0 && c == ':' { + lastWasColon = true + } else if lastWasColon { + if c == '\n' { + pos := bytes.LastIndex(w.buf[:len(w.buf)-1], []byte{'\n'}) + if pos < 0 { + pos = 0 + } + line := w.buf[pos:] + w.buf = w.buf[:pos] + if err = w.columnate(); err != nil { + if len(line) < i { + return i - len(line), err + } + return 0, err + } + if n, err := w.w.Write(line); err != nil { + if r := len(line) - n; r < i { + return i - r, err + } + return 0, err + } + } + lastWasColon = false + } + if c == '\n' { + linelen = 0 + } + } + return len(p), nil +} + +// Flush should be called after the last call to Write to ensure that any data +// buffered in the Writer is written to output. +func (w *Writer) Flush() error { + return w.columnate() +} + +func (w *Writer) columnate() error { + words := bytes.Split(w.buf, []byte{'\n'}) + w.buf = nil + if len(words[len(words)-1]) == 0 { + words = words[:len(words)-1] + } + maxwidth := 0 + for _, wd := range words { + if n := utf8.RuneCount(wd); n > maxwidth { + maxwidth = n + } + } + maxwidth++ // space char + wordsPerLine := w.width / maxwidth + if wordsPerLine <= 0 { + wordsPerLine = 1 + } + nlines := (len(words) + wordsPerLine - 1) / wordsPerLine + for i := 0; i < nlines; i++ { + col := 0 + endcol := 0 + for j := i; j < len(words); j += nlines { + endcol += maxwidth + _, err := w.w.Write(words[j]) + if err != nil { + return err + } + col += utf8.RuneCount(words[j]) + if j+nlines < len(words) { + for col < endcol { + _, err := w.w.Write([]byte{' '}) + if err != nil { + return err + } + col++ + } + } + } + _, err := w.w.Write([]byte{'\n'}) + if err != nil { + return err + } + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/kr/text/colwriter/column_test.go b/Godeps/_workspace/src/github.com/kr/text/colwriter/column_test.go new file mode 100644 index 0000000000..8d0bf8f3be --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/colwriter/column_test.go @@ -0,0 +1,90 @@ +package colwriter + +import ( + "bytes" + "testing" +) + +var src = ` +.git +.gitignore +.godir +Procfile: +README.md +api.go +apps.go +auth.go +darwin.go +data.go +dyno.go: +env.go +git.go +help.go +hkdist +linux.go +ls.go +main.go +plugin.go +run.go +scale.go +ssh.go +tail.go +term +unix.go +update.go +version.go +windows.go +`[1:] + +var tests = []struct{ + wid int + flag uint + src string + want string +}{ + {80, 0, "", ""}, + {80, 0, src, ` +.git README.md darwin.go git.go ls.go scale.go unix.go +.gitignore api.go data.go help.go main.go ssh.go update.go +.godir apps.go dyno.go: hkdist plugin.go tail.go version.go +Procfile: auth.go env.go linux.go run.go term windows.go +`[1:]}, + {80, BreakOnColon, src, ` +.git .gitignore .godir + +Procfile: +README.md api.go apps.go auth.go darwin.go data.go + +dyno.go: +env.go hkdist main.go scale.go term version.go +git.go linux.go plugin.go ssh.go unix.go windows.go +help.go ls.go run.go tail.go update.go +`[1:]}, + {20, 0, ` +Hello +Γειά σου +안녕 +今日は +`[1:], ` +Hello 안녕 +Γειά σου 今日は +`[1:]}, +} + +func TestWriter(t *testing.T) { + for _, test := range tests { + b := new(bytes.Buffer) + w := NewWriter(b, test.wid, test.flag) + if _, err := w.Write([]byte(test.src)); err != nil { + t.Error(err) + } + if err := w.Flush(); err != nil { + t.Error(err) + } + if g := b.String(); test.want != g { + t.Log("\n" + test.want) + t.Log("\n" + g) + t.Errorf("%q != %q", test.want, g) + } + } +} diff --git a/Godeps/_workspace/src/github.com/kr/text/doc.go b/Godeps/_workspace/src/github.com/kr/text/doc.go new file mode 100644 index 0000000000..cf4c198f95 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/doc.go @@ -0,0 +1,3 @@ +// Package text provides rudimentary functions for manipulating text in +// paragraphs. +package text diff --git a/Godeps/_workspace/src/github.com/kr/text/indent.go b/Godeps/_workspace/src/github.com/kr/text/indent.go new file mode 100644 index 0000000000..4ebac45c09 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/indent.go @@ -0,0 +1,74 @@ +package text + +import ( + "io" +) + +// Indent inserts prefix at the beginning of each non-empty line of s. The +// end-of-line marker is NL. +func Indent(s, prefix string) string { + return string(IndentBytes([]byte(s), []byte(prefix))) +} + +// IndentBytes inserts prefix at the beginning of each non-empty line of b. +// The end-of-line marker is NL. +func IndentBytes(b, prefix []byte) []byte { + var res []byte + bol := true + for _, c := range b { + if bol && c != '\n' { + res = append(res, prefix...) + } + res = append(res, c) + bol = c == '\n' + } + return res +} + +// Writer indents each line of its input. +type indentWriter struct { + w io.Writer + bol bool + pre [][]byte + sel int + off int +} + +// NewIndentWriter makes a new write filter that indents the input +// lines. Each line is prefixed in order with the corresponding +// element of pre. If there are more lines than elements, the last +// element of pre is repeated for each subsequent line. +func NewIndentWriter(w io.Writer, pre ...[]byte) io.Writer { + return &indentWriter{ + w: w, + pre: pre, + bol: true, + } +} + +// The only errors returned are from the underlying indentWriter. +func (w *indentWriter) Write(p []byte) (n int, err error) { + for _, c := range p { + if w.bol { + var i int + i, err = w.w.Write(w.pre[w.sel][w.off:]) + w.off += i + if err != nil { + return n, err + } + } + _, err = w.w.Write([]byte{c}) + if err != nil { + return n, err + } + n++ + w.bol = c == '\n' + if w.bol { + w.off = 0 + if w.sel < len(w.pre)-1 { + w.sel++ + } + } + } + return n, nil +} diff --git a/Godeps/_workspace/src/github.com/kr/text/indent_test.go b/Godeps/_workspace/src/github.com/kr/text/indent_test.go new file mode 100644 index 0000000000..5c723eee85 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/indent_test.go @@ -0,0 +1,119 @@ +package text + +import ( + "bytes" + "testing" +) + +type T struct { + inp, exp, pre string +} + +var tests = []T{ + { + "The quick brown fox\njumps over the lazy\ndog.\nBut not quickly.\n", + "xxxThe quick brown fox\nxxxjumps over the lazy\nxxxdog.\nxxxBut not quickly.\n", + "xxx", + }, + { + "The quick brown fox\njumps over the lazy\ndog.\n\nBut not quickly.", + "xxxThe quick brown fox\nxxxjumps over the lazy\nxxxdog.\n\nxxxBut not quickly.", + "xxx", + }, +} + +func TestIndent(t *testing.T) { + for _, test := range tests { + got := Indent(test.inp, test.pre) + if got != test.exp { + t.Errorf("mismatch %q != %q", got, test.exp) + } + } +} + +type IndentWriterTest struct { + inp, exp string + pre []string +} + +var ts = []IndentWriterTest{ + { + ` +The quick brown fox +jumps over the lazy +dog. +But not quickly. +`[1:], + ` +xxxThe quick brown fox +xxxjumps over the lazy +xxxdog. +xxxBut not quickly. +`[1:], + []string{"xxx"}, + }, + { + ` +The quick brown fox +jumps over the lazy +dog. +But not quickly. +`[1:], + ` +xxaThe quick brown fox +xxxjumps over the lazy +xxxdog. +xxxBut not quickly. +`[1:], + []string{"xxa", "xxx"}, + }, + { + ` +The quick brown fox +jumps over the lazy +dog. +But not quickly. +`[1:], + ` +xxaThe quick brown fox +xxbjumps over the lazy +xxcdog. +xxxBut not quickly. +`[1:], + []string{"xxa", "xxb", "xxc", "xxx"}, + }, + { + ` +The quick brown fox +jumps over the lazy +dog. + +But not quickly.`[1:], + ` +xxaThe quick brown fox +xxxjumps over the lazy +xxxdog. +xxx +xxxBut not quickly.`[1:], + []string{"xxa", "xxx"}, + }, +} + +func TestIndentWriter(t *testing.T) { + for _, test := range ts { + b := new(bytes.Buffer) + pre := make([][]byte, len(test.pre)) + for i := range test.pre { + pre[i] = []byte(test.pre[i]) + } + w := NewIndentWriter(b, pre...) + if _, err := w.Write([]byte(test.inp)); err != nil { + t.Error(err) + } + if got := b.String(); got != test.exp { + t.Errorf("mismatch %q != %q", got, test.exp) + t.Log(got) + t.Log(test.exp) + } + } +} diff --git a/Godeps/_workspace/src/github.com/kr/text/mc/Readme b/Godeps/_workspace/src/github.com/kr/text/mc/Readme new file mode 100644 index 0000000000..519ddc00a1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/mc/Readme @@ -0,0 +1,9 @@ +Command mc prints in multiple columns. + + Usage: mc [-] [-N] [file...] + +Mc splits the input into as many columns as will fit in N +print positions. If the output is a tty, the default N is +the number of characters in a terminal line; otherwise the +default N is 80. Under option - each input line ending in +a colon ':' is printed separately. diff --git a/Godeps/_workspace/src/github.com/kr/text/mc/mc.go b/Godeps/_workspace/src/github.com/kr/text/mc/mc.go new file mode 100644 index 0000000000..00169a30f1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/mc/mc.go @@ -0,0 +1,62 @@ +// Command mc prints in multiple columns. +// +// Usage: mc [-] [-N] [file...] +// +// Mc splits the input into as many columns as will fit in N +// print positions. If the output is a tty, the default N is +// the number of characters in a terminal line; otherwise the +// default N is 80. Under option - each input line ending in +// a colon ':' is printed separately. +package main + +import ( + "github.com/kr/pty" + "github.com/kr/text/colwriter" + "io" + "log" + "os" + "strconv" +) + +func main() { + var width int + var flag uint + args := os.Args[1:] + for len(args) > 0 && len(args[0]) > 0 && args[0][0] == '-' { + if len(args[0]) > 1 { + width, _ = strconv.Atoi(args[0][1:]) + } else { + flag |= colwriter.BreakOnColon + } + args = args[1:] + } + if width < 1 { + _, width, _ = pty.Getsize(os.Stdout) + } + if width < 1 { + width = 80 + } + + w := colwriter.NewWriter(os.Stdout, width, flag) + if len(args) > 0 { + for _, s := range args { + if f, err := os.Open(s); err == nil { + copyin(w, f) + f.Close() + } else { + log.Println(err) + } + } + } else { + copyin(w, os.Stdin) + } +} + +func copyin(w *colwriter.Writer, r io.Reader) { + if _, err := io.Copy(w, r); err != nil { + log.Println(err) + } + if err := w.Flush(); err != nil { + log.Println(err) + } +} diff --git a/Godeps/_workspace/src/github.com/kr/text/wrap.go b/Godeps/_workspace/src/github.com/kr/text/wrap.go new file mode 100644 index 0000000000..ca8856515c --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/wrap.go @@ -0,0 +1,86 @@ +package text + +import ( + "bytes" + "math" +) + +var ( + nl = []byte{'\n'} + sp = []byte{' '} +) + +const defaultPenalty = 1e5 + +// Wrap wraps s into a paragraph of lines of length lim, with minimal +// raggedness. +func Wrap(s string, lim int) string { + return string(WrapBytes([]byte(s), lim)) +} + +// WrapBytes wraps b into a paragraph of lines of length lim, with minimal +// raggedness. +func WrapBytes(b []byte, lim int) []byte { + words := bytes.Split(bytes.Replace(bytes.TrimSpace(b), nl, sp, -1), sp) + var lines [][]byte + for _, line := range WrapWords(words, 1, lim, defaultPenalty) { + lines = append(lines, bytes.Join(line, sp)) + } + return bytes.Join(lines, nl) +} + +// WrapWords is the low-level line-breaking algorithm, useful if you need more +// control over the details of the text wrapping process. For most uses, either +// Wrap or WrapBytes will be sufficient and more convenient. +// +// WrapWords splits a list of words into lines with minimal "raggedness", +// treating each byte as one unit, accounting for spc units between adjacent +// words on each line, and attempting to limit lines to lim units. Raggedness +// is the total error over all lines, where error is the square of the +// difference of the length of the line and lim. Too-long lines (which only +// happen when a single word is longer than lim units) have pen penalty units +// added to the error. +func WrapWords(words [][]byte, spc, lim, pen int) [][][]byte { + n := len(words) + + length := make([][]int, n) + for i := 0; i < n; i++ { + length[i] = make([]int, n) + length[i][i] = len(words[i]) + for j := i + 1; j < n; j++ { + length[i][j] = length[i][j-1] + spc + len(words[j]) + } + } + + nbrk := make([]int, n) + cost := make([]int, n) + for i := range cost { + cost[i] = math.MaxInt32 + } + for i := n - 1; i >= 0; i-- { + if length[i][n-1] <= lim { + cost[i] = 0 + nbrk[i] = n + } else { + for j := i + 1; j < n; j++ { + d := lim - length[i][j-1] + c := d*d + cost[j] + if length[i][j-1] > lim { + c += pen // too-long lines get a worse penalty + } + if c < cost[i] { + cost[i] = c + nbrk[i] = j + } + } + } + } + + var lines [][][]byte + i := 0 + for i < n { + lines = append(lines, words[i:nbrk[i]]) + i = nbrk[i] + } + return lines +} diff --git a/Godeps/_workspace/src/github.com/kr/text/wrap_test.go b/Godeps/_workspace/src/github.com/kr/text/wrap_test.go new file mode 100644 index 0000000000..90f065cd1a --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/wrap_test.go @@ -0,0 +1,44 @@ +package text + +import ( + "bytes" + "testing" +) + +var text = "The quick brown fox jumps over the lazy dog." + +func TestWrap(t *testing.T) { + exp := [][]string{ + {"The", "quick", "brown", "fox"}, + {"jumps", "over", "the", "lazy", "dog."}, + } + words := bytes.Split([]byte(text), sp) + got := WrapWords(words, 1, 24, defaultPenalty) + if len(exp) != len(got) { + t.Fail() + } + for i := range exp { + if len(exp[i]) != len(got[i]) { + t.Fail() + } + for j := range exp[i] { + if exp[i][j] != string(got[i][j]) { + t.Fatal(i, exp[i][j], got[i][j]) + } + } + } +} + +func TestWrapNarrow(t *testing.T) { + exp := "The\nquick\nbrown\nfox\njumps\nover\nthe\nlazy\ndog." + if Wrap(text, 5) != exp { + t.Fail() + } +} + +func TestWrapOneLine(t *testing.T) { + exp := "The quick brown fox jumps over the lazy dog." + if Wrap(text, 500) != exp { + t.Fail() + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/LICENSE b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/LICENSE new file mode 100644 index 0000000000..f9c841a51e --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Mitchell Hashimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/README.md b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/README.md new file mode 100644 index 0000000000..659d6885fc --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/README.md @@ -0,0 +1,46 @@ +# mapstructure + +mapstructure is a Go library for decoding generic map values to structures +and vice versa, while providing helpful error handling. + +This library is most useful when decoding values from some data stream (JSON, +Gob, etc.) where you don't _quite_ know the structure of the underlying data +until you read a part of it. You can therefore read a `map[string]interface{}` +and use this library to decode it into the proper underlying native Go +structure. + +## Installation + +Standard `go get`: + +``` +$ go get github.com/mitchellh/mapstructure +``` + +## Usage & Example + +For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/mapstructure). + +The `Decode` function has examples associated with it there. + +## But Why?! + +Go offers fantastic standard libraries for decoding formats such as JSON. +The standard method is to have a struct pre-created, and populate that struct +from the bytes of the encoded format. This is great, but the problem is if +you have configuration or an encoding that changes slightly depending on +specific fields. For example, consider this JSON: + +```json +{ + "type": "person", + "name": "Mitchell" +} +``` + +Perhaps we can't populate a specific structure without first reading +the "type" field from the JSON. We could always do two passes over the +decoding of the JSON (reading the "type" first, and the rest later). +However, it is much simpler to just decode this into a `map[string]interface{}` +structure, read the "type" key, then use something like this library +to decode it into the proper structure. diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks.go new file mode 100644 index 0000000000..087a392b91 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks.go @@ -0,0 +1,84 @@ +package mapstructure + +import ( + "reflect" + "strconv" + "strings" +) + +// ComposeDecodeHookFunc creates a single DecodeHookFunc that +// automatically composes multiple DecodeHookFuncs. +// +// The composed funcs are called in order, with the result of the +// previous transformation. +func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc { + return func( + f reflect.Kind, + t reflect.Kind, + data interface{}) (interface{}, error) { + var err error + for _, f1 := range fs { + data, err = f1(f, t, data) + if err != nil { + return nil, err + } + + // Modify the from kind to be correct with the new data + f = getKind(reflect.ValueOf(data)) + } + + return data, nil + } +} + +// StringToSliceHookFunc returns a DecodeHookFunc that converts +// string to []string by splitting on the given sep. +func StringToSliceHookFunc(sep string) DecodeHookFunc { + return func( + f reflect.Kind, + t reflect.Kind, + data interface{}) (interface{}, error) { + if f != reflect.String || t != reflect.Slice { + return data, nil + } + + raw := data.(string) + if raw == "" { + return []string{}, nil + } + + return strings.Split(raw, sep), nil + } +} + +func WeaklyTypedHook( + f reflect.Kind, + t reflect.Kind, + data interface{}) (interface{}, error) { + dataVal := reflect.ValueOf(data) + switch t { + case reflect.String: + switch f { + case reflect.Bool: + if dataVal.Bool() { + return "1", nil + } else { + return "0", nil + } + case reflect.Float32: + return strconv.FormatFloat(dataVal.Float(), 'f', -1, 64), nil + case reflect.Int: + return strconv.FormatInt(dataVal.Int(), 10), nil + case reflect.Slice: + dataType := dataVal.Type() + elemKind := dataType.Elem().Kind() + if elemKind == reflect.Uint8 { + return string(dataVal.Interface().([]uint8)), nil + } + case reflect.Uint: + return strconv.FormatUint(dataVal.Uint(), 10), nil + } + } + + return data, nil +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks_test.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks_test.go new file mode 100644 index 0000000000..b417deeb64 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/decode_hooks_test.go @@ -0,0 +1,191 @@ +package mapstructure + +import ( + "errors" + "reflect" + "testing" +) + +func TestComposeDecodeHookFunc(t *testing.T) { + f1 := func( + f reflect.Kind, + t reflect.Kind, + data interface{}) (interface{}, error) { + return data.(string) + "foo", nil + } + + f2 := func( + f reflect.Kind, + t reflect.Kind, + data interface{}) (interface{}, error) { + return data.(string) + "bar", nil + } + + f := ComposeDecodeHookFunc(f1, f2) + + result, err := f(reflect.String, reflect.Slice, "") + if err != nil { + t.Fatalf("bad: %s", err) + } + if result.(string) != "foobar" { + t.Fatalf("bad: %#v", result) + } +} + +func TestComposeDecodeHookFunc_err(t *testing.T) { + f1 := func(reflect.Kind, reflect.Kind, interface{}) (interface{}, error) { + return nil, errors.New("foo") + } + + f2 := func(reflect.Kind, reflect.Kind, interface{}) (interface{}, error) { + panic("NOPE") + } + + f := ComposeDecodeHookFunc(f1, f2) + + _, err := f(reflect.String, reflect.Slice, 42) + if err.Error() != "foo" { + t.Fatalf("bad: %s", err) + } +} + +func TestComposeDecodeHookFunc_kinds(t *testing.T) { + var f2From reflect.Kind + + f1 := func( + f reflect.Kind, + t reflect.Kind, + data interface{}) (interface{}, error) { + return int(42), nil + } + + f2 := func( + f reflect.Kind, + t reflect.Kind, + data interface{}) (interface{}, error) { + f2From = f + return data, nil + } + + f := ComposeDecodeHookFunc(f1, f2) + + _, err := f(reflect.String, reflect.Slice, "") + if err != nil { + t.Fatalf("bad: %s", err) + } + if f2From != reflect.Int { + t.Fatalf("bad: %#v", f2From) + } +} + +func TestStringToSliceHookFunc(t *testing.T) { + f := StringToSliceHookFunc(",") + + cases := []struct { + f, t reflect.Kind + data interface{} + result interface{} + err bool + }{ + {reflect.Slice, reflect.Slice, 42, 42, false}, + {reflect.String, reflect.String, 42, 42, false}, + { + reflect.String, + reflect.Slice, + "foo,bar,baz", + []string{"foo", "bar", "baz"}, + false, + }, + { + reflect.String, + reflect.Slice, + "", + []string{}, + false, + }, + } + + for i, tc := range cases { + actual, err := f(tc.f, tc.t, tc.data) + if tc.err != (err != nil) { + t.Fatalf("case %d: expected err %#v", i, tc.err) + } + if !reflect.DeepEqual(actual, tc.result) { + t.Fatalf( + "case %d: expected %#v, got %#v", + i, tc.result, actual) + } + } +} + +func TestWeaklyTypedHook(t *testing.T) { + var f DecodeHookFunc = WeaklyTypedHook + + cases := []struct { + f, t reflect.Kind + data interface{} + result interface{} + err bool + }{ + // TO STRING + { + reflect.Bool, + reflect.String, + false, + "0", + false, + }, + + { + reflect.Bool, + reflect.String, + true, + "1", + false, + }, + + { + reflect.Float32, + reflect.String, + float32(7), + "7", + false, + }, + + { + reflect.Int, + reflect.String, + int(7), + "7", + false, + }, + + { + reflect.Slice, + reflect.String, + []uint8("foo"), + "foo", + false, + }, + + { + reflect.Uint, + reflect.String, + uint(7), + "7", + false, + }, + } + + for i, tc := range cases { + actual, err := f(tc.f, tc.t, tc.data) + if tc.err != (err != nil) { + t.Fatalf("case %d: expected err %#v", i, tc.err) + } + if !reflect.DeepEqual(actual, tc.result) { + t.Fatalf( + "case %d: expected %#v, got %#v", + i, tc.result, actual) + } + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/error.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/error.go new file mode 100644 index 0000000000..3460799f80 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/error.go @@ -0,0 +1,32 @@ +package mapstructure + +import ( + "fmt" + "strings" +) + +// Error implements the error interface and can represents multiple +// errors that occur in the course of a single decode. +type Error struct { + Errors []string +} + +func (e *Error) Error() string { + points := make([]string, len(e.Errors)) + for i, err := range e.Errors { + points[i] = fmt.Sprintf("* %s", err) + } + + return fmt.Sprintf( + "%d error(s) decoding:\n\n%s", + len(e.Errors), strings.Join(points, "\n")) +} + +func appendErrors(errors []string, err error) []string { + switch e := err.(type) { + case *Error: + return append(errors, e.Errors...) + default: + return append(errors, e.Error()) + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure.go new file mode 100644 index 0000000000..381ba5d487 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure.go @@ -0,0 +1,704 @@ +// The mapstructure package exposes functionality to convert an +// abitrary map[string]interface{} into a native Go structure. +// +// The Go structure can be arbitrarily complex, containing slices, +// other structs, etc. and the decoder will properly decode nested +// maps and so on into the proper structures in the native Go struct. +// See the examples to see what the decoder is capable of. +package mapstructure + +import ( + "errors" + "fmt" + "reflect" + "sort" + "strconv" + "strings" +) + +// DecodeHookFunc is the callback function that can be used for +// data transformations. See "DecodeHook" in the DecoderConfig +// struct. +type DecodeHookFunc func( + from reflect.Kind, + to reflect.Kind, + data interface{}) (interface{}, error) + +// DecoderConfig is the configuration that is used to create a new decoder +// and allows customization of various aspects of decoding. +type DecoderConfig struct { + // DecodeHook, if set, will be called before any decoding and any + // type conversion (if WeaklyTypedInput is on). This lets you modify + // the values before they're set down onto the resulting struct. + // + // If an error is returned, the entire decode will fail with that + // error. + DecodeHook DecodeHookFunc + + // If ErrorUnused is true, then it is an error for there to exist + // keys in the original map that were unused in the decoding process + // (extra keys). + ErrorUnused bool + + // If WeaklyTypedInput is true, the decoder will make the following + // "weak" conversions: + // + // - bools to string (true = "1", false = "0") + // - numbers to string (base 10) + // - bools to int/uint (true = 1, false = 0) + // - strings to int/uint (base implied by prefix) + // - int to bool (true if value != 0) + // - string to bool (accepts: 1, t, T, TRUE, true, True, 0, f, F, + // FALSE, false, False. Anything else is an error) + // - empty array = empty map and vice versa + // + WeaklyTypedInput bool + + // Metadata is the struct that will contain extra metadata about + // the decoding. If this is nil, then no metadata will be tracked. + Metadata *Metadata + + // Result is a pointer to the struct that will contain the decoded + // value. + Result interface{} + + // The tag name that mapstructure reads for field names. This + // defaults to "mapstructure" + TagName string +} + +// A Decoder takes a raw interface value and turns it into structured +// data, keeping track of rich error information along the way in case +// anything goes wrong. Unlike the basic top-level Decode method, you can +// more finely control how the Decoder behaves using the DecoderConfig +// structure. The top-level Decode method is just a convenience that sets +// up the most basic Decoder. +type Decoder struct { + config *DecoderConfig +} + +// Metadata contains information about decoding a structure that +// is tedious or difficult to get otherwise. +type Metadata struct { + // Keys are the keys of the structure which were successfully decoded + Keys []string + + // Unused is a slice of keys that were found in the raw value but + // weren't decoded since there was no matching field in the result interface + Unused []string +} + +// Decode takes a map and uses reflection to convert it into the +// given Go native structure. val must be a pointer to a struct. +func Decode(m interface{}, rawVal interface{}) error { + config := &DecoderConfig{ + Metadata: nil, + Result: rawVal, + } + + decoder, err := NewDecoder(config) + if err != nil { + return err + } + + return decoder.Decode(m) +} + +// WeakDecode is the same as Decode but is shorthand to enable +// WeaklyTypedInput. See DecoderConfig for more info. +func WeakDecode(input, output interface{}) error { + config := &DecoderConfig{ + Metadata: nil, + Result: output, + WeaklyTypedInput: true, + } + + decoder, err := NewDecoder(config) + if err != nil { + return err + } + + return decoder.Decode(input) +} + +// NewDecoder returns a new decoder for the given configuration. Once +// a decoder has been returned, the same configuration must not be used +// again. +func NewDecoder(config *DecoderConfig) (*Decoder, error) { + val := reflect.ValueOf(config.Result) + if val.Kind() != reflect.Ptr { + return nil, errors.New("result must be a pointer") + } + + val = val.Elem() + if !val.CanAddr() { + return nil, errors.New("result must be addressable (a pointer)") + } + + if config.Metadata != nil { + if config.Metadata.Keys == nil { + config.Metadata.Keys = make([]string, 0) + } + + if config.Metadata.Unused == nil { + config.Metadata.Unused = make([]string, 0) + } + } + + if config.TagName == "" { + config.TagName = "mapstructure" + } + + result := &Decoder{ + config: config, + } + + return result, nil +} + +// Decode decodes the given raw interface to the target pointer specified +// by the configuration. +func (d *Decoder) Decode(raw interface{}) error { + return d.decode("", raw, reflect.ValueOf(d.config.Result).Elem()) +} + +// Decodes an unknown data type into a specific reflection value. +func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error { + if data == nil { + // If the data is nil, then we don't set anything. + return nil + } + + dataVal := reflect.ValueOf(data) + if !dataVal.IsValid() { + // If the data value is invalid, then we just set the value + // to be the zero value. + val.Set(reflect.Zero(val.Type())) + return nil + } + + if d.config.DecodeHook != nil { + // We have a DecodeHook, so let's pre-process the data. + var err error + data, err = d.config.DecodeHook(getKind(dataVal), getKind(val), data) + if err != nil { + return err + } + } + + var err error + dataKind := getKind(val) + switch dataKind { + case reflect.Bool: + err = d.decodeBool(name, data, val) + case reflect.Interface: + err = d.decodeBasic(name, data, val) + case reflect.String: + err = d.decodeString(name, data, val) + case reflect.Int: + err = d.decodeInt(name, data, val) + case reflect.Uint: + err = d.decodeUint(name, data, val) + case reflect.Float32: + err = d.decodeFloat(name, data, val) + case reflect.Struct: + err = d.decodeStruct(name, data, val) + case reflect.Map: + err = d.decodeMap(name, data, val) + case reflect.Ptr: + err = d.decodePtr(name, data, val) + case reflect.Slice: + err = d.decodeSlice(name, data, val) + default: + // If we reached this point then we weren't able to decode it + return fmt.Errorf("%s: unsupported type: %s", name, dataKind) + } + + // If we reached here, then we successfully decoded SOMETHING, so + // mark the key as used if we're tracking metadata. + if d.config.Metadata != nil && name != "" { + d.config.Metadata.Keys = append(d.config.Metadata.Keys, name) + } + + return err +} + +// This decodes a basic type (bool, int, string, etc.) and sets the +// value to "data" of that type. +func (d *Decoder) decodeBasic(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataValType := dataVal.Type() + if !dataValType.AssignableTo(val.Type()) { + return fmt.Errorf( + "'%s' expected type '%s', got '%s'", + name, val.Type(), dataValType) + } + + val.Set(dataVal) + return nil +} + +func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataKind := getKind(dataVal) + + converted := true + switch { + case dataKind == reflect.String: + val.SetString(dataVal.String()) + case dataKind == reflect.Bool && d.config.WeaklyTypedInput: + if dataVal.Bool() { + val.SetString("1") + } else { + val.SetString("0") + } + case dataKind == reflect.Int && d.config.WeaklyTypedInput: + val.SetString(strconv.FormatInt(dataVal.Int(), 10)) + case dataKind == reflect.Uint && d.config.WeaklyTypedInput: + val.SetString(strconv.FormatUint(dataVal.Uint(), 10)) + case dataKind == reflect.Float32 && d.config.WeaklyTypedInput: + val.SetString(strconv.FormatFloat(dataVal.Float(), 'f', -1, 64)) + case dataKind == reflect.Slice && d.config.WeaklyTypedInput: + dataType := dataVal.Type() + elemKind := dataType.Elem().Kind() + switch { + case elemKind == reflect.Uint8: + val.SetString(string(dataVal.Interface().([]uint8))) + default: + converted = false + } + default: + converted = false + } + + if !converted { + return fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type()) + } + + return nil +} + +func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataKind := getKind(dataVal) + + switch { + case dataKind == reflect.Int: + val.SetInt(dataVal.Int()) + case dataKind == reflect.Uint: + val.SetInt(int64(dataVal.Uint())) + case dataKind == reflect.Float32: + val.SetInt(int64(dataVal.Float())) + case dataKind == reflect.Bool && d.config.WeaklyTypedInput: + if dataVal.Bool() { + val.SetInt(1) + } else { + val.SetInt(0) + } + case dataKind == reflect.String && d.config.WeaklyTypedInput: + i, err := strconv.ParseInt(dataVal.String(), 0, val.Type().Bits()) + if err == nil { + val.SetInt(i) + } else { + return fmt.Errorf("cannot parse '%s' as int: %s", name, err) + } + default: + return fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type()) + } + + return nil +} + +func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataKind := getKind(dataVal) + + switch { + case dataKind == reflect.Int: + val.SetUint(uint64(dataVal.Int())) + case dataKind == reflect.Uint: + val.SetUint(dataVal.Uint()) + case dataKind == reflect.Float32: + val.SetUint(uint64(dataVal.Float())) + case dataKind == reflect.Bool && d.config.WeaklyTypedInput: + if dataVal.Bool() { + val.SetUint(1) + } else { + val.SetUint(0) + } + case dataKind == reflect.String && d.config.WeaklyTypedInput: + i, err := strconv.ParseUint(dataVal.String(), 0, val.Type().Bits()) + if err == nil { + val.SetUint(i) + } else { + return fmt.Errorf("cannot parse '%s' as uint: %s", name, err) + } + default: + return fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type()) + } + + return nil +} + +func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataKind := getKind(dataVal) + + switch { + case dataKind == reflect.Bool: + val.SetBool(dataVal.Bool()) + case dataKind == reflect.Int && d.config.WeaklyTypedInput: + val.SetBool(dataVal.Int() != 0) + case dataKind == reflect.Uint && d.config.WeaklyTypedInput: + val.SetBool(dataVal.Uint() != 0) + case dataKind == reflect.Float32 && d.config.WeaklyTypedInput: + val.SetBool(dataVal.Float() != 0) + case dataKind == reflect.String && d.config.WeaklyTypedInput: + b, err := strconv.ParseBool(dataVal.String()) + if err == nil { + val.SetBool(b) + } else if dataVal.String() == "" { + val.SetBool(false) + } else { + return fmt.Errorf("cannot parse '%s' as bool: %s", name, err) + } + default: + return fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type()) + } + + return nil +} + +func (d *Decoder) decodeFloat(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.ValueOf(data) + dataKind := getKind(dataVal) + + switch { + case dataKind == reflect.Int: + val.SetFloat(float64(dataVal.Int())) + case dataKind == reflect.Uint: + val.SetFloat(float64(dataVal.Uint())) + case dataKind == reflect.Float32: + val.SetFloat(float64(dataVal.Float())) + case dataKind == reflect.Bool && d.config.WeaklyTypedInput: + if dataVal.Bool() { + val.SetFloat(1) + } else { + val.SetFloat(0) + } + case dataKind == reflect.String && d.config.WeaklyTypedInput: + f, err := strconv.ParseFloat(dataVal.String(), val.Type().Bits()) + if err == nil { + val.SetFloat(f) + } else { + return fmt.Errorf("cannot parse '%s' as float: %s", name, err) + } + default: + return fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type()) + } + + return nil +} + +func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) error { + valType := val.Type() + valKeyType := valType.Key() + valElemType := valType.Elem() + + // Make a new map to hold our result + mapType := reflect.MapOf(valKeyType, valElemType) + valMap := reflect.MakeMap(mapType) + + // Check input type + dataVal := reflect.Indirect(reflect.ValueOf(data)) + if dataVal.Kind() != reflect.Map { + // Accept empty array/slice instead of an empty map in weakly typed mode + if d.config.WeaklyTypedInput && + (dataVal.Kind() == reflect.Slice || dataVal.Kind() == reflect.Array) && + dataVal.Len() == 0 { + val.Set(valMap) + return nil + } else { + return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) + } + } + + // Accumulate errors + errors := make([]string, 0) + + for _, k := range dataVal.MapKeys() { + fieldName := fmt.Sprintf("%s[%s]", name, k) + + // First decode the key into the proper type + currentKey := reflect.Indirect(reflect.New(valKeyType)) + if err := d.decode(fieldName, k.Interface(), currentKey); err != nil { + errors = appendErrors(errors, err) + continue + } + + // Next decode the data into the proper type + v := dataVal.MapIndex(k).Interface() + currentVal := reflect.Indirect(reflect.New(valElemType)) + if err := d.decode(fieldName, v, currentVal); err != nil { + errors = appendErrors(errors, err) + continue + } + + valMap.SetMapIndex(currentKey, currentVal) + } + + // Set the built up map to the value + val.Set(valMap) + + // If we had errors, return those + if len(errors) > 0 { + return &Error{errors} + } + + return nil +} + +func (d *Decoder) decodePtr(name string, data interface{}, val reflect.Value) error { + // Create an element of the concrete (non pointer) type and decode + // into that. Then set the value of the pointer to this type. + valType := val.Type() + valElemType := valType.Elem() + realVal := reflect.New(valElemType) + if err := d.decode(name, data, reflect.Indirect(realVal)); err != nil { + return err + } + + val.Set(realVal) + return nil +} + +func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.Indirect(reflect.ValueOf(data)) + dataValKind := dataVal.Kind() + valType := val.Type() + valElemType := valType.Elem() + sliceType := reflect.SliceOf(valElemType) + + // Check input type + if dataValKind != reflect.Array && dataValKind != reflect.Slice { + // Accept empty map instead of array/slice in weakly typed mode + if d.config.WeaklyTypedInput && dataVal.Kind() == reflect.Map && dataVal.Len() == 0 { + val.Set(reflect.MakeSlice(sliceType, 0, 0)) + return nil + } else { + return fmt.Errorf( + "'%s': source data must be an array or slice, got %s", name, dataValKind) + } + } + + // Make a new slice to hold our result, same size as the original data. + valSlice := reflect.MakeSlice(sliceType, dataVal.Len(), dataVal.Len()) + + // Accumulate any errors + errors := make([]string, 0) + + for i := 0; i < dataVal.Len(); i++ { + currentData := dataVal.Index(i).Interface() + currentField := valSlice.Index(i) + + fieldName := fmt.Sprintf("%s[%d]", name, i) + if err := d.decode(fieldName, currentData, currentField); err != nil { + errors = appendErrors(errors, err) + } + } + + // Finally, set the value to the slice we built up + val.Set(valSlice) + + // If there were errors, we return those + if len(errors) > 0 { + return &Error{errors} + } + + return nil +} + +func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.Indirect(reflect.ValueOf(data)) + dataValKind := dataVal.Kind() + if dataValKind != reflect.Map { + return fmt.Errorf("'%s' expected a map, got '%s'", name, dataValKind) + } + + dataValType := dataVal.Type() + if kind := dataValType.Key().Kind(); kind != reflect.String && kind != reflect.Interface { + return fmt.Errorf( + "'%s' needs a map with string keys, has '%s' keys", + name, dataValType.Key().Kind()) + } + + dataValKeys := make(map[reflect.Value]struct{}) + dataValKeysUnused := make(map[interface{}]struct{}) + for _, dataValKey := range dataVal.MapKeys() { + dataValKeys[dataValKey] = struct{}{} + dataValKeysUnused[dataValKey.Interface()] = struct{}{} + } + + errors := make([]string, 0) + + // This slice will keep track of all the structs we'll be decoding. + // There can be more than one struct if there are embedded structs + // that are squashed. + structs := make([]reflect.Value, 1, 5) + structs[0] = val + + // Compile the list of all the fields that we're going to be decoding + // from all the structs. + fields := make(map[*reflect.StructField]reflect.Value) + for len(structs) > 0 { + structVal := structs[0] + structs = structs[1:] + + structType := structVal.Type() + for i := 0; i < structType.NumField(); i++ { + fieldType := structType.Field(i) + + if fieldType.Anonymous { + fieldKind := fieldType.Type.Kind() + if fieldKind != reflect.Struct { + errors = appendErrors(errors, + fmt.Errorf("%s: unsupported type: %s", fieldType.Name, fieldKind)) + continue + } + + // We have an embedded field. We "squash" the fields down + // if specified in the tag. + squash := false + tagParts := strings.Split(fieldType.Tag.Get(d.config.TagName), ",") + for _, tag := range tagParts[1:] { + if tag == "squash" { + squash = true + break + } + } + + if squash { + structs = append(structs, val.FieldByName(fieldType.Name)) + continue + } + } + + // Normal struct field, store it away + fields[&fieldType] = structVal.Field(i) + } + } + + for fieldType, field := range fields { + fieldName := fieldType.Name + + tagValue := fieldType.Tag.Get(d.config.TagName) + tagValue = strings.SplitN(tagValue, ",", 2)[0] + if tagValue != "" { + fieldName = tagValue + } + + rawMapKey := reflect.ValueOf(fieldName) + rawMapVal := dataVal.MapIndex(rawMapKey) + if !rawMapVal.IsValid() { + // Do a slower search by iterating over each key and + // doing case-insensitive search. + for dataValKey, _ := range dataValKeys { + mK, ok := dataValKey.Interface().(string) + if !ok { + // Not a string key + continue + } + + if strings.EqualFold(mK, fieldName) { + rawMapKey = dataValKey + rawMapVal = dataVal.MapIndex(dataValKey) + break + } + } + + if !rawMapVal.IsValid() { + // There was no matching key in the map for the value in + // the struct. Just ignore. + continue + } + } + + // Delete the key we're using from the unused map so we stop tracking + delete(dataValKeysUnused, rawMapKey.Interface()) + + if !field.IsValid() { + // This should never happen + panic("field is not valid") + } + + // If we can't set the field, then it is unexported or something, + // and we just continue onwards. + if !field.CanSet() { + continue + } + + // If the name is empty string, then we're at the root, and we + // don't dot-join the fields. + if name != "" { + fieldName = fmt.Sprintf("%s.%s", name, fieldName) + } + + if err := d.decode(fieldName, rawMapVal.Interface(), field); err != nil { + errors = appendErrors(errors, err) + } + } + + if d.config.ErrorUnused && len(dataValKeysUnused) > 0 { + keys := make([]string, 0, len(dataValKeysUnused)) + for rawKey, _ := range dataValKeysUnused { + keys = append(keys, rawKey.(string)) + } + sort.Strings(keys) + + err := fmt.Errorf("'%s' has invalid keys: %s", name, strings.Join(keys, ", ")) + errors = appendErrors(errors, err) + } + + if len(errors) > 0 { + return &Error{errors} + } + + // Add the unused keys to the list of unused keys if we're tracking metadata + if d.config.Metadata != nil { + for rawKey, _ := range dataValKeysUnused { + key := rawKey.(string) + if name != "" { + key = fmt.Sprintf("%s.%s", name, key) + } + + d.config.Metadata.Unused = append(d.config.Metadata.Unused, key) + } + } + + return nil +} + +func getKind(val reflect.Value) reflect.Kind { + kind := val.Kind() + + switch { + case kind >= reflect.Int && kind <= reflect.Int64: + return reflect.Int + case kind >= reflect.Uint && kind <= reflect.Uint64: + return reflect.Uint + case kind >= reflect.Float32 && kind <= reflect.Float64: + return reflect.Float32 + default: + return kind + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_benchmark_test.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_benchmark_test.go new file mode 100644 index 0000000000..b50ac36e5d --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_benchmark_test.go @@ -0,0 +1,243 @@ +package mapstructure + +import ( + "testing" +) + +func Benchmark_Decode(b *testing.B) { + type Person struct { + Name string + Age int + Emails []string + Extra map[string]string + } + + input := map[string]interface{}{ + "name": "Mitchell", + "age": 91, + "emails": []string{"one", "two", "three"}, + "extra": map[string]string{ + "twitter": "mitchellh", + }, + } + + var result Person + for i := 0; i < b.N; i++ { + Decode(input, &result) + } +} + +func Benchmark_DecodeBasic(b *testing.B) { + input := map[string]interface{}{ + "vstring": "foo", + "vint": 42, + "Vuint": 42, + "vbool": true, + "Vfloat": 42.42, + "vsilent": true, + "vdata": 42, + } + + var result Basic + for i := 0; i < b.N; i++ { + Decode(input, &result) + } +} + +func Benchmark_DecodeEmbedded(b *testing.B) { + input := map[string]interface{}{ + "vstring": "foo", + "Basic": map[string]interface{}{ + "vstring": "innerfoo", + }, + "vunique": "bar", + } + + var result Embedded + for i := 0; i < b.N; i++ { + Decode(input, &result) + } +} + +func Benchmark_DecodeTypeConversion(b *testing.B) { + input := map[string]interface{}{ + "IntToFloat": 42, + "IntToUint": 42, + "IntToBool": 1, + "IntToString": 42, + "UintToInt": 42, + "UintToFloat": 42, + "UintToBool": 42, + "UintToString": 42, + "BoolToInt": true, + "BoolToUint": true, + "BoolToFloat": true, + "BoolToString": true, + "FloatToInt": 42.42, + "FloatToUint": 42.42, + "FloatToBool": 42.42, + "FloatToString": 42.42, + "StringToInt": "42", + "StringToUint": "42", + "StringToBool": "1", + "StringToFloat": "42.42", + "SliceToMap": []interface{}{}, + "MapToSlice": map[string]interface{}{}, + } + + var resultStrict TypeConversionResult + for i := 0; i < b.N; i++ { + Decode(input, &resultStrict) + } +} + +func Benchmark_DecodeMap(b *testing.B) { + input := map[string]interface{}{ + "vfoo": "foo", + "vother": map[interface{}]interface{}{ + "foo": "foo", + "bar": "bar", + }, + } + + var result Map + for i := 0; i < b.N; i++ { + Decode(input, &result) + } +} + +func Benchmark_DecodeMapOfStruct(b *testing.B) { + input := map[string]interface{}{ + "value": map[string]interface{}{ + "foo": map[string]string{"vstring": "one"}, + "bar": map[string]string{"vstring": "two"}, + }, + } + + var result MapOfStruct + for i := 0; i < b.N; i++ { + Decode(input, &result) + } +} + +func Benchmark_DecodeSlice(b *testing.B) { + input := map[string]interface{}{ + "vfoo": "foo", + "vbar": []string{"foo", "bar", "baz"}, + } + + var result Slice + for i := 0; i < b.N; i++ { + Decode(input, &result) + } +} + +func Benchmark_DecodeSliceOfStruct(b *testing.B) { + input := map[string]interface{}{ + "value": []map[string]interface{}{ + {"vstring": "one"}, + {"vstring": "two"}, + }, + } + + var result SliceOfStruct + for i := 0; i < b.N; i++ { + Decode(input, &result) + } +} + +func Benchmark_DecodeWeaklyTypedInput(b *testing.B) { + type Person struct { + Name string + Age int + Emails []string + } + + // This input can come from anywhere, but typically comes from + // something like decoding JSON, generated by a weakly typed language + // such as PHP. + input := map[string]interface{}{ + "name": 123, // number => string + "age": "42", // string => number + "emails": map[string]interface{}{}, // empty map => empty array + } + + var result Person + config := &DecoderConfig{ + WeaklyTypedInput: true, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + panic(err) + } + + for i := 0; i < b.N; i++ { + decoder.Decode(input) + } +} + +func Benchmark_DecodeMetadata(b *testing.B) { + type Person struct { + Name string + Age int + } + + input := map[string]interface{}{ + "name": "Mitchell", + "age": 91, + "email": "foo@bar.com", + } + + var md Metadata + var result Person + config := &DecoderConfig{ + Metadata: &md, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + panic(err) + } + + for i := 0; i < b.N; i++ { + decoder.Decode(input) + } +} + +func Benchmark_DecodeMetadataEmbedded(b *testing.B) { + input := map[string]interface{}{ + "vstring": "foo", + "vunique": "bar", + } + + var md Metadata + var result EmbeddedSquash + config := &DecoderConfig{ + Metadata: &md, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + b.Fatalf("err: %s", err) + } + + for i := 0; i < b.N; i++ { + decoder.Decode(input) + } +} + +func Benchmark_DecodeTagged(b *testing.B) { + input := map[string]interface{}{ + "foo": "bar", + "bar": "value", + } + + var result Tagged + for i := 0; i < b.N; i++ { + Decode(input, &result) + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_bugs_test.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_bugs_test.go new file mode 100644 index 0000000000..7054f1ac9a --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_bugs_test.go @@ -0,0 +1,47 @@ +package mapstructure + +import "testing" + +// GH-1 +func TestDecode_NilValue(t *testing.T) { + input := map[string]interface{}{ + "vfoo": nil, + "vother": nil, + } + + var result Map + err := Decode(input, &result) + if err != nil { + t.Fatalf("should not error: %s", err) + } + + if result.Vfoo != "" { + t.Fatalf("value should be default: %s", result.Vfoo) + } + + if result.Vother != nil { + t.Fatalf("Vother should be nil: %s", result.Vother) + } +} + +// GH-10 +func TestDecode_mapInterfaceInterface(t *testing.T) { + input := map[interface{}]interface{}{ + "vfoo": nil, + "vother": nil, + } + + var result Map + err := Decode(input, &result) + if err != nil { + t.Fatalf("should not error: %s", err) + } + + if result.Vfoo != "" { + t.Fatalf("value should be default: %s", result.Vfoo) + } + + if result.Vother != nil { + t.Fatalf("Vother should be nil: %s", result.Vother) + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_examples_test.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_examples_test.go new file mode 100644 index 0000000000..aa393cc572 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_examples_test.go @@ -0,0 +1,169 @@ +package mapstructure + +import ( + "fmt" +) + +func ExampleDecode() { + type Person struct { + Name string + Age int + Emails []string + Extra map[string]string + } + + // This input can come from anywhere, but typically comes from + // something like decoding JSON where we're not quite sure of the + // struct initially. + input := map[string]interface{}{ + "name": "Mitchell", + "age": 91, + "emails": []string{"one", "two", "three"}, + "extra": map[string]string{ + "twitter": "mitchellh", + }, + } + + var result Person + err := Decode(input, &result) + if err != nil { + panic(err) + } + + fmt.Printf("%#v", result) + // Output: + // mapstructure.Person{Name:"Mitchell", Age:91, Emails:[]string{"one", "two", "three"}, Extra:map[string]string{"twitter":"mitchellh"}} +} + +func ExampleDecode_errors() { + type Person struct { + Name string + Age int + Emails []string + Extra map[string]string + } + + // This input can come from anywhere, but typically comes from + // something like decoding JSON where we're not quite sure of the + // struct initially. + input := map[string]interface{}{ + "name": 123, + "age": "bad value", + "emails": []int{1, 2, 3}, + } + + var result Person + err := Decode(input, &result) + if err == nil { + panic("should have an error") + } + + fmt.Println(err.Error()) + // Output: + // 5 error(s) decoding: + // + // * 'Name' expected type 'string', got unconvertible type 'int' + // * 'Age' expected type 'int', got unconvertible type 'string' + // * 'Emails[0]' expected type 'string', got unconvertible type 'int' + // * 'Emails[1]' expected type 'string', got unconvertible type 'int' + // * 'Emails[2]' expected type 'string', got unconvertible type 'int' +} + +func ExampleDecode_metadata() { + type Person struct { + Name string + Age int + } + + // This input can come from anywhere, but typically comes from + // something like decoding JSON where we're not quite sure of the + // struct initially. + input := map[string]interface{}{ + "name": "Mitchell", + "age": 91, + "email": "foo@bar.com", + } + + // For metadata, we make a more advanced DecoderConfig so we can + // more finely configure the decoder that is used. In this case, we + // just tell the decoder we want to track metadata. + var md Metadata + var result Person + config := &DecoderConfig{ + Metadata: &md, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + panic(err) + } + + if err := decoder.Decode(input); err != nil { + panic(err) + } + + fmt.Printf("Unused keys: %#v", md.Unused) + // Output: + // Unused keys: []string{"email"} +} + +func ExampleDecode_weaklyTypedInput() { + type Person struct { + Name string + Age int + Emails []string + } + + // This input can come from anywhere, but typically comes from + // something like decoding JSON, generated by a weakly typed language + // such as PHP. + input := map[string]interface{}{ + "name": 123, // number => string + "age": "42", // string => number + "emails": map[string]interface{}{}, // empty map => empty array + } + + var result Person + config := &DecoderConfig{ + WeaklyTypedInput: true, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + panic(err) + } + + err = decoder.Decode(input) + if err != nil { + panic(err) + } + + fmt.Printf("%#v", result) + // Output: mapstructure.Person{Name:"123", Age:42, Emails:[]string{}} +} + +func ExampleDecode_tags() { + // Note that the mapstructure tags defined in the struct type + // can indicate which fields the values are mapped to. + type Person struct { + Name string `mapstructure:"person_name"` + Age int `mapstructure:"person_age"` + } + + input := map[string]interface{}{ + "person_name": "Mitchell", + "person_age": 91, + } + + var result Person + err := Decode(input, &result) + if err != nil { + panic(err) + } + + fmt.Printf("%#v", result) + // Output: + // mapstructure.Person{Name:"Mitchell", Age:91} +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_test.go b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_test.go new file mode 100644 index 0000000000..23029c7c4a --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/mapstructure/mapstructure_test.go @@ -0,0 +1,828 @@ +package mapstructure + +import ( + "reflect" + "sort" + "testing" +) + +type Basic struct { + Vstring string + Vint int + Vuint uint + Vbool bool + Vfloat float64 + Vextra string + vsilent bool + Vdata interface{} +} + +type Embedded struct { + Basic + Vunique string +} + +type EmbeddedPointer struct { + *Basic + Vunique string +} + +type EmbeddedSquash struct { + Basic `mapstructure:",squash"` + Vunique string +} + +type Map struct { + Vfoo string + Vother map[string]string +} + +type MapOfStruct struct { + Value map[string]Basic +} + +type Nested struct { + Vfoo string + Vbar Basic +} + +type NestedPointer struct { + Vfoo string + Vbar *Basic +} + +type Slice struct { + Vfoo string + Vbar []string +} + +type SliceOfStruct struct { + Value []Basic +} + +type Tagged struct { + Extra string `mapstructure:"bar,what,what"` + Value string `mapstructure:"foo"` +} + +type TypeConversionResult struct { + IntToFloat float32 + IntToUint uint + IntToBool bool + IntToString string + UintToInt int + UintToFloat float32 + UintToBool bool + UintToString string + BoolToInt int + BoolToUint uint + BoolToFloat float32 + BoolToString string + FloatToInt int + FloatToUint uint + FloatToBool bool + FloatToString string + SliceUint8ToString string + StringToInt int + StringToUint uint + StringToBool bool + StringToFloat float32 + SliceToMap map[string]interface{} + MapToSlice []interface{} +} + +func TestBasicTypes(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "foo", + "vint": 42, + "Vuint": 42, + "vbool": true, + "Vfloat": 42.42, + "vsilent": true, + "vdata": 42, + } + + var result Basic + err := Decode(input, &result) + if err != nil { + t.Errorf("got an err: %s", err.Error()) + t.FailNow() + } + + if result.Vstring != "foo" { + t.Errorf("vstring value should be 'foo': %#v", result.Vstring) + } + + if result.Vint != 42 { + t.Errorf("vint value should be 42: %#v", result.Vint) + } + + if result.Vuint != 42 { + t.Errorf("vuint value should be 42: %#v", result.Vuint) + } + + if result.Vbool != true { + t.Errorf("vbool value should be true: %#v", result.Vbool) + } + + if result.Vfloat != 42.42 { + t.Errorf("vfloat value should be 42.42: %#v", result.Vfloat) + } + + if result.Vextra != "" { + t.Errorf("vextra value should be empty: %#v", result.Vextra) + } + + if result.vsilent != false { + t.Error("vsilent should not be set, it is unexported") + } + + if result.Vdata != 42 { + t.Error("vdata should be valid") + } +} + +func TestBasic_IntWithFloat(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vint": float64(42), + } + + var result Basic + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err) + } +} + +func TestDecode_Embedded(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "foo", + "Basic": map[string]interface{}{ + "vstring": "innerfoo", + }, + "vunique": "bar", + } + + var result Embedded + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err.Error()) + } + + if result.Vstring != "innerfoo" { + t.Errorf("vstring value should be 'innerfoo': %#v", result.Vstring) + } + + if result.Vunique != "bar" { + t.Errorf("vunique value should be 'bar': %#v", result.Vunique) + } +} + +func TestDecode_EmbeddedPointer(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "foo", + "Basic": map[string]interface{}{ + "vstring": "innerfoo", + }, + "vunique": "bar", + } + + var result EmbeddedPointer + err := Decode(input, &result) + if err == nil { + t.Fatal("should get error") + } +} + +func TestDecode_EmbeddedSquash(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "foo", + "vunique": "bar", + } + + var result EmbeddedSquash + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err.Error()) + } + + if result.Vstring != "foo" { + t.Errorf("vstring value should be 'foo': %#v", result.Vstring) + } + + if result.Vunique != "bar" { + t.Errorf("vunique value should be 'bar': %#v", result.Vunique) + } +} + +func TestDecode_DecodeHook(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vint": "WHAT", + } + + decodeHook := func(from reflect.Kind, to reflect.Kind, v interface{}) (interface{}, error) { + if from == reflect.String && to != reflect.String { + return 5, nil + } + + return v, nil + } + + var result Basic + config := &DecoderConfig{ + DecodeHook: decodeHook, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err != nil { + t.Fatalf("got an err: %s", err) + } + + if result.Vint != 5 { + t.Errorf("vint should be 5: %#v", result.Vint) + } +} + +func TestDecode_Nil(t *testing.T) { + t.Parallel() + + var input interface{} = nil + result := Basic{ + Vstring: "foo", + } + + err := Decode(input, &result) + if err != nil { + t.Fatalf("err: %s", err) + } + + if result.Vstring != "foo" { + t.Fatalf("bad: %#v", result.Vstring) + } +} + +func TestDecode_NonStruct(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "foo": "bar", + "bar": "baz", + } + + var result map[string]string + err := Decode(input, &result) + if err != nil { + t.Fatalf("err: %s", err) + } + + if result["foo"] != "bar" { + t.Fatal("foo is not bar") + } +} + +func TestDecode_TypeConversion(t *testing.T) { + input := map[string]interface{}{ + "IntToFloat": 42, + "IntToUint": 42, + "IntToBool": 1, + "IntToString": 42, + "UintToInt": 42, + "UintToFloat": 42, + "UintToBool": 42, + "UintToString": 42, + "BoolToInt": true, + "BoolToUint": true, + "BoolToFloat": true, + "BoolToString": true, + "FloatToInt": 42.42, + "FloatToUint": 42.42, + "FloatToBool": 42.42, + "FloatToString": 42.42, + "SliceUint8ToString": []uint8("foo"), + "StringToInt": "42", + "StringToUint": "42", + "StringToBool": "1", + "StringToFloat": "42.42", + "SliceToMap": []interface{}{}, + "MapToSlice": map[string]interface{}{}, + } + + expectedResultStrict := TypeConversionResult{ + IntToFloat: 42.0, + IntToUint: 42, + UintToInt: 42, + UintToFloat: 42, + BoolToInt: 0, + BoolToUint: 0, + BoolToFloat: 0, + FloatToInt: 42, + FloatToUint: 42, + } + + expectedResultWeak := TypeConversionResult{ + IntToFloat: 42.0, + IntToUint: 42, + IntToBool: true, + IntToString: "42", + UintToInt: 42, + UintToFloat: 42, + UintToBool: true, + UintToString: "42", + BoolToInt: 1, + BoolToUint: 1, + BoolToFloat: 1, + BoolToString: "1", + FloatToInt: 42, + FloatToUint: 42, + FloatToBool: true, + FloatToString: "42.42", + SliceUint8ToString: "foo", + StringToInt: 42, + StringToUint: 42, + StringToBool: true, + StringToFloat: 42.42, + SliceToMap: map[string]interface{}{}, + MapToSlice: []interface{}{}, + } + + // Test strict type conversion + var resultStrict TypeConversionResult + err := Decode(input, &resultStrict) + if err == nil { + t.Errorf("should return an error") + } + if !reflect.DeepEqual(resultStrict, expectedResultStrict) { + t.Errorf("expected %v, got: %v", expectedResultStrict, resultStrict) + } + + // Test weak type conversion + var decoder *Decoder + var resultWeak TypeConversionResult + + config := &DecoderConfig{ + WeaklyTypedInput: true, + Result: &resultWeak, + } + + decoder, err = NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err != nil { + t.Fatalf("got an err: %s", err) + } + + if !reflect.DeepEqual(resultWeak, expectedResultWeak) { + t.Errorf("expected \n%#v, got: \n%#v", expectedResultWeak, resultWeak) + } +} + +func TestDecoder_ErrorUnused(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "hello", + "foo": "bar", + } + + var result Basic + config := &DecoderConfig{ + ErrorUnused: true, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err == nil { + t.Fatal("expected error") + } +} + +func TestMap(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vfoo": "foo", + "vother": map[interface{}]interface{}{ + "foo": "foo", + "bar": "bar", + }, + } + + var result Map + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an error: %s", err) + } + + if result.Vfoo != "foo" { + t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) + } + + if result.Vother == nil { + t.Fatal("vother should not be nil") + } + + if len(result.Vother) != 2 { + t.Error("vother should have two items") + } + + if result.Vother["foo"] != "foo" { + t.Errorf("'foo' key should be foo, got: %#v", result.Vother["foo"]) + } + + if result.Vother["bar"] != "bar" { + t.Errorf("'bar' key should be bar, got: %#v", result.Vother["bar"]) + } +} + +func TestMapOfStruct(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "value": map[string]interface{}{ + "foo": map[string]string{"vstring": "one"}, + "bar": map[string]string{"vstring": "two"}, + }, + } + + var result MapOfStruct + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err) + } + + if result.Value == nil { + t.Fatal("value should not be nil") + } + + if len(result.Value) != 2 { + t.Error("value should have two items") + } + + if result.Value["foo"].Vstring != "one" { + t.Errorf("foo value should be 'one', got: %s", result.Value["foo"].Vstring) + } + + if result.Value["bar"].Vstring != "two" { + t.Errorf("bar value should be 'two', got: %s", result.Value["bar"].Vstring) + } +} + +func TestNestedType(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vfoo": "foo", + "vbar": map[string]interface{}{ + "vstring": "foo", + "vint": 42, + "vbool": true, + }, + } + + var result Nested + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err.Error()) + } + + if result.Vfoo != "foo" { + t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) + } + + if result.Vbar.Vstring != "foo" { + t.Errorf("vstring value should be 'foo': %#v", result.Vbar.Vstring) + } + + if result.Vbar.Vint != 42 { + t.Errorf("vint value should be 42: %#v", result.Vbar.Vint) + } + + if result.Vbar.Vbool != true { + t.Errorf("vbool value should be true: %#v", result.Vbar.Vbool) + } + + if result.Vbar.Vextra != "" { + t.Errorf("vextra value should be empty: %#v", result.Vbar.Vextra) + } +} + +func TestNestedTypePointer(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vfoo": "foo", + "vbar": &map[string]interface{}{ + "vstring": "foo", + "vint": 42, + "vbool": true, + }, + } + + var result NestedPointer + err := Decode(input, &result) + if err != nil { + t.Fatalf("got an err: %s", err.Error()) + } + + if result.Vfoo != "foo" { + t.Errorf("vfoo value should be 'foo': %#v", result.Vfoo) + } + + if result.Vbar.Vstring != "foo" { + t.Errorf("vstring value should be 'foo': %#v", result.Vbar.Vstring) + } + + if result.Vbar.Vint != 42 { + t.Errorf("vint value should be 42: %#v", result.Vbar.Vint) + } + + if result.Vbar.Vbool != true { + t.Errorf("vbool value should be true: %#v", result.Vbar.Vbool) + } + + if result.Vbar.Vextra != "" { + t.Errorf("vextra value should be empty: %#v", result.Vbar.Vextra) + } +} + +func TestSlice(t *testing.T) { + t.Parallel() + + inputStringSlice := map[string]interface{}{ + "vfoo": "foo", + "vbar": []string{"foo", "bar", "baz"}, + } + + inputStringSlicePointer := map[string]interface{}{ + "vfoo": "foo", + "vbar": &[]string{"foo", "bar", "baz"}, + } + + outputStringSlice := &Slice{ + "foo", + []string{"foo", "bar", "baz"}, + } + + testSliceInput(t, inputStringSlice, outputStringSlice) + testSliceInput(t, inputStringSlicePointer, outputStringSlice) +} + +func TestInvalidSlice(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vfoo": "foo", + "vbar": 42, + } + + result := Slice{} + err := Decode(input, &result) + if err == nil { + t.Errorf("expected failure") + } +} + +func TestSliceOfStruct(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "value": []map[string]interface{}{ + {"vstring": "one"}, + {"vstring": "two"}, + }, + } + + var result SliceOfStruct + err := Decode(input, &result) + if err != nil { + t.Fatalf("got unexpected error: %s", err) + } + + if len(result.Value) != 2 { + t.Fatalf("expected two values, got %d", len(result.Value)) + } + + if result.Value[0].Vstring != "one" { + t.Errorf("first value should be 'one', got: %s", result.Value[0].Vstring) + } + + if result.Value[1].Vstring != "two" { + t.Errorf("second value should be 'two', got: %s", result.Value[1].Vstring) + } +} + +func TestInvalidType(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": 42, + } + + var result Basic + err := Decode(input, &result) + if err == nil { + t.Fatal("error should exist") + } + + derr, ok := err.(*Error) + if !ok { + t.Fatalf("error should be kind of Error, instead: %#v", err) + } + + if derr.Errors[0] != "'Vstring' expected type 'string', got unconvertible type 'int'" { + t.Errorf("got unexpected error: %s", err) + } +} + +func TestMetadata(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vfoo": "foo", + "vbar": map[string]interface{}{ + "vstring": "foo", + "Vuint": 42, + "foo": "bar", + }, + "bar": "nil", + } + + var md Metadata + var result Nested + config := &DecoderConfig{ + Metadata: &md, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err != nil { + t.Fatalf("err: %s", err.Error()) + } + + expectedKeys := []string{"Vfoo", "Vbar.Vstring", "Vbar.Vuint", "Vbar"} + if !reflect.DeepEqual(md.Keys, expectedKeys) { + t.Fatalf("bad keys: %#v", md.Keys) + } + + expectedUnused := []string{"Vbar.foo", "bar"} + if !reflect.DeepEqual(md.Unused, expectedUnused) { + t.Fatalf("bad unused: %#v", md.Unused) + } +} + +func TestMetadata_Embedded(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": "foo", + "vunique": "bar", + } + + var md Metadata + var result EmbeddedSquash + config := &DecoderConfig{ + Metadata: &md, + Result: &result, + } + + decoder, err := NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err != nil { + t.Fatalf("err: %s", err.Error()) + } + + expectedKeys := []string{"Vstring", "Vunique"} + + sort.Strings(md.Keys) + if !reflect.DeepEqual(md.Keys, expectedKeys) { + t.Fatalf("bad keys: %#v", md.Keys) + } + + expectedUnused := []string{} + if !reflect.DeepEqual(md.Unused, expectedUnused) { + t.Fatalf("bad unused: %#v", md.Unused) + } +} + +func TestNonPtrValue(t *testing.T) { + t.Parallel() + + err := Decode(map[string]interface{}{}, Basic{}) + if err == nil { + t.Fatal("error should exist") + } + + if err.Error() != "result must be a pointer" { + t.Errorf("got unexpected error: %s", err) + } +} + +func TestTagged(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "foo": "bar", + "bar": "value", + } + + var result Tagged + err := Decode(input, &result) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if result.Value != "bar" { + t.Errorf("value should be 'bar', got: %#v", result.Value) + } + + if result.Extra != "value" { + t.Errorf("extra should be 'value', got: %#v", result.Extra) + } +} + +func TestWeakDecode(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "foo": "4", + "bar": "value", + } + + var result struct { + Foo int + Bar string + } + + if err := WeakDecode(input, &result); err != nil { + t.Fatalf("err: %s", err) + } + if result.Foo != 4 { + t.Fatalf("bad: %#v", result) + } + if result.Bar != "value" { + t.Fatalf("bad: %#v", result) + } +} + +func testSliceInput(t *testing.T, input map[string]interface{}, expected *Slice) { + var result Slice + err := Decode(input, &result) + if err != nil { + t.Fatalf("got error: %s", err) + } + + if result.Vfoo != expected.Vfoo { + t.Errorf("Vfoo expected '%s', got '%s'", expected.Vfoo, result.Vfoo) + } + + if result.Vbar == nil { + t.Fatalf("Vbar a slice, got '%#v'", result.Vbar) + } + + if len(result.Vbar) != len(expected.Vbar) { + t.Errorf("Vbar length should be %d, got %d", len(expected.Vbar), len(result.Vbar)) + } + + for i, v := range result.Vbar { + if v != expected.Vbar[i] { + t.Errorf( + "Vbar[%d] should be '%#v', got '%#v'", + i, expected.Vbar[i], v) + } + } +} diff --git a/Godeps/_workspace/src/github.com/racker/perigee/.gitignore b/Godeps/_workspace/src/github.com/racker/perigee/.gitignore new file mode 100644 index 0000000000..49ca32aa20 --- /dev/null +++ b/Godeps/_workspace/src/github.com/racker/perigee/.gitignore @@ -0,0 +1,2 @@ +bin/* +pkg/* diff --git a/Godeps/_workspace/src/github.com/racker/perigee/LICENSE b/Godeps/_workspace/src/github.com/racker/perigee/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/Godeps/_workspace/src/github.com/racker/perigee/LICENSE @@ -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. diff --git a/Godeps/_workspace/src/github.com/racker/perigee/README.md b/Godeps/_workspace/src/github.com/racker/perigee/README.md new file mode 100644 index 0000000000..81cbf4a95f --- /dev/null +++ b/Godeps/_workspace/src/github.com/racker/perigee/README.md @@ -0,0 +1,120 @@ +# perigee + +Perigee provides a REST client that, while it should be generic enough to use with most any RESTful API, is nonetheless optimized to the needs of the OpenStack APIs. +Perigee grew out of the need to refactor out common API access code from the [gorax](http://github.com/racker/gorax) project. + +Several things influenced the name of the project. +Numerous elements of the OpenStack ecosystem are named after astronomical artifacts. +Additionally, perigee occurs when two orbiting bodies are closest to each other. +Perigee seemed appropriate for something aiming to bring OpenStack and other RESTful services closer to the end-user. + +**This library is still in the very early stages of development. Unless you want to contribute, it probably isn't what you want** + +## Installation and Testing + +To install: + +```bash +go get github.com/racker/perigee +``` + +To run unit tests: + +```bash +go test github.com/racker/perigee +``` + +## Contributing + +The following guidelines are preliminary, as this project is just starting out. +However, this should serve as a working first-draft. + +### Branching + +The master branch must always be a valid build. +The `go get` command will not work otherwise. +Therefore, development must occur on a different branch. + +When creating a feature branch, do so off the master branch: + +```bash +git checkout master +git pull +git checkout -b featureBranch +git checkout -b featureBranch-wip # optional +``` + +Perform all your editing and testing in the WIP-branch. +Feel free to make as many commits as you see fit. +You may even open "WIP" pull requests from your feature branch to seek feedback. +WIP pull requests will **never** be merged, however. + +To get code merged, you'll need to "squash" your changes into one or more clean commits in the feature branch. +These steps should be followed: + +```bash +git checkout featureBranch +git merge --squash featureBranch-wip +git commit -a +git push origin featureBranch +``` + +You may now open a nice, clean, self-contained pull request from featureBranch to master. + +The `git commit -a` command above will open a text editor so that +you may provide a comprehensive description of the changes. + +In general, when submitting a pull request against master, +be sure to answer the following questions: + +- What is the problem? +- Why is it a problem? +- What is your solution? +- How does your solution work? (Recommended for non-trivial changes.) +- Why should we use your solution over someone elses? (Recommended especially if multiple solutions being discussed.) + +Remember that monster-sized pull requests are a bear to code-review, +so having helpful commit logs are an absolute must to review changes as quickly as possible. + +Finally, (s)he who breaks master is ultimately responsible for fixing master. + +### Source Representation + +The Go community firmly believes in a consistent representation for all Go source code. +We do too. +Make sure all source code is passed through "go fmt" *before* you create your pull request. + +Please note, however, that we fully acknowledge and recognize that we no longer rely upon punch-cards for representing source files. +Therefore, no 80-column limit exists. +However, if a line exceeds 132 columns, you may want to consider splitting the line. + +### Unit and Integration Tests + +Pull requests that include non-trivial code changes without accompanying unit tests will be flatly rejected. +While we have no way of enforcing this practice, +you can ensure your code is thoroughly tested by always [writing tests first by intention.](http://en.wikipedia.org/wiki/Test-driven_development) + +When creating a pull request, if even one test fails, the PR will be rejected. +Make sure all unit tests pass. +Make sure all integration tests pass. + +### Documentation + +Private functions and methods which are obvious to anyone unfamiliar with gorax needn't be accompanied by documentation. +However, this is a code-smell; if submitting a PR, expect to justify your decision. + +Public functions, regardless of how obvious, **must** have accompanying godoc-style documentation. +This is not to suggest you should provide a tome for each function, however. +Sometimes a link to more information is more appropriate, provided the link is stable, reliable, and pertinent. + +Changing documentation often results in bizarre diffs in pull requests, due to text often spanning multiple lines. +To work around this, put [one logical thought or sentence on a single line.](http://rhodesmill.org/brandon/2012/one-sentence-per-line/) +While this looks weird in a plain-text editor, +remember that both godoc and HTML viewers will reflow text. +The source code and its comments should be easy to edit with minimal diff pollution. +Let software dedicated to presenting the documentation to human readers deal with its presentation. + +## Examples + +t.b.d. + diff --git a/Godeps/_workspace/src/github.com/racker/perigee/api.go b/Godeps/_workspace/src/github.com/racker/perigee/api.go new file mode 100644 index 0000000000..0fcbadbee5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/racker/perigee/api.go @@ -0,0 +1,269 @@ +// vim: ts=8 sw=8 noet ai + +package perigee + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "strings" +) + +// The UnexpectedResponseCodeError structure represents a mismatch in understanding between server and client in terms of response codes. +// Most often, this is due to an actual error condition (e.g., getting a 404 for a resource when you expect a 200). +// However, it needn't always be the case (e.g., getting a 204 (No Content) response back when a 200 is expected). +type UnexpectedResponseCodeError struct { + Url string + Expected []int + Actual int + Body []byte +} + +func (err *UnexpectedResponseCodeError) Error() string { + return fmt.Sprintf("Expected HTTP response code %d when accessing URL(%s); got %d instead with the following body:\n%s", err.Expected, err.Url, err.Actual, string(err.Body)) +} + +// Request issues an HTTP request, marshaling parameters, and unmarshaling results, as configured in the provided Options parameter. +// The Response structure returned, if any, will include accumulated results recovered from the HTTP server. +// See the Response structure for more details. +func Request(method string, url string, opts Options) (*Response, error) { + var body io.Reader + var response Response + + client := opts.CustomClient + if client == nil { + client = new(http.Client) + } + + contentType := opts.ContentType + + body = nil + if opts.ReqBody != nil { + if contentType == "" { + contentType = "application/json" + } + + if contentType == "application/json" { + bodyText, err := json.Marshal(opts.ReqBody) + if err != nil { + return nil, err + } + body = strings.NewReader(string(bodyText)) + if opts.DumpReqJson { + log.Printf("Making request:\n%#v\n", string(bodyText)) + } + } else { + // assume opts.ReqBody implements the correct interface + body = opts.ReqBody.(io.Reader) + } + } + + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + + if contentType != "" { + req.Header.Add("Content-Type", contentType) + } + + if opts.ContentLength > 0 { + req.ContentLength = opts.ContentLength + req.Header.Add("Content-Length", string(opts.ContentLength)) + } + + if opts.MoreHeaders != nil { + for k, v := range opts.MoreHeaders { + req.Header.Add(k, v) + } + } + + if accept := req.Header.Get("Accept"); accept == "" { + accept = opts.Accept + if accept == "" { + accept = "application/json" + } + req.Header.Add("Accept", accept) + } + + if opts.SetHeaders != nil { + err = opts.SetHeaders(req) + if err != nil { + return &response, err + } + } + + httpResponse, err := client.Do(req) + if httpResponse != nil { + response.HttpResponse = *httpResponse + response.StatusCode = httpResponse.StatusCode + } + + if err != nil { + return &response, err + } + // This if-statement is legacy code, preserved for backward compatibility. + if opts.StatusCode != nil { + *opts.StatusCode = httpResponse.StatusCode + } + + acceptableResponseCodes := opts.OkCodes + if len(acceptableResponseCodes) != 0 { + if not_in(httpResponse.StatusCode, acceptableResponseCodes) { + b, _ := ioutil.ReadAll(httpResponse.Body) + httpResponse.Body.Close() + return &response, &UnexpectedResponseCodeError{ + Url: url, + Expected: acceptableResponseCodes, + Actual: httpResponse.StatusCode, + Body: b, + } + } + } + if opts.Results != nil { + defer httpResponse.Body.Close() + jsonResult, err := ioutil.ReadAll(httpResponse.Body) + response.JsonResult = jsonResult + if err != nil { + return &response, err + } + + err = json.Unmarshal(jsonResult, opts.Results) + // This if-statement is legacy code, preserved for backward compatibility. + if opts.ResponseJson != nil { + *opts.ResponseJson = jsonResult + } + } + return &response, err +} + +// not_in returns false if, and only if, the provided needle is _not_ +// in the given set of integers. +func not_in(needle int, haystack []int) bool { + for _, straw := range haystack { + if needle == straw { + return false + } + } + return true +} + +// Post makes a POST request against a server using the provided HTTP client. +// The url must be a fully-formed URL string. +// DEPRECATED. Use Request() instead. +func Post(url string, opts Options) error { + r, err := Request("POST", url, opts) + if opts.Response != nil { + *opts.Response = r + } + return err +} + +// Get makes a GET request against a server using the provided HTTP client. +// The url must be a fully-formed URL string. +// DEPRECATED. Use Request() instead. +func Get(url string, opts Options) error { + r, err := Request("GET", url, opts) + if opts.Response != nil { + *opts.Response = r + } + return err +} + +// Delete makes a DELETE request against a server using the provided HTTP client. +// The url must be a fully-formed URL string. +// DEPRECATED. Use Request() instead. +func Delete(url string, opts Options) error { + r, err := Request("DELETE", url, opts) + if opts.Response != nil { + *opts.Response = r + } + return err +} + +// Put makes a PUT request against a server using the provided HTTP client. +// The url must be a fully-formed URL string. +// DEPRECATED. Use Request() instead. +func Put(url string, opts Options) error { + r, err := Request("PUT", url, opts) + if opts.Response != nil { + *opts.Response = r + } + return err +} + +// Options describes a set of optional parameters to the various request calls. +// +// The custom client can be used for a variety of purposes beyond selecting encrypted versus unencrypted channels. +// Transports can be defined to provide augmented logging, header manipulation, et. al. +// +// If the ReqBody field is provided, it will be embedded as a JSON object. +// Otherwise, provide nil. +// +// If JSON output is to be expected from the response, +// provide either a pointer to the container structure in Results, +// or a pointer to a nil-initialized pointer variable. +// The latter method will cause the unmarshaller to allocate the container type for you. +// If no response is expected, provide a nil Results value. +// +// The MoreHeaders map, if non-nil or empty, provides a set of headers to add to those +// already present in the request. At present, only Accepted and Content-Type are set +// by default. +// +// OkCodes provides a set of acceptable, positive responses. +// +// If provided, StatusCode specifies a pointer to an integer, which will receive the +// returned HTTP status code, successful or not. DEPRECATED; use the Response.StatusCode field instead for new software. +// +// ResponseJson, if specified, provides a means for returning the raw JSON. This is +// most useful for diagnostics. DEPRECATED; use the Response.JsonResult field instead for new software. +// +// DumpReqJson, if set to true, will cause the request to appear to stdout for debugging purposes. +// This attribute may be removed at any time in the future; DO NOT use this attribute in production software. +// +// Response, if set, provides a way to communicate the complete set of HTTP response, raw JSON, status code, and +// other useful attributes back to the caller. Note that the Request() method returns a Response structure as part +// of its public interface; you don't need to set the Response field here to use this structure. The Response field +// exists primarily for legacy or deprecated functions. +// +// SetHeaders allows the caller to provide code to set any custom headers programmatically. Typically, this +// facility can invoke, e.g., SetBasicAuth() on the request to easily set up authentication. +// Any error generated will terminate the request and will propegate back to the caller. +type Options struct { + CustomClient *http.Client + ReqBody interface{} + Results interface{} + MoreHeaders map[string]string + OkCodes []int + StatusCode *int `DEPRECATED` + DumpReqJson bool `UNSUPPORTED` + ResponseJson *[]byte `DEPRECATED` + Response **Response + ContentType string `json:"Content-Type,omitempty"` + ContentLength int64 `json:"Content-Length,omitempty"` + Accept string `json:"Accept,omitempty"` + SetHeaders func(r *http.Request) error +} + +// Response contains return values from the various request calls. +// +// HttpResponse will return the http response from the request call. +// Note: HttpResponse.Body is always closed and will not be available from this return value. +// +// StatusCode specifies the returned HTTP status code, successful or not. +// +// If Results is specified in the Options: +// - JsonResult will contain the raw return from the request call +// This is most useful for diagnostics. +// - Result will contain the unmarshalled json either in the Result passed in +// or the unmarshaller will allocate the container type for you. + +type Response struct { + HttpResponse http.Response + JsonResult []byte + Results interface{} + StatusCode int +} diff --git a/Godeps/_workspace/src/github.com/racker/perigee/api_test.go b/Godeps/_workspace/src/github.com/racker/perigee/api_test.go new file mode 100644 index 0000000000..da943b247b --- /dev/null +++ b/Godeps/_workspace/src/github.com/racker/perigee/api_test.go @@ -0,0 +1,226 @@ +package perigee + +import ( + "bytes" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestNormal(t *testing.T) { + handler := http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("testing")) + }) + ts := httptest.NewServer(handler) + defer ts.Close() + + response, err := Request("GET", ts.URL, Options{}) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + if response.StatusCode != 200 { + t.Fatalf("response code %d is not 200", response.StatusCode) + } +} + +func TestOKCodes(t *testing.T) { + expectCode := 201 + handler := http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(expectCode) + w.Write([]byte("testing")) + }) + ts := httptest.NewServer(handler) + defer ts.Close() + + options := Options{ + OkCodes: []int{expectCode}, + } + results, err := Request("GET", ts.URL, options) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + if results.StatusCode != expectCode { + t.Fatalf("response code %d is not %d", results.StatusCode, expectCode) + } +} + +func TestLocation(t *testing.T) { + newLocation := "http://www.example.com" + handler := http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Location", newLocation) + w.Write([]byte("testing")) + }) + ts := httptest.NewServer(handler) + defer ts.Close() + + response, err := Request("GET", ts.URL, Options{}) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + location, err := response.HttpResponse.Location() + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if location.String() != newLocation { + t.Fatalf("location returned \"%s\" is not \"%s\"", location.String(), newLocation) + } +} + +func TestHeaders(t *testing.T) { + newLocation := "http://www.example.com" + handler := http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Location", newLocation) + w.Write([]byte("testing")) + }) + ts := httptest.NewServer(handler) + defer ts.Close() + + response, err := Request("GET", ts.URL, Options{}) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + location := response.HttpResponse.Header.Get("Location") + if location == "" { + t.Fatalf("Location should not empty") + } + + if location != newLocation { + t.Fatalf("location returned \"%s\" is not \"%s\"", location, newLocation) + } +} + +func TestCustomHeaders(t *testing.T) { + var contentType, accept, contentLength string + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + m := map[string][]string(r.Header) + contentType = m["Content-Type"][0] + accept = m["Accept"][0] + contentLength = m["Content-Length"][0] + }) + ts := httptest.NewServer(handler) + defer ts.Close() + + _, err := Request("GET", ts.URL, Options{ + ContentLength: 5, + ContentType: "x-application/vb", + Accept: "x-application/c", + ReqBody: strings.NewReader("Hello"), + }) + if err != nil { + t.Fatalf(err.Error()) + } + + if contentType != "x-application/vb" { + t.Fatalf("I expected x-application/vb; got ", contentType) + } + + if contentLength != "5" { + t.Fatalf("I expected 5 byte content length; got ", contentLength) + } + + if accept != "x-application/c" { + t.Fatalf("I expected x-application/c; got ", accept) + } +} + +func TestJson(t *testing.T) { + newLocation := "http://www.example.com" + jsonBytes := []byte(`{"foo": {"bar": "baz"}}`) + handler := http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Location", newLocation) + w.Write(jsonBytes) + }) + ts := httptest.NewServer(handler) + defer ts.Close() + + type Data struct { + Foo struct { + Bar string `json:"bar"` + } `json:"foo"` + } + var data Data + + response, err := Request("GET", ts.URL, Options{Results: &data}) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if bytes.Compare(jsonBytes, response.JsonResult) != 0 { + t.Fatalf("json returned \"%s\" is not \"%s\"", response.JsonResult, jsonBytes) + } + + if data.Foo.Bar != "baz" { + t.Fatalf("Results returned %v", data) + } +} + +func TestSetHeaders(t *testing.T) { + var wasCalled bool + handler := http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hi")) + }) + ts := httptest.NewServer(handler) + defer ts.Close() + + _, err := Request("GET", ts.URL, Options{ + SetHeaders: func(r *http.Request) error { + wasCalled = true + return nil + }, + }) + + if err != nil { + t.Fatal(err) + } + + if !wasCalled { + t.Fatal("I expected header setter callback to be called, but it wasn't") + } + + myError := fmt.Errorf("boo") + + _, err = Request("GET", ts.URL, Options{ + SetHeaders: func(r *http.Request) error { + return myError + }, + }) + + if err != myError { + t.Fatal("I expected errors to propegate back to the caller.") + } +} + +func TestBodilessMethodsAreSentWithoutContentHeaders(t *testing.T) { + var h map[string][]string + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h = r.Header + }) + ts := httptest.NewServer(handler) + defer ts.Close() + + _, err := Request("GET", ts.URL, Options{}) + if err != nil { + t.Fatalf(err.Error()) + } + + if len(h["Content-Type"]) != 0 { + t.Fatalf("I expected nothing for Content-Type but got ", h["Content-Type"]) + } + + if len(h["Content-Length"]) != 0 { + t.Fatalf("I expected nothing for Content-Length but got ", h["Content-Length"]) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/.editorconfig b/Godeps/_workspace/src/github.com/rackspace/gophercloud/.editorconfig new file mode 100644 index 0000000000..2655ebc083 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# All files +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true + +# Golang +[*.go] +indent_style = tab +indent_size = 2 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml b/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml new file mode 100644 index 0000000000..6e1dbd0a83 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/.travis.yml @@ -0,0 +1,14 @@ +language: go +install: + - go get -v . +go: + - 1.1 + - 1.2 + - tip +after_success: + - go get code.google.com/p/go.tools/cmd/cover + - go get github.com/axw/gocov/gocov + - go get github.com/mattn/goveralls + - export PATH=$PATH:$HOME/gopath/bin/ + - goveralls 2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8 + diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTORS.md b/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTORS.md new file mode 100644 index 0000000000..9076695c3f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/CONTRIBUTORS.md @@ -0,0 +1,6 @@ +Contributors +============ + +Samuel A. Falvo II +Glen Campbell +Jesse Noller diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/LICENSE b/Godeps/_workspace/src/github.com/rackspace/gophercloud/LICENSE new file mode 100644 index 0000000000..fbbbc9e4cb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/LICENSE @@ -0,0 +1,191 @@ +Copyright 2012-2013 Rackspace, 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. + +------ + + 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 diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/README.asciidoc b/Godeps/_workspace/src/github.com/rackspace/gophercloud/README.asciidoc new file mode 100644 index 0000000000..b7a7c01643 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/README.asciidoc @@ -0,0 +1,44 @@ +== Gophercloud -- V0.1.0 image:https://secure.travis-ci.org/rackspace/gophercloud.png?branch=master["build status",link="https://travis-ci.org/rackspace/gophercloud"] + +Gophercloud currently lets you authenticate with OpenStack providers to create and manage servers. +We are working on extending the API to further include cloud files, block storage, DNS, databases, security groups, and other features. + +WARNING: This library is still in the very early stages of development. Unless you want to contribute, it probably isn't what you want. Yet. + +=== Outstanding Features + +1. Apache 2.0 License, making Gophercloud friendly to commercial and open-source enterprises alike. +2. Gophercloud is one of the most actively maintained Go SDKs for OpenStack. +3. Gophercloud supports Identity V2 and Nova V2 APIs. More coming soon! +4. The up-coming Gophercloud 0.2.0 release supports API extensions, and makes writing support for new extensions easy. +5. Gophercloud supports automatic reauthentication upon auth token timeout, if enabled by your software. +6. Gophercloud is the only SDK implementation with actual acceptance-level integration tests. + +=== What Does it Look Like? + +The Gophercloud 0.1.0 and earlier APIs are now deprecated and obsolete. +No new feature development will occur for 0.1.0 or 0.0.0. +However, we will accept and provide bug fixes for these APIs. +Please refer to the acceptance tests in the master brach for code examples using the v0.1.0 API. +The most up to date documentation for version 0.1.x can be found at link:http://godoc.org/github.com/rackspace/gophercloud[our Godoc.org documentation]. + +We are working on a new API that provides much better support for extensions, pagination, and other features that proved difficult to implement before. +This new API will be substantially more Go-idiomatic as well; one of the complaints received about 0.1.x and earlier is that it didn't "feel" right. +To see what this new API is going to look like, you can look at the code examples up on the link:http://gophercloud.io/docs.html[Gophercloud website]. +If you're interested in tracking progress, note that features for version 0.2.0 will appear in the `v0.2.0` branch until merged to master. + +=== How can I Contribute? + +After using Gophercloud for a while, you might find that it lacks some useful feature, or that existing behavior seems buggy. We welcome contributions +from our users for both missing functionality as well as for bug fixes. We encourage contributors to collaborate with the +link:http://gophercloud.io/community.html[Gophercloud community.] + +Finally, Gophercloud maintains its own link:http://gophercloud.io[announcements and updates blog.] +Feel free to check back now and again to see what's new. + +== License + +Copyright (C) 2013, 2014 Rackspace, Inc. + +Licensed under the Apache License, Version 2.0 + diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/00-authentication.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/00-authentication.go new file mode 100644 index 0000000000..6467203f64 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/00-authentication.go @@ -0,0 +1,30 @@ +// +build acceptance,old + +package main + +import ( + "fmt" + "github.com/rackspace/gophercloud" + "os" + "strings" +) + +func main() { + provider, username, _, apiKey := getCredentials() + + if !strings.Contains(provider, "rackspace") { + fmt.Fprintf(os.Stdout, "Skipping test because provider doesn't support API_KEYs\n") + return + } + + _, err := gophercloud.Authenticate( + provider, + gophercloud.AuthOptions{ + Username: username, + ApiKey: apiKey, + }, + ) + if err != nil { + panic(err) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/01-authentication.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/01-authentication.go new file mode 100644 index 0000000000..5cc9d38d5d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/01-authentication.go @@ -0,0 +1,22 @@ +// +build acceptance,old + +package main + +import ( + "github.com/rackspace/gophercloud" +) + +func main() { + provider, username, password, _ := getCredentials() + + _, err := gophercloud.Authenticate( + provider, + gophercloud.AuthOptions{ + Username: username, + Password: password, + }, + ) + if err != nil { + panic(err) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/02-list-servers.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/02-list-servers.go new file mode 100644 index 0000000000..772852e394 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/02-list-servers.go @@ -0,0 +1,62 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" +) + +var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") + +func main() { + flag.Parse() + + withIdentity(false, func(acc gophercloud.AccessProvider) { + withServerApi(acc, func(api gophercloud.CloudServersProvider) { + tryFullDetails(api) + tryLinksOnly(api) + }) + }) +} + +func tryLinksOnly(api gophercloud.CloudServersProvider) { + servers, err := api.ListServersLinksOnly() + if err != nil { + panic(err) + } + + if !*quiet { + fmt.Println("Id,Name") + for _, s := range servers { + if s.AccessIPv4 != "" { + panic("IPv4 not expected") + } + + if s.Status != "" { + panic("Status not expected") + } + + if s.Progress != 0 { + panic("Progress not expected") + } + + fmt.Printf("%s,\"%s\"\n", s.Id, s.Name) + } + } +} + +func tryFullDetails(api gophercloud.CloudServersProvider) { + servers, err := api.ListServers() + if err != nil { + panic(err) + } + + if !*quiet { + fmt.Println("Id,Name,AccessIPv4,Status,Progress") + for _, s := range servers { + fmt.Printf("%s,\"%s\",%s,%s,%d\n", s.Id, s.Name, s.AccessIPv4, s.Status, s.Progress) + } + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/03-get-server-details.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/03-get-server-details.go new file mode 100644 index 0000000000..01140a9d70 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/03-get-server-details.go @@ -0,0 +1,134 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" + "os" + "github.com/racker/perigee" +) + +var id = flag.String("i", "", "Server ID to get info on. Defaults to first server in your account if unspecified.") +var rgn = flag.String("r", "", "Datacenter region. Leave blank for default region.") +var quiet = flag.Bool("quiet", false, "Run quietly, for acceptance testing. $? non-zero if issue.") + +func main() { + flag.Parse() + + resultCode := 0 + withIdentity(false, func(auth gophercloud.AccessProvider) { + withServerApi(auth, func(servers gophercloud.CloudServersProvider) { + var ( + err error + serverId string + deleteAfterwards bool + ) + + // Figure out which server to provide server details for. + if *id == "" { + deleteAfterwards, serverId, err = locateAServer(servers) + if err != nil { + panic(err) + } + if deleteAfterwards { + defer servers.DeleteServerById(serverId) + } + } else { + serverId = *id + } + + // Grab server details by ID, and provide a report. + s, err := servers.ServerById(serverId) + if err != nil { + panic(err) + } + + configs := []string{ + "Access IPv4: %s\n", + "Access IPv6: %s\n", + " Created: %s\n", + " Flavor: %s\n", + " Host ID: %s\n", + " ID: %s\n", + " Image: %s\n", + " Name: %s\n", + " Progress: %s\n", + " Status: %s\n", + " Tenant ID: %s\n", + " Updated: %s\n", + " User ID: %s\n", + } + + values := []string{ + s.AccessIPv4, + s.AccessIPv6, + s.Created, + s.Flavor.Id, + s.HostId, + s.Id, + s.Image.Id, + s.Name, + fmt.Sprintf("%d", s.Progress), + s.Status, + s.TenantId, + s.Updated, + s.UserId, + } + + if !*quiet { + fmt.Println("Server info:") + for i, _ := range configs { + fmt.Printf(configs[i], values[i]) + } + } + }) + + // Negative test -- We should absolutely never panic for a server that doesn't exist. + withServerApi(auth, func(servers gophercloud.CloudServersProvider) { + _, err := servers.ServerById(randomString("garbage", 32)) + if err == nil { + fmt.Printf("Expected a 404 response when looking for a server known not to exist\n") + resultCode = 1 + } + perigeeError, ok := err.(*perigee.UnexpectedResponseCodeError) + if !ok { + fmt.Printf("Unexpected error type\n") + resultCode = 1 + } else { + if perigeeError.Actual != 404 { + fmt.Printf("Expected a 404 error code\n") + } + } + }) + }) + os.Exit(resultCode) +} + +// locateAServer queries the set of servers owned by the user. If at least one +// exists, the first found is picked, and its ID is returned. Otherwise, a new +// server will be created, and its ID returned. +// +// deleteAfter will be true if the caller should schedule a call to DeleteServerById() +// to clean up. +func locateAServer(servers gophercloud.CloudServersProvider) (deleteAfter bool, id string, err error) { + ss, err := servers.ListServers() + if err != nil { + return false, "", err + } + + if len(ss) > 0 { + // We could just cheat and dump the server details from ss[0]. + // But, that tests ListServers(), and not ServerById(). So, we + // elect not to cheat. + return false, ss[0].Id, nil + } + + serverId, err := createServer(servers, "", "", "", "") + if err != nil { + return false, "", err + } + err = waitForServerState(servers, serverId, "ACTIVE") + return true, serverId, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/04-create-server.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/04-create-server.go new file mode 100644 index 0000000000..03fd606cb7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/04-create-server.go @@ -0,0 +1,47 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" +) + +var region, serverName, imageRef, flavorRef *string +var adminPass = flag.String("a", "", "Administrator password (auto-assigned if none)") +var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance tests. $? non-zero if error.") + +func configure() { + region = flag.String("r", "", "Region in which to create the server. Leave blank for provider-default region.") + serverName = flag.String("n", randomString("ACPTTEST--", 16), "Server name (what you see in the control panel)") + imageRef = flag.String("i", "", "ID of image to deploy onto the server") + flavorRef = flag.String("f", "", "Flavor of server to deploy image upon") + + flag.Parse() +} + +func main() { + configure() + + withIdentity(false, func(auth gophercloud.AccessProvider) { + withServerApi(auth, func(servers gophercloud.CloudServersProvider) { + _, err := createServer(servers, *imageRef, *flavorRef, *serverName, *adminPass) + if err != nil { + panic(err) + } + + allServers, err := servers.ListServers() + if err != nil { + panic(err) + } + + if !*quiet { + fmt.Printf("ID,Name,Status,Progress\n") + for _, i := range allServers { + fmt.Printf("%s,\"%s\",%s,%d\n", i.Id, i.Name, i.Status, i.Progress) + } + } + }) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/05-list-images.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/05-list-images.go new file mode 100644 index 0000000000..5ead18beb4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/05-list-images.go @@ -0,0 +1,32 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" +) + +var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing. $? non-zero on error though.") +var rgn = flag.String("r", "", "Datacenter region to interrogate. Leave blank for provider-default region.") + +func main() { + flag.Parse() + + withIdentity(false, func(auth gophercloud.AccessProvider) { + withServerApi(auth, func(servers gophercloud.CloudServersProvider) { + images, err := servers.ListImages() + if err != nil { + panic(err) + } + + if !*quiet { + fmt.Println("ID,Name,MinRam,MinDisk") + for _, image := range images { + fmt.Printf("%s,\"%s\",%d,%d\n", image.Id, image.Name, image.MinRam, image.MinDisk) + } + } + }) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/06-list-flavors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/06-list-flavors.go new file mode 100644 index 0000000000..65db7da6de --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/06-list-flavors.go @@ -0,0 +1,32 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" +) + +var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing. $? non-zero on error though.") +var rgn = flag.String("r", "", "Datacenter region to interrogate. Leave blank for provider-default region.") + +func main() { + flag.Parse() + + withIdentity(false, func(auth gophercloud.AccessProvider) { + withServerApi(auth, func(servers gophercloud.CloudServersProvider) { + flavors, err := servers.ListFlavors() + if err != nil { + panic(err) + } + + if !*quiet { + fmt.Println("ID,Name,MinRam,MinDisk") + for _, f := range flavors { + fmt.Printf("%s,\"%s\",%d,%d\n", f.Id, f.Name, f.Ram, f.Disk) + } + } + }) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/07-change-admin-password.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/07-change-admin-password.go new file mode 100644 index 0000000000..880fbe8b8a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/07-change-admin-password.go @@ -0,0 +1,49 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" +) + +var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") +var serverId = flag.String("i", "", "ID of server whose admin password is to be changed.") +var newPass = flag.String("p", "", "New password for the server.") + +func main() { + flag.Parse() + + withIdentity(false, func(acc gophercloud.AccessProvider) { + withServerApi(acc, func(api gophercloud.CloudServersProvider) { + // If user doesn't explicitly provide a server ID, create one dynamically. + if *serverId == "" { + var err error + *serverId, err = createServer(api, "", "", "", "") + if err != nil { + panic(err) + } + waitForServerState(api, *serverId, "ACTIVE") + } + + // If no password is provided, create one dynamically. + if *newPass == "" { + *newPass = randomString("", 16) + } + + // Submit the request for changing the admin password. + // Note that we don't verify this actually completes; + // doing so is beyond the scope of the SDK, and should be + // the responsibility of your specific OpenStack provider. + err := api.SetAdminPassword(*serverId, *newPass) + if err != nil { + panic(err) + } + + if !*quiet { + fmt.Println("Password change request submitted.") + } + }) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/08-reauthentication.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/08-reauthentication.go new file mode 100644 index 0000000000..c46f5bbc84 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/08-reauthentication.go @@ -0,0 +1,50 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" +) + +var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing. $? non-zero on error though.") +var rgn = flag.String("r", "", "Datacenter region to interrogate. Leave blank for provider-default region.") + +func main() { + flag.Parse() + + // Invoke withIdentity such that re-auth is enabled. + withIdentity(true, func(auth gophercloud.AccessProvider) { + token1 := auth.AuthToken() + + withServerApi(auth, func(servers gophercloud.CloudServersProvider) { + // Just to confirm everything works, we should be able to list images without error. + _, err := servers.ListImages() + if err != nil { + panic(err) + } + + // Revoke our current authentication token. + auth.Revoke(auth.AuthToken()) + + // Attempt to list images again. This should _succeed_, because we enabled re-authentication. + _, err = servers.ListImages() + if err != nil { + panic(err) + } + + // However, our new authentication token should differ. + token2 := auth.AuthToken() + + if !*quiet { + fmt.Println("Old authentication token: ", token1) + fmt.Println("New authentication token: ", token2) + } + + if token1 == token2 { + panic("Tokens should differ") + } + }) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/09-resize-server.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/09-resize-server.go new file mode 100644 index 0000000000..a2ef3c87e6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/09-resize-server.go @@ -0,0 +1,102 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" + "time" +) + +var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") + +func main() { + flag.Parse() + + withIdentity(false, func(acc gophercloud.AccessProvider) { + withServerApi(acc, func(api gophercloud.CloudServersProvider) { + // These tests are going to take some time to complete. + // So, we'll do two tests at the same time to help amortize test time. + done := make(chan bool) + go resizeRejectTest(api, done) + go resizeAcceptTest(api, done) + _ = <-done + _ = <-done + + if !*quiet { + fmt.Println("Done.") + } + }) + }) +} + +// Perform the resize test, but reject the resize request. +func resizeRejectTest(api gophercloud.CloudServersProvider, done chan bool) { + withServer(api, func(id string) { + newFlavorId := findAlternativeFlavor() + err := api.ResizeServer(id, randomString("ACPTTEST", 24), newFlavorId, "") + if err != nil { + panic(err) + } + + waitForServerState(api, id, "VERIFY_RESIZE") + + err = api.RevertResize(id) + if err != nil { + panic(err) + } + }) + done <- true +} + +// Perform the resize test, but accept the resize request. +func resizeAcceptTest(api gophercloud.CloudServersProvider, done chan bool) { + withServer(api, func(id string) { + newFlavorId := findAlternativeFlavor() + err := api.ResizeServer(id, randomString("ACPTTEST", 24), newFlavorId, "") + if err != nil { + panic(err) + } + + waitForServerState(api, id, "VERIFY_RESIZE") + + err = api.ConfirmResize(id) + if err != nil { + panic(err) + } + }) + done <- true +} + +func withServer(api gophercloud.CloudServersProvider, f func(string)) { + id, err := createServer(api, "", "", "", "") + if err != nil { + panic(err) + } + + for { + s, err := api.ServerById(id) + if err != nil { + panic(err) + } + if s.Status == "ACTIVE" { + break + } + time.Sleep(10 * time.Second) + } + + f(id) + + // I've learned that resizing an instance can fail if a delete request + // comes in prior to its completion. This ends up leaving the server + // in an error state, and neither the resize NOR the delete complete. + // This is a bug in OpenStack, as far as I'm concerned, but thankfully, + // there's an easy work-around -- just wait for your server to return to + // active state first! + waitForServerState(api, id, "ACTIVE") + err = api.DeleteServerById(id) + if err != nil { + panic(err) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/10-reboot-server.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/10-reboot-server.go new file mode 100644 index 0000000000..ba6215a325 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/10-reboot-server.go @@ -0,0 +1,45 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" +) + +var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") + +func main() { + flag.Parse() + + withIdentity(false, func(acc gophercloud.AccessProvider) { + withServerApi(acc, func(servers gophercloud.CloudServersProvider) { + log("Creating server") + serverId, err := createServer(servers, "", "", "", "") + if err != nil { + panic(err) + } + waitForServerState(servers, serverId, "ACTIVE") + + log("Soft-rebooting server") + servers.RebootServer(serverId, false) + waitForServerState(servers, serverId, "REBOOT") + waitForServerState(servers, serverId, "ACTIVE") + + log("Hard-rebooting server") + servers.RebootServer(serverId, true) + waitForServerState(servers, serverId, "HARD_REBOOT") + waitForServerState(servers, serverId, "ACTIVE") + + log("Done") + servers.DeleteServerById(serverId) + }) + }) +} + +func log(s string) { + if !*quiet { + fmt.Println(s) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/11-rescue-unrescue-server.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/11-rescue-unrescue-server.go new file mode 100644 index 0000000000..008ad9d597 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/11-rescue-unrescue-server.go @@ -0,0 +1,52 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" +) + +var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") + +func main() { + flag.Parse() + withIdentity(false, func(acc gophercloud.AccessProvider) { + withServerApi(acc, func(servers gophercloud.CloudServersProvider) { + log("Creating server") + id, err := createServer(servers, "", "", "", "") + if err != nil { + panic(err) + } + waitForServerState(servers, id, "ACTIVE") + defer servers.DeleteServerById(id) + + log("Rescuing server") + adminPass, err := servers.RescueServer(id) + if err != nil { + panic(err) + } + log(" Admin password = " + adminPass) + if len(adminPass) < 1 { + panic("Empty admin password") + } + waitForServerState(servers, id, "RESCUE") + + log("Unrescuing server") + err = servers.UnrescueServer(id) + if err != nil { + panic(err) + } + waitForServerState(servers, id, "ACTIVE") + + log("Done") + }) + }) +} + +func log(s string) { + if !*quiet { + fmt.Println(s) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/12-update-server.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/12-update-server.go new file mode 100644 index 0000000000..c0191f1fee --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/12-update-server.go @@ -0,0 +1,46 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" +) + +var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") + +func main() { + flag.Parse() + withIdentity(false, func(acc gophercloud.AccessProvider) { + withServerApi(acc, func(servers gophercloud.CloudServersProvider) { + log("Creating server") + id, err := createServer(servers, "", "", "", "") + if err != nil { + panic(err) + } + waitForServerState(servers, id, "ACTIVE") + defer servers.DeleteServerById(id) + + log("Updating name of server") + newName := randomString("ACPTTEST", 32) + newDetails, err := servers.UpdateServer(id, gophercloud.NewServerSettings{ + Name: newName, + }) + if err != nil { + panic(err) + } + if newDetails.Name != newName { + panic("Name change didn't appear to take") + } + + log("Done") + }) + }) +} + +func log(s string) { + if !*quiet { + fmt.Println(s) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/13-rebuild-server.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/13-rebuild-server.go new file mode 100644 index 0000000000..ae7e19f60d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/13-rebuild-server.go @@ -0,0 +1,46 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" +) + +var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") + +func main() { + flag.Parse() + withIdentity(false, func(acc gophercloud.AccessProvider) { + withServerApi(acc, func(servers gophercloud.CloudServersProvider) { + log("Creating server") + id, err := createServer(servers, "", "", "", "") + if err != nil { + panic(err) + } + waitForServerState(servers, id, "ACTIVE") + defer servers.DeleteServerById(id) + + log("Rebuilding server") + newDetails, err := servers.RebuildServer(id, gophercloud.NewServer{ + Name: randomString("ACPTTEST", 32), + ImageRef: findAlternativeImage(), + FlavorRef: findAlternativeFlavor(), + AdminPass: randomString("", 16), + }) + if err != nil { + panic(err) + } + waitForServerState(servers, newDetails.Id, "ACTIVE") + + log("Done") + }) + }) +} + +func log(s string) { + if !*quiet { + fmt.Println(s) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/14-list-addresses.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/14-list-addresses.go new file mode 100644 index 0000000000..1d7d26b54d --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/14-list-addresses.go @@ -0,0 +1,66 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" +) + +var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") + +func main() { + flag.Parse() + withIdentity(false, func(acc gophercloud.AccessProvider) { + withServerApi(acc, func(api gophercloud.CloudServersProvider) { + log("Creating server") + id, err := createServer(api, "", "", "", "") + if err != nil { + panic(err) + } + waitForServerState(api, id, "ACTIVE") + defer api.DeleteServerById(id) + + tryAllAddresses(id, api) + tryAddressesByNetwork("private", id, api) + + log("Done") + }) + }) +} + +func tryAllAddresses(id string, api gophercloud.CloudServersProvider) { + log("Getting list of all addresses...") + addresses, err := api.ListAddresses(id) + if (err != nil) && (err != gophercloud.WarnUnauthoritative) { + panic(err) + } + if err == gophercloud.WarnUnauthoritative { + log("Uh oh -- got a response back, but it's not authoritative for some reason.") + } + if !*quiet { + fmt.Println("Addresses:") + fmt.Printf("%+v\n", addresses) + } +} + +func tryAddressesByNetwork(networkLabel string, id string, api gophercloud.CloudServersProvider) { + log("Getting list of addresses on", networkLabel, "network...") + network, err := api.ListAddressesByNetwork(id, networkLabel) + if (err != nil) && (err != gophercloud.WarnUnauthoritative) { + panic(err) + } + if err == gophercloud.WarnUnauthoritative { + log("Uh oh -- got a response back, but it's not authoritative for some reason.") + } + for _, addr := range network[networkLabel] { + log("Address:", addr.Addr, " IPv", addr.Version) + } +} + +func log(s ...interface{}) { + if !*quiet { + fmt.Println(s...) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/15-list-keypairs.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/15-list-keypairs.go new file mode 100644 index 0000000000..1a617edd20 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/15-list-keypairs.go @@ -0,0 +1,32 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" +) + +var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing. $? non-zero on error though.") +var rgn = flag.String("r", "", "Datacenter region to interrogate. Leave blank for provider-default region.") + +func main() { + flag.Parse() + + withIdentity(false, func(auth gophercloud.AccessProvider) { + withServerApi(auth, func(servers gophercloud.CloudServersProvider) { + keypairs, err := servers.ListKeyPairs() + if err != nil { + panic(err) + } + + if !*quiet { + fmt.Println("name,fingerprint,publickey") + for _, key := range keypairs { + fmt.Printf("%s,%s,%s\n", key.Name, key.FingerPrint, key.PublicKey) + } + } + }) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/16-create-delete-keypair.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/16-create-delete-keypair.go new file mode 100644 index 0000000000..f59e51c50f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/16-create-delete-keypair.go @@ -0,0 +1,45 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" +) + +var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing. $? non-zero on error though.") +var rgn = flag.String("r", "", "Datacenter region to interrogate. Leave blank for provider-default region.") + +func main() { + flag.Parse() + + withIdentity(false, func(auth gophercloud.AccessProvider) { + withServerApi(auth, func(servers gophercloud.CloudServersProvider) { + name := randomString("ACPTTEST", 16) + kp := gophercloud.NewKeyPair{ + Name: name, + } + keypair, err := servers.CreateKeyPair(kp) + if err != nil { + panic(err) + } + if !*quiet { + fmt.Printf("%s,%s,%s\n", keypair.Name, keypair.FingerPrint, keypair.PublicKey) + } + + keypair, err = servers.ShowKeyPair(name) + if err != nil { + panic(err) + } + if !*quiet { + fmt.Printf("%s,%s,%s\n", keypair.Name, keypair.FingerPrint, keypair.PublicKey) + } + + err = servers.DeleteKeyPair(name) + if err != nil { + panic(err) + } + }) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/17-create-delete-image.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/17-create-delete-image.go new file mode 100644 index 0000000000..b3d80a37e2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/17-create-delete-image.go @@ -0,0 +1,52 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" +) + +var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing. $? non-zero on error though.") +var rgn = flag.String("r", "", "Datacenter region to interrogate. Leave blank for provider-default region.") + +func main() { + flag.Parse() + + withIdentity(false, func(auth gophercloud.AccessProvider) { + withServerApi(auth, func(servers gophercloud.CloudServersProvider) { + log("Creating server") + serverId, err := createServer(servers, "", "", "", "") + if err != nil { + panic(err) + } + waitForServerState(servers, serverId, "ACTIVE") + + log("Creating image") + name := randomString("ACPTTEST", 16) + createImage := gophercloud.CreateImage{ + Name: name, + } + imageId, err := servers.CreateImage(serverId, createImage) + if err != nil { + panic(err) + } + waitForImageState(servers, imageId, "ACTIVE") + + log("Deleting server") + servers.DeleteServerById(serverId) + + log("Deleting image") + servers.DeleteImageById(imageId) + + log("Done") + }) + }) +} + +func log(s string) { + if !*quiet { + fmt.Println(s) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/18-osutil-authentication.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/18-osutil-authentication.go new file mode 100644 index 0000000000..01ff4e9855 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/18-osutil-authentication.go @@ -0,0 +1,19 @@ +// +build acceptance,old + +package main + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/osutil" +) + +func main() { + provider, authOptions, err := osutil.AuthOptions() + if err != nil { + panic(err) + } + _, err = gophercloud.Authenticate(provider, authOptions) + if err != nil { + panic(err) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/19-list-addresses-0.1.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/19-list-addresses-0.1.go new file mode 100644 index 0000000000..d60557b031 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/19-list-addresses-0.1.go @@ -0,0 +1,58 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" +) + +var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing. $? still indicates errors though.") + +func main() { + flag.Parse() + withIdentity(false, func(acc gophercloud.AccessProvider) { + withServerApi(acc, func(api gophercloud.CloudServersProvider) { + log("Creating server") + id, err := createServer(api, "", "", "", "") + if err != nil { + panic(err) + } + waitForServerState(api, id, "ACTIVE") + defer api.DeleteServerById(id) + + tryAllAddresses(id, api) + + log("Done") + }) + }) +} + +func tryAllAddresses(id string, api gophercloud.CloudServersProvider) { + log("Getting the server instance") + s, err := api.ServerById(id) + if err != nil { + panic(err) + } + + log("Getting the complete set of pools") + ps, err := s.AllAddressPools() + if err != nil { + panic(err) + } + + log("Listing IPs for each pool") + for k, v := range ps { + log(fmt.Sprintf(" Pool %s", k)) + for _, a := range v { + log(fmt.Sprintf(" IP: %s, Version: %d", a.Addr, a.Version)) + } + } +} + +func log(s ...interface{}) { + if !*quiet { + fmt.Println(s...) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/99-delete-server.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/99-delete-server.go new file mode 100644 index 0000000000..3e38ba4f35 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/99-delete-server.go @@ -0,0 +1,48 @@ +// +build acceptance,old + +package main + +import ( + "flag" + "fmt" + "github.com/rackspace/gophercloud" +) + +var quiet = flag.Bool("quiet", false, "Quiet operation for acceptance tests. $? non-zero if problem.") +var region = flag.String("r", "", "Datacenter region. Leave blank for provider-default region.") + +func main() { + flag.Parse() + + withIdentity(false, func(auth gophercloud.AccessProvider) { + withServerApi(auth, func(servers gophercloud.CloudServersProvider) { + // Grab a listing of all servers. + ss, err := servers.ListServers() + if err != nil { + panic(err) + } + + // And for each one that starts with the ACPTTEST prefix, delete it. + // These are likely left-overs from previously running acceptance tests. + // Note that 04-create-servers.go is intended to leak servers by intention, + // so as to test this code. :) + n := 0 + for _, s := range ss { + if len(s.Name) < 8 { + continue + } + if s.Name[0:8] == "ACPTTEST" { + err := servers.DeleteServerById(s.Id) + if err != nil { + panic(err) + } + n++ + } + } + + if !*quiet { + fmt.Printf("%d servers removed.\n", n) + } + }) + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/libargs.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/libargs.go new file mode 100644 index 0000000000..cf234e7e8a --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/acceptance/libargs.go @@ -0,0 +1,239 @@ +// +build acceptance,old + +package main + +import ( + "crypto/rand" + "fmt" + "github.com/rackspace/gophercloud" + "os" + "strings" + "time" +) + +// getCredentials will verify existence of needed credential information +// provided through environment variables. This function will not return +// if at least one piece of required information is missing. +func getCredentials() (provider, username, password, apiKey string) { + provider = os.Getenv("SDK_PROVIDER") + username = os.Getenv("SDK_USERNAME") + password = os.Getenv("SDK_PASSWORD") + apiKey = os.Getenv("SDK_API_KEY") + var authURL = os.Getenv("OS_AUTH_URL") + + if (provider == "") || (username == "") || (password == "") { + fmt.Fprintf(os.Stderr, "One or more of the following environment variables aren't set:\n") + fmt.Fprintf(os.Stderr, " SDK_PROVIDER=\"%s\"\n", provider) + fmt.Fprintf(os.Stderr, " SDK_USERNAME=\"%s\"\n", username) + fmt.Fprintf(os.Stderr, " SDK_PASSWORD=\"%s\"\n", password) + os.Exit(1) + } + + if strings.Contains(provider, "rackspace") && (authURL != "") { + provider = authURL + "/v2.0/tokens" + } + + return +} + +// randomString generates a string of given length, but random content. +// All content will be within the ASCII graphic character set. +// (Implementation from Even Shaw's contribution on +// http://stackoverflow.com/questions/12771930/what-is-the-fastest-way-to-generate-a-long-random-string-in-go). +func randomString(prefix string, n int) string { + const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + var bytes = make([]byte, n) + rand.Read(bytes) + for i, b := range bytes { + bytes[i] = alphanum[b%byte(len(alphanum))] + } + return prefix + string(bytes) +} + +// aSuitableImage finds a minimal image for use in dynamically creating servers. +// If none can be found, this function will panic. +func aSuitableImage(api gophercloud.CloudServersProvider) string { + images, err := api.ListImages() + if err != nil { + panic(err) + } + + // TODO(sfalvo): + // Works for Rackspace, might not work for your provider! + // Need to figure out why ListImages() provides 0 values for + // Ram and Disk fields. + // + // Until then, just return Ubuntu 12.04 LTS. + for i := 0; i < len(images); i++ { + if strings.Contains(images[i].Name, "Ubuntu 12.04 LTS") { + return images[i].Id + } + } + panic("Image for Ubuntu 12.04 LTS not found.") +} + +// aSuitableFlavor finds the minimum flavor capable of running the test image +// chosen by aSuitableImage. If none can be found, this function will panic. +func aSuitableFlavor(api gophercloud.CloudServersProvider) string { + flavors, err := api.ListFlavors() + if err != nil { + panic(err) + } + + // TODO(sfalvo): + // Works for Rackspace, might not work for your provider! + // Need to figure out why ListFlavors() provides 0 values for + // Ram and Disk fields. + // + // Until then, just return Ubuntu 12.04 LTS. + for i := 0; i < len(flavors); i++ { + if flavors[i].Id == "2" { + return flavors[i].Id + } + } + panic("Flavor 2 (512MB 1-core 20GB machine) not found.") +} + +// createServer creates a new server in a manner compatible with acceptance testing. +// In particular, it ensures that the name of the server always starts with "ACPTTEST--", +// which the delete servers acceptance test relies on to identify servers to delete. +// Passing in empty image and flavor references will force the use of reasonable defaults. +// An empty name string will result in a dynamically created name prefixed with "ACPTTEST--". +// A blank admin password will cause a password to be automatically generated; however, +// at present no means of recovering this password exists, as no acceptance tests yet require +// this data. +func createServer(servers gophercloud.CloudServersProvider, imageRef, flavorRef, name, adminPass string) (string, error) { + if imageRef == "" { + imageRef = aSuitableImage(servers) + } + + if flavorRef == "" { + flavorRef = aSuitableFlavor(servers) + } + + if len(name) < 1 { + name = randomString("ACPTTEST", 16) + } + + if (len(name) < 8) || (name[0:8] != "ACPTTEST") { + name = fmt.Sprintf("ACPTTEST--%s", name) + } + + newServer, err := servers.CreateServer(gophercloud.NewServer{ + Name: name, + ImageRef: imageRef, + FlavorRef: flavorRef, + AdminPass: adminPass, + }) + + if err != nil { + return "", err + } + + return newServer.Id, nil +} + +// findAlternativeFlavor locates a flavor to resize a server to. It is guaranteed to be different +// than what aSuitableFlavor() returns. If none could be found, this function will panic. +func findAlternativeFlavor() string { + return "3" // 1GB image, up from 512MB image +} + +// findAlternativeImage locates an image to resize or rebuild a server with. It is guaranteed to be +// different than what aSuitableImage() returns. If none could be found, this function will panic. +func findAlternativeImage() string { + return "c6f9c411-e708-4952-91e5-62ded5ea4d3e" +} + +// withIdentity authenticates the user against the provider's identity service, and provides an +// accessor for additional services. +func withIdentity(ar bool, f func(gophercloud.AccessProvider)) { + _, _, _, apiKey := getCredentials() + if len(apiKey) == 0 { + withPasswordIdentity(ar, f) + } else { + withAPIKeyIdentity(ar, f) + } +} + +func withPasswordIdentity(ar bool, f func(gophercloud.AccessProvider)) { + provider, username, password, _ := getCredentials() + acc, err := gophercloud.Authenticate( + provider, + gophercloud.AuthOptions{ + Username: username, + Password: password, + AllowReauth: ar, + }, + ) + if err != nil { + panic(err) + } + + f(acc) +} + +func withAPIKeyIdentity(ar bool, f func(gophercloud.AccessProvider)) { + provider, username, _, apiKey := getCredentials() + acc, err := gophercloud.Authenticate( + provider, + gophercloud.AuthOptions{ + Username: username, + ApiKey: apiKey, + AllowReauth: ar, + }, + ) + if err != nil { + panic(err) + } + + f(acc) +} + +// withServerApi acquires the cloud servers API. +func withServerApi(acc gophercloud.AccessProvider, f func(gophercloud.CloudServersProvider)) { + api, err := gophercloud.ServersApi(acc, gophercloud.ApiCriteria{ + Name: "cloudServersOpenStack", + VersionId: "2", + UrlChoice: gophercloud.PublicURL, + }) + if err != nil { + panic(err) + } + + f(api) +} + +// waitForServerState polls, every 10 seconds, for a given server to appear in the indicated state. +// This call will block forever if it never appears in the desired state, so if a timeout is required, +// make sure to call this function in a goroutine. +func waitForServerState(api gophercloud.CloudServersProvider, id, state string) error { + for { + s, err := api.ServerById(id) + if err != nil { + return err + } + if s.Status == state { + return nil + } + time.Sleep(10 * time.Second) + } + panic("Impossible") +} + +// waitForImageState polls, every 10 seconds, for a given image to appear in the indicated state. +// This call will block forever if it never appears in the desired state, so if a timeout is required, +// make sure to call this function in a goroutine. +func waitForImageState(api gophercloud.CloudServersProvider, id, state string) error { + for { + s, err := api.ImageById(id) + if err != nil { + return err + } + if s.Status == state { + return nil + } + time.Sleep(10 * time.Second) + } + panic("Impossible") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/api_fetch.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/api_fetch.go new file mode 100644 index 0000000000..196047e8e1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/api_fetch.go @@ -0,0 +1,49 @@ +package gophercloud + +import( + "fmt" + "github.com/mitchellh/mapstructure" +) + +//The default generic openstack api +var OpenstackApi = map[string]interface{}{ + "Type": "compute", + "UrlChoice": PublicURL, +} + +// Api for use with rackspace +var RackspaceApi = map[string]interface{}{ + "Name": "cloudServersOpenStack", + "VersionId": "2", + "UrlChoice": PublicURL, +} + + +//Populates an ApiCriteria struct with the api values +//from one of the api maps +func PopulateApi(variant string) (ApiCriteria, error){ + var Api ApiCriteria + var variantMap map[string]interface{} + + switch variant { + case "": + variantMap = OpenstackApi + + case "openstack": + variantMap = OpenstackApi + + case "rackspace": + variantMap = RackspaceApi + + default: + var err = fmt.Errorf( + "PopulateApi: Unknown variant %# v; legal values: \"openstack\", \"rackspace\"", variant) + return Api, err + } + + err := mapstructure.Decode(variantMap,&Api) + if err != nil{ + return Api,err + } + return Api, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/authenticate.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/authenticate.go new file mode 100644 index 0000000000..ff609aad2f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/authenticate.go @@ -0,0 +1,257 @@ +package gophercloud + +import ( + "fmt" + "github.com/racker/perigee" +) + +// AuthOptions lets anyone calling Authenticate() supply the required access credentials. +// At present, only Identity V2 API support exists; therefore, only Username, Password, +// and optionally, TenantId are provided. If future Identity API versions become available, +// alternative fields unique to those versions may appear here. +type AuthOptions struct { + // Username and Password are required if using Identity V2 API. + // Consult with your provider's control panel to discover your + // account's username and password. + Username, Password string + + // ApiKey used for providers that support Api Key authentication + ApiKey string + + // The TenantId field is optional for the Identity V2 API. + TenantId string + + // The TenantName can be specified instead of the TenantId + TenantName string + + // AllowReauth should be set to true if you grant permission for Gophercloud to cache + // your credentials in memory, and to allow Gophercloud to attempt to re-authenticate + // automatically if/when your token expires. If you set it to false, it will not cache + // these settings, but re-authentication will not be possible. This setting defaults + // to false. + AllowReauth bool +} + +// AuthContainer provides a JSON encoding wrapper for passing credentials to the Identity +// service. You will not work with this structure directly. +type AuthContainer struct { + Auth Auth `json:"auth"` +} + +// Auth provides a JSON encoding wrapper for passing credentials to the Identity +// service. You will not work with this structure directly. +type Auth struct { + PasswordCredentials *PasswordCredentials `json:"passwordCredentials,omitempty"` + ApiKeyCredentials *ApiKeyCredentials `json:"RAX-KSKEY:apiKeyCredentials,omitempty"` + TenantId string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` +} + +// PasswordCredentials provides a JSON encoding wrapper for passing credentials to the Identity +// service. You will not work with this structure directly. +type PasswordCredentials struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type ApiKeyCredentials struct { + Username string `json:"username"` + ApiKey string `json:"apiKey"` +} + +// Access encapsulates the API token and its relevant fields, as well as the +// services catalog that Identity API returns once authenticated. +type Access struct { + Token Token + ServiceCatalog []CatalogEntry + User User + provider Provider `json:"-"` + options AuthOptions `json:"-"` + context *Context `json:"-"` +} + +// Token encapsulates an authentication token and when it expires. It also includes +// tenant information if available. +type Token struct { + Id, Expires string + Tenant Tenant +} + +// Tenant encapsulates tenant authentication information. If, after authentication, +// no tenant information is supplied, both Id and Name will be "". +type Tenant struct { + Id, Name string +} + +// User encapsulates the user credentials, and provides visibility in what +// the user can do through its role assignments. +type User struct { + Id, Name string + XRaxDefaultRegion string `json:"RAX-AUTH:defaultRegion"` + Roles []Role +} + +// Role encapsulates a permission that a user can rely on. +type Role struct { + Description, Id, Name string +} + +// CatalogEntry encapsulates a service catalog record. +type CatalogEntry struct { + Name, Type string + Endpoints []EntryEndpoint +} + +// EntryEndpoint encapsulates how to get to the API of some service. +type EntryEndpoint struct { + Region, TenantId string + PublicURL, InternalURL string + VersionId, VersionInfo, VersionList string +} + +type AuthError struct { + StatusCode int +} + +func (ae *AuthError) Error() string { + switch ae.StatusCode { + case 401: + return "Auth failed. Bad credentials." + + default: + return fmt.Sprintf("Auth failed. Status code is: %s.", ae.StatusCode) + } +} + +// +func getAuthCredentials(options AuthOptions) Auth { + if options.ApiKey == "" { + return Auth{ + PasswordCredentials: &PasswordCredentials{ + Username: options.Username, + Password: options.Password, + }, + TenantId: options.TenantId, + TenantName: options.TenantName, + } + } else { + return Auth{ + ApiKeyCredentials: &ApiKeyCredentials{ + Username: options.Username, + ApiKey: options.ApiKey, + }, + TenantId: options.TenantId, + TenantName: options.TenantName, + } + } +} + +// papersPlease contains the common logic between authentication and re-authentication. +// The name, obviously a joke on the process of authentication, was chosen because +// of how many other entities exist in the program containing the word Auth or Authorization. +// I didn't need another one. +func (c *Context) papersPlease(p Provider, options AuthOptions) (*Access, error) { + var access *Access + access = new(Access) + + if (options.Username == "") || (options.Password == "" && options.ApiKey == "") { + return nil, ErrCredentials + } + + resp, err := perigee.Request("POST", p.AuthEndpoint, perigee.Options{ + CustomClient: c.httpClient, + ReqBody: &AuthContainer{ + Auth: getAuthCredentials(options), + }, + Results: &struct { + Access **Access `json:"access"` + }{ + &access, + }, + }) + + if err == nil { + switch resp.StatusCode { + case 200: + access.options = options + access.provider = p + access.context = c + + default: + err = &AuthError { + StatusCode: resp.StatusCode, + } + } + } + + return access, err +} + +// Authenticate() grants access to the OpenStack-compatible provider API. +// +// Providers are identified through a unique key string. +// See the RegisterProvider() method for more details. +// +// The supplied AuthOptions instance allows the client to specify only those credentials +// relevant for the authentication request. At present, support exists for OpenStack +// Identity V2 API only; support for V3 will become available as soon as documentation for it +// becomes readily available. +// +// For Identity V2 API requirements, you must provide at least the Username and Password +// options. The TenantId field is optional, and defaults to "". +func (c *Context) Authenticate(provider string, options AuthOptions) (*Access, error) { + p, err := c.ProviderByName(provider) + if err != nil { + return nil, err + } + return c.papersPlease(p, options) +} + +// Reauthenticate attempts to reauthenticate using the configured access credentials, if +// allowed. This method takes no action unless your AuthOptions has the AllowReauth flag +// set to true. +func (a *Access) Reauthenticate() error { + var other *Access + var err error + + if a.options.AllowReauth { + other, err = a.context.papersPlease(a.provider, a.options) + if err == nil { + *a = *other + } + } + return err +} + +// See AccessProvider interface definition for details. +func (a *Access) FirstEndpointUrlByCriteria(ac ApiCriteria) string { + ep := FindFirstEndpointByCriteria(a.ServiceCatalog, ac) + urls := []string{ep.PublicURL, ep.InternalURL} + return urls[ac.UrlChoice] +} + +// See AccessProvider interface definition for details. +func (a *Access) AuthToken() string { + return a.Token.Id +} + +// See AccessProvider interface definition for details. +func (a *Access) Revoke(tok string) error { + url := a.provider.AuthEndpoint + "/" + tok + err := perigee.Delete(url, perigee.Options{ + MoreHeaders: map[string]string{ + "X-Auth-Token": a.AuthToken(), + }, + OkCodes: []int{204}, + }) + return err +} + +// See ServiceCatalogerForIdentityV2 interface definition for details. +// Note that the raw slice is returend; be careful not to alter the fields of any members, +// for other components of Gophercloud may depend upon them. +// If this becomes a problem in the future, +// a future revision may return a deep-copy of the service catalog instead. +func (a *Access) V2ServiceCatalog() []CatalogEntry { + return a.ServiceCatalog +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/authenticate_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/authenticate_test.go new file mode 100644 index 0000000000..b05c7800f1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/authenticate_test.go @@ -0,0 +1,264 @@ +package gophercloud + +import ( + "net/http" + "testing" +) + +const SUCCESSFUL_RESPONSE = `{ + "access": { + "serviceCatalog": [{ + "endpoints": [{ + "publicURL": "https://ord.servers.api.rackspacecloud.com/v2/12345", + "region": "ORD", + "tenantId": "12345", + "versionId": "2", + "versionInfo": "https://ord.servers.api.rackspacecloud.com/v2", + "versionList": "https://ord.servers.api.rackspacecloud.com/" + },{ + "publicURL": "https://dfw.servers.api.rackspacecloud.com/v2/12345", + "region": "DFW", + "tenantId": "12345", + "versionId": "2", + "versionInfo": "https://dfw.servers.api.rackspacecloud.com/v2", + "versionList": "https://dfw.servers.api.rackspacecloud.com/" + }], + "name": "cloudServersOpenStack", + "type": "compute" + },{ + "endpoints": [{ + "publicURL": "https://ord.databases.api.rackspacecloud.com/v1.0/12345", + "region": "ORD", + "tenantId": "12345" + }], + "name": "cloudDatabases", + "type": "rax:database" + }], + "token": { + "expires": "2012-04-13T13:15:00.000-05:00", + "id": "aaaaa-bbbbb-ccccc-dddd" + }, + "user": { + "RAX-AUTH:defaultRegion": "DFW", + "id": "161418", + "name": "demoauthor", + "roles": [{ + "description": "User Admin Role.", + "id": "3", + "name": "identity:user-admin" + }] + } + } +} +` + +func TestAuthProvider(t *testing.T) { + tt := newTransport().WithResponse(SUCCESSFUL_RESPONSE) + c := TestContext().UseCustomClient(&http.Client{ + Transport: tt, + }) + + _, err := c.Authenticate("", AuthOptions{}) + if err == nil { + t.Error("Expected error for empty provider string") + return + } + _, err = c.Authenticate("unknown-provider", AuthOptions{Username: "u", Password: "p"}) + if err == nil { + t.Error("Expected error for unknown service provider") + return + } + + err = c.RegisterProvider("provider", Provider{AuthEndpoint: "/"}) + if err != nil { + t.Error(err) + return + } + _, err = c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"}) + if err != nil { + t.Error(err) + return + } + if tt.called != 1 { + t.Error("Expected transport to be called once.") + return + } +} + +func TestTenantIdEncoding(t *testing.T) { + tt := newTransport().WithResponse(SUCCESSFUL_RESPONSE) + c := TestContext(). + UseCustomClient(&http.Client{ + Transport: tt, + }). + WithProvider("provider", Provider{AuthEndpoint: "/"}) + + tt.IgnoreTenantId() + _, err := c.Authenticate("provider", AuthOptions{ + Username: "u", + Password: "p", + }) + if err != nil { + t.Error(err) + return + } + if tt.tenantIdFound { + t.Error("Tenant ID should not have been encoded") + return + } + + tt.ExpectTenantId() + _, err = c.Authenticate("provider", AuthOptions{ + Username: "u", + Password: "p", + TenantId: "t", + }) + if err != nil { + t.Error(err) + return + } + if !tt.tenantIdFound { + t.Error("Tenant ID should have been encoded") + return + } +} + +func TestUserNameAndPassword(t *testing.T) { + c := TestContext(). + WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"}). + UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)}) + + credentials := []AuthOptions{ + {}, + {Username: "u"}, + {Password: "p"}, + } + for i, auth := range credentials { + _, err := c.Authenticate("provider", auth) + if err == nil { + t.Error("Expected error from missing credentials (%d)", i) + return + } + } + + _, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"}) + if err != nil { + t.Error(err) + return + } +} + +func TestUserNameAndApiKey(t *testing.T) { + c := TestContext(). + WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"}). + UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)}) + + credentials := []AuthOptions{ + {}, + {Username: "u"}, + {ApiKey: "a"}, + } + for i, auth := range credentials { + _, err := c.Authenticate("provider", auth) + if err == nil { + t.Error("Expected error from missing credentials (%d)", i) + return + } + } + + _, err := c.Authenticate("provider", AuthOptions{Username: "u", ApiKey: "a"}) + if err != nil { + t.Error(err) + return + } +} + +func TestTokenAcquisition(t *testing.T) { + c := TestContext(). + UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)}). + WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"}) + + acc, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"}) + if err != nil { + t.Error(err) + return + } + + tok := acc.Token + if (tok.Id == "") || (tok.Expires == "") { + t.Error("Expected a valid token for successful login; got %s, %s", tok.Id, tok.Expires) + return + } +} + +func TestServiceCatalogAcquisition(t *testing.T) { + c := TestContext(). + UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)}). + WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"}) + + acc, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"}) + if err != nil { + t.Error(err) + return + } + + svcs := acc.ServiceCatalog + if len(svcs) < 2 { + t.Error("Expected 2 service catalog entries; got %d", len(svcs)) + return + } + + types := map[string]bool{ + "compute": true, + "rax:database": true, + } + for _, entry := range svcs { + if !types[entry.Type] { + t.Error("Expected to find type %s.", entry.Type) + return + } + } +} + +func TestUserAcquisition(t *testing.T) { + c := TestContext(). + UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)}). + WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"}) + + acc, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"}) + if err != nil { + t.Error(err) + return + } + + u := acc.User + if u.Id != "161418" { + t.Error("Expected user ID of 16148; got", u.Id) + return + } +} + +func TestAuthenticationNeverReauths(t *testing.T) { + tt := newTransport().WithError(401) + c := TestContext(). + UseCustomClient(&http.Client{Transport: tt}). + WithProvider("provider", Provider{AuthEndpoint: "http://localhost"}) + + _, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"}) + if err == nil { + t.Error("Expected an error from a 401 Unauthorized response") + return + } + + rc, _ := ActualResponseCode(err) + if rc != 401 { + t.Error("Expected a 401 error code") + return + } + + err = tt.VerifyCalls(t, 1) + if err != nil { + // Test object already flagged. + return + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/common_types.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/common_types.go new file mode 100644 index 0000000000..044b308dd7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/common_types.go @@ -0,0 +1,24 @@ +package gophercloud + +// Link is used for JSON (un)marshalling. +// It provides RESTful links to a resource. +type Link struct { + Href string `json:"href"` + Rel string `json:"rel"` + Type string `json:"type"` +} + +// FileConfig structures represent a blob of data which must appear at a +// a specific location in a server's filesystem. The file contents are +// base-64 encoded. +type FileConfig struct { + Path string `json:"path"` + Contents string `json:"contents"` +} + +// NetworkConfig structures represent an affinity between a server and a +// specific, uniquely identified network. Networks are identified through +// universally unique IDs. +type NetworkConfig struct { + Uuid string `json:"uuid"` +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/context.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/context.go new file mode 100644 index 0000000000..e753c8b670 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/context.go @@ -0,0 +1,150 @@ +package gophercloud + +import ( + "net/http" + "strings" + "fmt" + "github.com/tonnerre/golang-pretty" +) + +// Provider structures exist for each tangible provider of OpenStack service. +// For example, Rackspace, Hewlett-Packard, and NASA might have their own instance of this structure. +// +// At a minimum, a provider must expose an authentication endpoint. +type Provider struct { + AuthEndpoint string +} + +// ReauthHandlerFunc functions are responsible for somehow performing the task of +// reauthentication. +type ReauthHandlerFunc func(AccessProvider) error + +// Context structures encapsulate Gophercloud-global state in a manner which +// facilitates easier unit testing. As a user of this SDK, you'll never +// have to use this structure, except when contributing new code to the SDK. +type Context struct { + // providerMap serves as a directory of supported providers. + providerMap map[string]Provider + + // httpClient refers to the current HTTP client interface to use. + httpClient *http.Client + + // reauthHandler provides the functionality needed to re-authenticate + // if that feature is enabled. Note: in order to allow for automatic + // re-authentication, the Context object will need to remember your + // username, password, and tenant ID as provided in the initial call + // to Authenticate(). If you do not desire this, you'll need to handle + // reauthentication yourself through other means. Two methods exist: + // the first approach is to just handle errors yourself at the application + // layer, and the other is through a custom reauthentication handler + // set through the WithReauthHandler() method. + reauthHandler ReauthHandlerFunc +} + +// TestContext yields a new Context instance, pre-initialized with a barren +// state suitable for per-unit-test customization. This configuration consists +// of: +// +// * An empty provider map. +// +// * An HTTP client built by the net/http package (see http://godoc.org/net/http#Client). +func TestContext() *Context { + return &Context{ + providerMap: make(map[string]Provider), + httpClient: &http.Client{}, + reauthHandler: func(acc AccessProvider) error { + return acc.Reauthenticate() + }, + } +} + +// UseCustomClient configures the context to use a customized HTTP client +// instance. By default, TestContext() will return a Context which uses +// the net/http package's default client instance. +func (c *Context) UseCustomClient(hc *http.Client) *Context { + c.httpClient = hc + return c +} + +// RegisterProvider allows a unit test to register a mythical provider convenient for testing. +// If the provider structure lacks adequate configuration, or the configuration given has some +// detectable error, an ErrConfiguration error will result. +func (c *Context) RegisterProvider(name string, p Provider) error { + if p.AuthEndpoint == "" { + return ErrConfiguration + } + + c.providerMap[name] = p + return nil +} + +// WithProvider offers convenience for unit tests. +func (c *Context) WithProvider(name string, p Provider) *Context { + err := c.RegisterProvider(name, p) + if err != nil { + panic(err) + } + return c +} + +// ProviderByName will locate a provider amongst those previously registered, if it exists. +// If the named provider has not been registered, an ErrProvider error will result. +// +// You may also specify a custom Identity API URL. +// Any provider name that contains the characters "://", in that order, will be treated as a custom Identity API URL. +// Custom URLs, important for private cloud deployments, overrides all provider configurations. +func (c *Context) ProviderByName(name string) (p Provider, err error) { + for provider, descriptor := range c.providerMap { + if name == provider { + return descriptor, nil + } + } + if strings.Contains(name, "://") { + p = Provider{ + AuthEndpoint: name, + } + return p, nil + } + return Provider{}, ErrProvider +} + +func getServiceCatalogFromAccessProvider(provider AccessProvider) ([]CatalogEntry) { + access, found := provider.(*Access) + if found { + return access.ServiceCatalog + } else { + return nil + } +} + +// Instantiates a Cloud Servers API for the provider given. +func (c *Context) ServersApi(provider AccessProvider, criteria ApiCriteria) (CloudServersProvider, error) { + url := provider.FirstEndpointUrlByCriteria(criteria) + if url == "" { + var err = fmt.Errorf( + "Missing endpoint, or insufficient privileges to access endpoint; criteria = %# v; serviceCatalog = %# v", + pretty.Formatter(criteria), + pretty.Formatter(getServiceCatalogFromAccessProvider(provider))) + return nil, err + } + + gcp := &genericServersProvider{ + endpoint: url, + context: c, + access: provider, + } + + return gcp, nil +} + +// WithReauthHandler configures the context to handle reauthentication attempts using the supplied +// funtion. By default, reauthentication happens by invoking Authenticate(), which is unlikely to be +// useful in a unit test. +// +// Do not confuse this function with WithReauth()! Although they work together to support reauthentication, +// WithReauth() actually contains the decision-making logic to determine when to perform a reauth, +// while WithReauthHandler() is used to configure what a reauth actually entails. +func (c *Context) WithReauthHandler(f ReauthHandlerFunc) *Context { + c.reauthHandler = f + return c +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/context_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/context_test.go new file mode 100644 index 0000000000..2936526401 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/context_test.go @@ -0,0 +1,28 @@ +package gophercloud + +import ( + "testing" +) + +func TestProviderRegistry(t *testing.T) { + c := TestContext() + + _, err := c.ProviderByName("aProvider") + if err == nil { + t.Error("Expected error when looking for a provider by non-existant name") + return + } + + err = c.RegisterProvider("aProvider", Provider{}) + if err != ErrConfiguration { + t.Error("Unexpected error/nil when registering a provider w/out an auth endpoint\n %s", err) + return + } + + _ = c.RegisterProvider("aProvider", Provider{AuthEndpoint: "http://localhost/auth"}) + _, err = c.ProviderByName("aProvider") + if err != nil { + t.Error(err) + return + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/errors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/errors.go new file mode 100644 index 0000000000..726ba7e97c --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/errors.go @@ -0,0 +1,39 @@ +package gophercloud + +import ( + "fmt" +) + +// ErrNotImplemented should be used only while developing new SDK features. +// No established function or method will ever produce this error. +var ErrNotImplemented = fmt.Errorf("Not implemented") + +// ErrProvider errors occur when attempting to reference an unsupported +// provider. More often than not, this error happens due to a typo in +// the name. +var ErrProvider = fmt.Errorf("Missing or incorrect provider") + +// ErrCredentials errors happen when attempting to authenticate using a +// set of credentials not recognized by the Authenticate() method. +// For example, not providing a username or password when attempting to +// authenticate against an Identity V2 API. +var ErrCredentials = fmt.Errorf("Missing or incomplete credentials") + +// ErrConfiguration errors happen when attempting to add a new provider, and +// the provider added lacks a correct or consistent configuration. +// For example, all providers must expose at least an Identity V2 API +// for authentication; if this endpoint isn't specified, you may receive +// this error when attempting to register it against a context. +var ErrConfiguration = fmt.Errorf("Missing or incomplete configuration") + +// ErrError errors happen when you attempt to discover the response code +// responsible for a previous request bombing with an error, but pass in an +// error interface which doesn't belong to the web client. +var ErrError = fmt.Errorf("Attempt to solicit actual HTTP response code from error entity which doesn't know") + +// WarnUnauthoritative warnings happen when a service believes its response +// to be correct, but is not in a position of knowing for sure at the moment. +// For example, the service could be responding with cached data that has +// exceeded its time-to-live setting, but which has not yet received an official +// update from an authoritative source. +var WarnUnauthoritative = fmt.Errorf("Unauthoritative data") diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/flavors.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/flavors.go new file mode 100644 index 0000000000..eb864d5787 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/flavors.go @@ -0,0 +1,55 @@ +package gophercloud + +import ( + "github.com/racker/perigee" +) + +// See CloudServersProvider interface for details. +func (gsp *genericServersProvider) ListFlavors() ([]Flavor, error) { + var fs []Flavor + + err := gsp.context.WithReauth(gsp.access, func() error { + url := gsp.endpoint + "/flavors/detail" + return perigee.Get(url, perigee.Options{ + CustomClient: gsp.context.httpClient, + Results: &struct{ Flavors *[]Flavor }{&fs}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + }) + }) + return fs, err +} + +// FlavorLink provides a reference to a flavor by either ID or by direct URL. +// Some services use just the ID, others use just the URL. +// This structure provides a common means of expressing both in a single field. +type FlavorLink struct { + Id string `json:"id"` + Links []Link `json:"links"` +} + +// Flavor records represent (virtual) hardware configurations for server resources in a region. +// +// The Id field contains the flavor's unique identifier. +// For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance. +// +// The Disk and Ram fields provide a measure of storage space offered by the flavor, in GB and MB, respectively. +// +// The Name field provides a human-readable moniker for the flavor. +// +// Swap indicates how much space is reserved for swap. +// If not provided, this field will be set to 0. +// +// VCpus indicates how many (virtual) CPUs are available for this flavor. +type Flavor struct { + OsFlvDisabled bool `json:"OS-FLV-DISABLED:disabled"` + Disk int `json:"disk"` + Id string `json:"id"` + Links []Link `json:"links"` + Name string `json:"name"` + Ram int `json:"ram"` + RxTxFactor float64 `json:"rxtx_factor"` + Swap int `json:"swap"` + VCpus int `json:"vcpus"` +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/floating_ips.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/floating_ips.go new file mode 100644 index 0000000000..11636673ea --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/floating_ips.go @@ -0,0 +1,88 @@ +package gophercloud + +import ( + "errors" + "fmt" + "github.com/racker/perigee" +) + +func (gsp *genericServersProvider) ListFloatingIps() ([]FloatingIp, error) { + var fips []FloatingIp + + err := gsp.context.WithReauth(gsp.access, func() error { + url := gsp.endpoint + "/os-floating-ips" + return perigee.Get(url, perigee.Options{ + CustomClient: gsp.context.httpClient, + Results: &struct { + FloatingIps *[]FloatingIp `json:"floating_ips"` + }{&fips}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + }) + }) + return fips, err +} + +func (gsp *genericServersProvider) CreateFloatingIp(pool string) (FloatingIp, error) { + fip := new(FloatingIp) + + err := gsp.context.WithReauth(gsp.access, func() error { + url := gsp.endpoint + "/os-floating-ips" + return perigee.Post(url, perigee.Options{ + CustomClient: gsp.context.httpClient, + ReqBody: map[string]string{ + "pool": pool, + }, + Results: &struct { + FloatingIp **FloatingIp `json:"floating_ip"` + }{&fip}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + }) + }) + + if fip.Ip == "" { + return *fip, errors.New("Error creating floating IP") + } + + return *fip, err +} + +func (gsp *genericServersProvider) AssociateFloatingIp(serverId string, ip FloatingIp) error { + return gsp.context.WithReauth(gsp.access, func() error { + ep := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, serverId) + return perigee.Post(ep, perigee.Options{ + CustomClient: gsp.context.httpClient, + ReqBody: map[string](map[string]string){ + "addFloatingIp": map[string]string{"address": ip.Ip}, + }, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + OkCodes: []int{202}, + }) + }) +} + +func (gsp *genericServersProvider) DeleteFloatingIp(ip FloatingIp) error { + return gsp.context.WithReauth(gsp.access, func() error { + ep := fmt.Sprintf("%s/os-floating-ips/%d", gsp.endpoint, ip.Id) + return perigee.Delete(ep, perigee.Options{ + CustomClient: gsp.context.httpClient, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + OkCodes: []int{202}, + }) + }) +} + +type FloatingIp struct { + Id int `json:"id"` + Pool string `json:"pool"` + Ip string `json:"ip"` + FixedIp string `json:"fixed_ip"` + InstanceId string `json:"instance_id"` +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/global_context.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/global_context.go new file mode 100644 index 0000000000..89d283b1bc --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/global_context.go @@ -0,0 +1,67 @@ +package gophercloud + +import ( + "github.com/racker/perigee" +) + +// globalContext is the, well, "global context." +// Most of this SDK is written in a manner to facilitate easier testing, +// which doesn't require all the configuration a real-world application would require. +// However, for real-world deployments, applications should be able to rely on a consistent configuration of providers, etc. +var globalContext *Context + +// providers is the set of supported providers. +var providers = map[string]Provider{ + "rackspace-us": { + AuthEndpoint: "https://identity.api.rackspacecloud.com/v2.0/tokens", + }, + "rackspace-uk": { + AuthEndpoint: "https://lon.identity.api.rackspacecloud.com/v2.0/tokens", + }, +} + +// Initialize the global context to sane configuration. +// The Go runtime ensures this function is called before main(), +// thus guaranteeing proper configuration before your application ever runs. +func init() { + globalContext = TestContext() + for name, descriptor := range providers { + globalContext.RegisterProvider(name, descriptor) + } +} + +// Authenticate() grants access to the OpenStack-compatible provider API. +// +// Providers are identified through a unique key string. +// Specifying an unsupported provider will result in an ErrProvider error. +// However, you may also specify a custom Identity API URL. +// Any provider name that contains the characters "://", in that order, will be treated as a custom Identity API URL. +// Custom URLs, important for private cloud deployments, overrides all provider configurations. +// +// The supplied AuthOptions instance allows the client to specify only those credentials +// relevant for the authentication request. At present, support exists for OpenStack +// Identity V2 API only; support for V3 will become available as soon as documentation for it +// becomes readily available. +// +// For Identity V2 API requirements, you must provide at least the Username and Password +// options. The TenantId field is optional, and defaults to "". +func Authenticate(provider string, options AuthOptions) (*Access, error) { + return globalContext.Authenticate(provider, options) +} + +// Instantiates a Cloud Servers object for the provider given. +func ServersApi(acc AccessProvider, criteria ApiCriteria) (CloudServersProvider, error) { + return globalContext.ServersApi(acc, criteria) +} + +// ActualResponseCode inspects a returned error, and discovers the actual response actual +// response code that caused the error to be raised. +func ActualResponseCode(e error) (int, error) { + if err, typeOk := e.(*perigee.UnexpectedResponseCodeError); typeOk { + return err.Actual, nil + } else if err, typeOk := e.(*AuthError); typeOk{ + return err.StatusCode, nil + } + + return 0, ErrError +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/images.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/images.go new file mode 100644 index 0000000000..a23e0bbb67 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/images.go @@ -0,0 +1,106 @@ +package gophercloud + +import ( + "github.com/racker/perigee" +) + +// See the CloudImagesProvider interface for details. +func (gsp *genericServersProvider) ListImages() ([]Image, error) { + var is []Image + + err := gsp.context.WithReauth(gsp.access, func() error { + url := gsp.endpoint + "/images/detail" + return perigee.Get(url, perigee.Options{ + CustomClient: gsp.context.httpClient, + Results: &struct{ Images *[]Image }{&is}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + }) + }) + return is, err +} + +func (gsp *genericServersProvider) ImageById(id string) (*Image, error) { + var is *Image + + err := gsp.context.WithReauth(gsp.access, func() error { + url := gsp.endpoint + "/images/" + id + return perigee.Get(url, perigee.Options{ + CustomClient: gsp.context.httpClient, + Results: &struct{ Image **Image }{&is}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + }) + }) + return is, err +} + +func (gsp *genericServersProvider) DeleteImageById(id string) error { + err := gsp.context.WithReauth(gsp.access, func() error { + url := gsp.endpoint + "/images/" + id + _, err := perigee.Request("DELETE", url, perigee.Options{ + CustomClient: gsp.context.httpClient, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + }) + return err + }) + return err +} + +// ImageLink provides a reference to a image by either ID or by direct URL. +// Some services use just the ID, others use just the URL. +// This structure provides a common means of expressing both in a single field. +type ImageLink struct { + Id string `json:"id"` + Links []Link `json:"links"` +} + +// Image is used for JSON (un)marshalling. +// It provides a description of an OS image. +// +// The Id field contains the image's unique identifier. +// For example, this identifier will be useful for specifying which operating system to install on a new server instance. +// +// The MinDisk and MinRam fields specify the minimum resources a server must provide to be able to install the image. +// +// The Name field provides a human-readable moniker for the OS image. +// +// The Progress and Status fields indicate image-creation status. +// Any usable image will have 100% progress. +// +// The Updated field indicates the last time this image was changed. +// +// OsDcfDiskConfig indicates the server's boot volume configuration. +// Valid values are: +// AUTO +// ---- +// The server is built with a single partition the size of the target flavor disk. +// The file system is automatically adjusted to fit the entire partition. +// This keeps things simple and automated. +// AUTO is valid only for images and servers with a single partition that use the EXT3 file system. +// This is the default setting for applicable Rackspace base images. +// +// MANUAL +// ------ +// The server is built using whatever partition scheme and file system is in the source image. +// If the target flavor disk is larger, +// the remaining disk space is left unpartitioned. +// This enables images to have non-EXT3 file systems, multiple partitions, and so on, +// and enables you to manage the disk configuration. +// +type Image struct { + Created string `json:"created"` + Id string `json:"id"` + Links []Link `json:"links"` + MinDisk int `json:"minDisk"` + MinRam int `json:"minRam"` + Name string `json:"name"` + Progress int `json:"progress"` + Status string `json:"status"` + Updated string `json:"updated"` + OsDcfDiskConfig string `json:"OS-DCF:diskConfig"` +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/interfaces.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/interfaces.go new file mode 100644 index 0000000000..4c7dbee422 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/interfaces.go @@ -0,0 +1,247 @@ +package gophercloud + +import "net/url" + +// AccessProvider instances encapsulate a Keystone authentication interface. +type AccessProvider interface { + // FirstEndpointUrlByCriteria searches through the service catalog for the first + // matching entry endpoint fulfilling the provided criteria. If nothing found, + // return "". Otherwise, return either the public or internal URL for the + // endpoint, depending on both its existence and the setting of the ApiCriteria.UrlChoice + // field. + FirstEndpointUrlByCriteria(ApiCriteria) string + + // AuthToken provides a copy of the current authentication token for the user's credentials. + // Note that AuthToken() will not automatically refresh an expired token. + AuthToken() string + + // Revoke allows you to terminate any program's access to the OpenStack API by token ID. + Revoke(string) error + + // Reauthenticate attempts to acquire a new authentication token, if the feature is enabled by + // AuthOptions.AllowReauth. + Reauthenticate() error +} + +// ServiceCatalogerIdentityV2 interface provides direct access to the service catalog as offered by the Identity V2 API. +// We regret we need to fracture the namespace of what should otherwise be a simple concept; however, +// the OpenStack community saw fit to render V3's service catalog completely incompatible with V2. +type ServiceCatalogerForIdentityV2 interface { + V2ServiceCatalog() []CatalogEntry +} + +// CloudServersProvider instances encapsulate a Cloud Servers API, should one exist in the service catalog +// for your provider. +type CloudServersProvider interface { + // Servers + + // ListServers provides a complete list of servers hosted by the user + // in a given region. This function differs from ListServersLinksOnly() + // in that it returns all available details for each server returned. + ListServers() ([]Server, error) + + // ListServersByFilters provides a list of servers hosted by the user in a + // given region. This function let you requests servers by certain URI + // paramaters defined by the API endpoint. This is sometimes more suitable + // if you have many servers and you only want to pick servers on certain + // criterias. An example usage could be : + // + // filter := url.Values{} + // filter.Set("name", "MyServer") + // filter.Set("status", "ACTIVE") + // + // filteredServers, err := c.ListServersByFilters(filter) + // + // Here, filteredServers only contains servers whose name started with + // "MyServer" and are in "ACTIVE" status. + ListServersByFilter(filter url.Values) ([]Server, error) + + // ListServers provides a complete list of servers hosted by the user + // in a given region. This function differs from ListServers() in that + // it returns only IDs and links to each server returned. + // + // This function should be used only under certain circumstances. + // It's most useful for checking to see if a server with a given ID exists, + // or that you have permission to work with that server. It's also useful + // when the cost of retrieving the server link list plus the overhead of manually + // invoking ServerById() for each of the servers you're interested in is less than + // just calling ListServers() to begin with. This may be a consideration, for + // example, with mobile applications. + // + // In other cases, you probably should just call ListServers() and cache the + // results to conserve overall bandwidth and reduce your access rate on the API. + ListServersLinksOnly() ([]Server, error) + + // ServerById will retrieve a detailed server description given the unique ID + // of a server. The ID can be returned by either ListServers() or by ListServersLinksOnly(). + ServerById(id string) (*Server, error) + + // CreateServer requests a new server to be created by the cloud server provider. + // The user must pass in a pointer to an initialized NewServerContainer structure. + // Please refer to the NewServerContainer documentation for more details. + // + // If the NewServer structure's AdminPass is empty (""), a password will be + // automatically generated by your OpenStack provider, and returned through the + // AdminPass field of the result. Take care, however; this will be the only time + // this happens. No other means exists in the public API to acquire a password + // for a pre-existing server. If you lose it, you'll need to call SetAdminPassword() + // to set a new one. + CreateServer(ns NewServer) (*NewServer, error) + + // DeleteServerById requests that the server with the assigned ID be removed + // from your account. The delete happens asynchronously. + DeleteServerById(id string) error + + // SetAdminPassword requests that the server with the specified ID have its + // administrative password changed. For Linux, BSD, or other POSIX-like + // system, this password corresponds to the root user. For Windows machines, + // the Administrator password will be affected instead. + SetAdminPassword(id string, pw string) error + + // ResizeServer can be a short-hand for RebuildServer where only the size of the server + // changes. Note that after the resize operation is requested, you will need to confirm + // the resize has completed for changes to take effect permanently. Changes will assume + // to be confirmed even without an explicit confirmation after 24 hours from the initial + // request. + ResizeServer(id, newName, newFlavor, newDiskConfig string) error + + // RevertResize will reject a server's resized configuration, thus + // rolling back to the original server. + RevertResize(id string) error + + // ConfirmResizeServer will acknowledge a server's resized configuration. + ConfirmResize(id string) error + + // RebootServer requests that the server with the specified ID be rebooted. + // Two reboot mechanisms exist. + // + // - Hard. This will physically power-cycle the unit. + // - Soft. This will attempt to use the server's software-based mechanisms to restart + // the machine. E.g., "shutdown -r now" on Linux. + RebootServer(id string, hard bool) error + + // RescueServer requests that the server with the specified ID be placed into + // a state of maintenance. The server instance is replaced with a new instance, + // of the same flavor and image. This new image will have the boot volume of the + // original machine mounted as a secondary device, so that repair and administration + // may occur. Use UnrescueServer() to restore the server to its previous state. + // Note also that many providers will impose a time limit for how long a server may + // exist in rescue mode! Consult the API documentation for your provider for + // details. + RescueServer(id string) (string, error) + + // UnrescueServer requests that a server in rescue state be placed into its nominal + // operating state. + UnrescueServer(id string) error + + // UpdateServer alters one or more fields of the identified server's Server record. + // However, not all fields may be altered. Presently, only Name, AccessIPv4, and + // AccessIPv6 fields may be altered. If unspecified, or set to an empty or zero + // value, the corresponding field remains unaltered. + // + // This function returns the new set of server details if successful. + UpdateServer(id string, newValues NewServerSettings) (*Server, error) + + // RebuildServer reprovisions a server to the specifications given by the + // NewServer structure. The following fields are guaranteed to be recognized: + // + // Name (required) AccessIPv4 + // imageRef (required) AccessIPv6 + // AdminPass (required) Metadata + // Personality + // + // Other providers may reserve the right to act on additional fields. + RebuildServer(id string, ns NewServer) (*Server, error) + + // CreateImage will create a new image from the specified server id returning the id of the new image. + CreateImage(id string, ci CreateImage) (string, error) + + // Addresses + + // ListAddresses yields the list of available addresses for the server. + // This information is also returned by ServerById() in the Server.Addresses + // field. However, if you have a lot of servers and all you need are addresses, + // this function might be more efficient. + ListAddresses(id string) (AddressSet, error) + + // ListAddressesByNetwork yields the list of available addresses for a given server id and networkLabel. + // Example: ListAddressesByNetwork("234-4353-4jfrj-43j2s", "private") + ListAddressesByNetwork(id, networkLabel string) (NetworkAddress, error) + + // ListFloatingIps yields the list of all floating IP addresses allocated to the current project. + ListFloatingIps() ([]FloatingIp, error) + + // CreateFloatingIp allocates a new IP from the named pool to the current project. + CreateFloatingIp(pool string) (FloatingIp, error) + + // DeleteFloatingIp returns the specified IP from the current project to the pool. + DeleteFloatingIp(ip FloatingIp) error + + // AssociateFloatingIp associates the given floating IP to the given server id. + AssociateFloatingIp(serverId string, ip FloatingIp) error + + // Images + + // ListImages yields the list of available operating system images. This function + // returns full details for each image, if available. + ListImages() ([]Image, error) + + // ImageById yields details about a specific image. + ImageById(id string) (*Image, error) + + // DeleteImageById will delete the specific image. + DeleteImageById(id string) error + + // Flavors + + // ListFlavors yields the list of available system flavors. This function + // returns full details for each flavor, if available. + ListFlavors() ([]Flavor, error) + + // KeyPairs + + // ListKeyPairs yields the list of available keypairs. + ListKeyPairs() ([]KeyPair, error) + + // CreateKeyPairs will create or generate a new keypair. + CreateKeyPair(nkp NewKeyPair) (KeyPair, error) + + // DeleteKeyPair wil delete a keypair. + DeleteKeyPair(name string) error + + // ShowKeyPair will yield the named keypair. + ShowKeyPair(name string) (KeyPair, error) + + // ListSecurityGroups provides a listing of security groups for the tenant. + // This method works only if the provider supports the os-security-groups extension. + ListSecurityGroups() ([]SecurityGroup, error) + + // CreateSecurityGroup lets a tenant create a new security group. + // Only the SecurityGroup fields which are specified will be marshalled to the API. + // This method works only if the provider supports the os-security-groups extension. + CreateSecurityGroup(desired SecurityGroup) (*SecurityGroup, error) + + // ListSecurityGroupsByServerId provides a list of security groups which apply to the indicated server. + // This method works only if the provider supports the os-security-groups extension. + ListSecurityGroupsByServerId(id string) ([]SecurityGroup, error) + + // SecurityGroupById returns a security group corresponding to the provided ID number. + // This method works only if the provider supports the os-security-groups extension. + SecurityGroupById(id int) (*SecurityGroup, error) + + // DeleteSecurityGroupById disposes of a security group corresponding to the provided ID number. + // This method works only if the provider supports the os-security-groups extension. + DeleteSecurityGroupById(id int) error + + // ListDefaultSGRules lists default security group rules. + // This method only works if the provider supports the os-security-groups-default-rules extension. + ListDefaultSGRules() ([]SGRule, error) + + // CreateDefaultSGRule creates a default security group rule. + // This method only works if the provider supports the os-security-groups-default-rules extension. + CreateDefaultSGRule(SGRule) (*SGRule, error) + + // GetSGRule obtains information for a specified security group rule. + // This method only works if the provider supports the os-security-groups-default-rules extension. + GetSGRule(string) (*SGRule, error) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/keypairs.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/keypairs.go new file mode 100644 index 0000000000..8ae8cd3939 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/keypairs.go @@ -0,0 +1,98 @@ +package gophercloud + +import ( + "github.com/racker/perigee" +) + +// See the CloudImagesProvider interface for details. +func (gsp *genericServersProvider) ListKeyPairs() ([]KeyPair, error) { + type KeyPairs struct { + KeyPairs []struct { + KeyPair KeyPair `json:"keypair"` + } `json:"keypairs"` + } + + var kp KeyPairs + + err := gsp.context.WithReauth(gsp.access, func() error { + url := gsp.endpoint + "/os-keypairs" + return perigee.Get(url, perigee.Options{ + CustomClient: gsp.context.httpClient, + Results: &kp, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + }) + }) + + // Flatten out the list of keypairs + var keypairs []KeyPair + for _, k := range kp.KeyPairs { + keypairs = append(keypairs, k.KeyPair) + } + return keypairs, err +} + +func (gsp *genericServersProvider) CreateKeyPair(nkp NewKeyPair) (KeyPair, error) { + var kp KeyPair + + err := gsp.context.WithReauth(gsp.access, func() error { + url := gsp.endpoint + "/os-keypairs" + return perigee.Post(url, perigee.Options{ + ReqBody: &struct { + KeyPair *NewKeyPair `json:"keypair"` + }{&nkp}, + CustomClient: gsp.context.httpClient, + Results: &struct{ KeyPair *KeyPair }{&kp}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + OkCodes: []int{200}, + }) + }) + return kp, err +} + +// See the CloudImagesProvider interface for details. +func (gsp *genericServersProvider) DeleteKeyPair(name string) error { + err := gsp.context.WithReauth(gsp.access, func() error { + url := gsp.endpoint + "/os-keypairs/" + name + return perigee.Delete(url, perigee.Options{ + CustomClient: gsp.context.httpClient, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + OkCodes: []int{202}, + }) + }) + return err +} + +func (gsp *genericServersProvider) ShowKeyPair(name string) (KeyPair, error) { + var kp KeyPair + + err := gsp.context.WithReauth(gsp.access, func() error { + url := gsp.endpoint + "/os-keypairs/" + name + return perigee.Get(url, perigee.Options{ + CustomClient: gsp.context.httpClient, + Results: &struct{ KeyPair *KeyPair }{&kp}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + }) + }) + return kp, err +} + +type KeyPair struct { + FingerPrint string `json:"fingerprint"` + Name string `json:"name"` + PrivateKey string `json:"private_key,omitempty"` + PublicKey string `json:"public_key"` + UserID string `json:"user_id,omitempty"` +} + +type NewKeyPair struct { + Name string `json:"name"` + PublicKey string `json:"public_key,omitempty"` +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/osutil/auth.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/osutil/auth.go new file mode 100644 index 0000000000..a411b6330b --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/osutil/auth.go @@ -0,0 +1,64 @@ +package osutil + +import ( + "fmt" + "github.com/rackspace/gophercloud" + "os" + "strings" +) + +var ( + nilOptions = gophercloud.AuthOptions{} + + // ErrNoAuthUrl errors occur when the value of the OS_AUTH_URL environment variable cannot be determined. + ErrNoAuthUrl = fmt.Errorf("Environment variable OS_AUTH_URL needs to be set.") + + // ErrNoUsername errors occur when the value of the OS_USERNAME environment variable cannot be determined. + ErrNoUsername = fmt.Errorf("Environment variable OS_USERNAME needs to be set.") + + // ErrNoPassword errors occur when the value of the OS_PASSWORD environment variable cannot be determined. + ErrNoPassword = fmt.Errorf("Environment variable OS_PASSWORD or OS_API_KEY needs to be set.") +) + +// AuthOptions fills out a gophercloud.AuthOptions structure with the settings found on the various OpenStack +// OS_* environment variables. The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME, +// OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME. Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must +// have settings, or an error will result. OS_TENANT_ID and OS_TENANT_NAME are optional. +// +// The value of OS_AUTH_URL will be returned directly to the caller, for subsequent use in +// gophercloud.Authenticate()'s Provider parameter. This function will not interpret the value of OS_AUTH_URL, +// so as a convenient extention, you may set OS_AUTH_URL to, e.g., "rackspace-uk", or any other Gophercloud-recognized +// provider shortcuts. For broad compatibility, especially with local installations, you should probably +// avoid the temptation to do this. +func AuthOptions() (string, gophercloud.AuthOptions, error) { + provider := os.Getenv("OS_AUTH_URL") + username := os.Getenv("OS_USERNAME") + password := os.Getenv("OS_PASSWORD") + tenantId := os.Getenv("OS_TENANT_ID") + tenantName := os.Getenv("OS_TENANT_NAME") + + if provider == "" { + return "", nilOptions, ErrNoAuthUrl + } + + if username == "" { + return "", nilOptions, ErrNoUsername + } + + if password == "" { + return "", nilOptions, ErrNoPassword + } + + ao := gophercloud.AuthOptions{ + Username: username, + Password: password, + TenantId: tenantId, + TenantName: tenantName, + } + + if !strings.HasSuffix(provider, "/tokens") { + provider += "/tokens" + } + + return provider, ao, nil +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/osutil/region.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/osutil/region.go new file mode 100644 index 0000000000..f7df507e55 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/osutil/region.go @@ -0,0 +1,9 @@ +package osutil + +import "os" + +// Region provides a means of querying the OS_REGION_NAME environment variable. +// At present, you may also use os.Getenv("OS_REGION_NAME") as well. +func Region() string { + return os.Getenv("OS_REGION_NAME") +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/package.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/package.go new file mode 100644 index 0000000000..396e523445 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/package.go @@ -0,0 +1,7 @@ +// Gophercloud provides a multi-vendor interface to OpenStack-compatible clouds which attempts to follow +// established Go community coding standards and social norms. +// +// Unless you intend on contributing code to the SDK, you will almost certainly never have to use any +// Context structures or any of its methods. Contextual methods exist for easier unit testing only. +// Stick with the global functions unless you know exactly what you're doing, and why. +package gophercloud diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/reauth.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/reauth.go new file mode 100644 index 0000000000..342aca4610 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/reauth.go @@ -0,0 +1,36 @@ +package gophercloud + +import ( + "github.com/racker/perigee" +) + +// WithReauth wraps a Perigee request fragment with logic to perform re-authentication +// if it's deemed necessary. +// +// Do not confuse this function with WithReauth()! Although they work together to support reauthentication, +// WithReauth() actually contains the decision-making logic to determine when to perform a reauth, +// while WithReauthHandler() is used to configure what a reauth actually entails. +func (c *Context) WithReauth(ap AccessProvider, f func() error) error { + err := f() + cause, ok := err.(*perigee.UnexpectedResponseCodeError) + if ok && cause.Actual == 401 { + err = c.reauthHandler(ap) + if err == nil { + err = f() + } + } + return err +} + +// This is like WithReauth above but returns a perigee Response object +func (c *Context) ResponseWithReauth(ap AccessProvider, f func() (*perigee.Response, error)) (*perigee.Response, error) { + response, err := f() + cause, ok := err.(*perigee.UnexpectedResponseCodeError) + if ok && cause.Actual == 401 { + err = c.reauthHandler(ap) + if err == nil { + response, err = f() + } + } + return response, err +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/reauth_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/reauth_test.go new file mode 100644 index 0000000000..e3501b87fb --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/reauth_test.go @@ -0,0 +1,133 @@ +package gophercloud + +import ( + "github.com/racker/perigee" + "testing" +) + +// This reauth-handler does nothing, and returns no error. +func doNothing(_ AccessProvider) error { + return nil +} + +func TestOtherErrorsPropegate(t *testing.T) { + calls := 0 + c := TestContext().WithReauthHandler(doNothing) + + err := c.WithReauth(nil, func() error { + calls++ + return &perigee.UnexpectedResponseCodeError{ + Expected: []int{204}, + Actual: 404, + } + }) + + if err == nil { + t.Error("Expected MyError to be returned; got nil instead.") + return + } + if _, ok := err.(*perigee.UnexpectedResponseCodeError); !ok { + t.Error("Expected UnexpectedResponseCodeError; got %#v", err) + return + } + if calls != 1 { + t.Errorf("Expected the body to be invoked once; found %d calls instead", calls) + return + } +} + +func Test401ErrorCausesBodyInvokation2ndTime(t *testing.T) { + calls := 0 + c := TestContext().WithReauthHandler(doNothing) + + err := c.WithReauth(nil, func() error { + calls++ + return &perigee.UnexpectedResponseCodeError{ + Expected: []int{204}, + Actual: 401, + } + }) + + if err == nil { + t.Error("Expected MyError to be returned; got nil instead.") + return + } + if calls != 2 { + t.Errorf("Expected the body to be invoked once; found %d calls instead", calls) + return + } +} + +func TestReauthAttemptShouldHappen(t *testing.T) { + calls := 0 + c := TestContext().WithReauthHandler(func(_ AccessProvider) error { + calls++ + return nil + }) + c.WithReauth(nil, func() error { + return &perigee.UnexpectedResponseCodeError{ + Expected: []int{204}, + Actual: 401, + } + }) + + if calls != 1 { + t.Errorf("Expected Reauthenticator to be called once; found %d instead", calls) + return + } +} + +type MyError struct{} + +func (*MyError) Error() string { + return "MyError instance" +} + +func TestReauthErrorShouldPropegate(t *testing.T) { + c := TestContext().WithReauthHandler(func(_ AccessProvider) error { + return &MyError{} + }) + + err := c.WithReauth(nil, func() error { + return &perigee.UnexpectedResponseCodeError{ + Expected: []int{204}, + Actual: 401, + } + }) + + if _, ok := err.(*MyError); !ok { + t.Errorf("Expected a MyError; got %#v", err) + return + } +} + +type MyAccess struct{} + +func (my *MyAccess) FirstEndpointUrlByCriteria(ApiCriteria) string { + return "" +} +func (my *MyAccess) AuthToken() string { + return "" +} +func (my *MyAccess) Revoke(string) error { + return nil +} +func (my *MyAccess) Reauthenticate() error { + return nil +} + +func TestReauthHandlerUsesSameAccessProvider(t *testing.T) { + fakeAccess := &MyAccess{} + c := TestContext().WithReauthHandler(func(acc AccessProvider) error { + if acc != fakeAccess { + t.Errorf("Expected acc = fakeAccess") + } + return nil + }) + c.WithReauth(fakeAccess, func() error { + return &perigee.UnexpectedResponseCodeError{ + Expected: []int{204}, + Actual: 401, + } + }) +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/scripts/create-environment.sh b/Godeps/_workspace/src/github.com/rackspace/gophercloud/scripts/create-environment.sh new file mode 100644 index 0000000000..6bae6e8f14 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/scripts/create-environment.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# +# This script helps new contributors set up their local workstation for +# gophercloud development and contributions. + +# Create the environment +export GOPATH=$HOME/go/gophercloud +mkdir -p $GOPATH + +# Download gophercloud into that environment +go get github.com/rackspace/gophercloud +cd $GOPATH/src/github.com/rackspace/gophercloud +git checkout master + +# Write out the env.sh convenience file. +cd $GOPATH +cat <env.sh +#!/bin/bash +export GOPATH=$(pwd) +export GOPHERCLOUD=$GOPATH/src/github.com/rackspace/gophercloud +EOF +chmod a+x env.sh + +# Make changes immediately available as a convenience. +. ./env.sh + diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/scripts/test-all.sh b/Godeps/_workspace/src/github.com/rackspace/gophercloud/scripts/test-all.sh new file mode 100644 index 0000000000..096736f259 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/scripts/test-all.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# +# This script is responsible for executing all the acceptance tests found in +# the acceptance/ directory. + +# Find where _this_ script is running from. +SCRIPTS=$(dirname $0) +SCRIPTS=$(cd $SCRIPTS; pwd) + +# Locate the acceptance test / examples directory. +ACCEPTANCE=$(cd $SCRIPTS/../acceptance; pwd) + +# Go workspace path +WS=$(cd $SCRIPTS/..; pwd) + +# In order to run Go code interactively, we need the GOPATH environment +# to be set. +if [ "x$GOPATH" == "x" ]; then + export GOPATH=$WS + echo "WARNING: You didn't have your GOPATH environment variable set." + echo " I'm assuming $GOPATH as its value." +fi + +# Run all acceptance tests sequentially. +# If any test fails, we fail fast. +LIBS=$(ls $ACCEPTANCE/lib*.go) +for T in $(ls -1 $ACCEPTANCE/[0-9][0-9]*.go); do + if ! [ -x $T ]; then + CMD="go run $T $LIBS -quiet" + echo "$CMD ..." + if ! $CMD ; then + echo "- FAILED. Try re-running w/out the -quiet option to see output." + exit 1 + fi + fi +done + diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/servers.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/servers.go new file mode 100644 index 0000000000..1f6a7a4788 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/servers.go @@ -0,0 +1,807 @@ +// TODO(sfalvo): Remove Rackspace-specific Server structure fields and refactor them into a provider-specific access method. +// Be sure to update godocs accordingly. + +package gophercloud + +import ( + "fmt" + "net/url" + "strings" + + "github.com/mitchellh/mapstructure" + "github.com/racker/perigee" +) + +// genericServersProvider structures provide the implementation for generic OpenStack-compatible +// CloudServersProvider interfaces. +type genericServersProvider struct { + // endpoint refers to the provider's API endpoint base URL. This will be used to construct + // and issue queries. + endpoint string + + // Test context (if any) in which to issue requests. + context *Context + + // access associates this API provider with a set of credentials, + // which may be automatically renewed if they near expiration. + access AccessProvider +} + +// See the CloudServersProvider interface for details. +func (gcp *genericServersProvider) ListServersByFilter(filter url.Values) ([]Server, error) { + var ss []Server + + err := gcp.context.WithReauth(gcp.access, func() error { + url := gcp.endpoint + "/servers/detail?" + filter.Encode() + return perigee.Get(url, perigee.Options{ + CustomClient: gcp.context.httpClient, + Results: &struct{ Servers *[]Server }{&ss}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gcp.access.AuthToken(), + }, + }) + }) + return ss, err +} + +// See the CloudServersProvider interface for details. +func (gcp *genericServersProvider) ListServersLinksOnly() ([]Server, error) { + var ss []Server + + err := gcp.context.WithReauth(gcp.access, func() error { + url := gcp.endpoint + "/servers" + return perigee.Get(url, perigee.Options{ + CustomClient: gcp.context.httpClient, + Results: &struct{ Servers *[]Server }{&ss}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gcp.access.AuthToken(), + }, + }) + }) + return ss, err +} + +// See the CloudServersProvider interface for details. +func (gcp *genericServersProvider) ListServers() ([]Server, error) { + var ss []Server + + err := gcp.context.WithReauth(gcp.access, func() error { + url := gcp.endpoint + "/servers/detail" + return perigee.Get(url, perigee.Options{ + CustomClient: gcp.context.httpClient, + Results: &struct{ Servers *[]Server }{&ss}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gcp.access.AuthToken(), + }, + }) + }) + + // Compatibility with v0.0.x -- we "map" our public and private + // addresses into a legacy structure field for the benefit of + // earlier software. + + if err != nil { + return ss, err + } + + for _, s := range ss { + err = mapstructure.Decode(s.RawAddresses, &s.Addresses) + if err != nil { + return ss, err + } + } + + return ss, err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) ServerById(id string) (*Server, error) { + var s *Server + + err := gsp.context.WithReauth(gsp.access, func() error { + url := gsp.endpoint + "/servers/" + id + return perigee.Get(url, perigee.Options{ + Results: &struct{ Server **Server }{&s}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + OkCodes: []int{200}, + }) + }) + + // Compatibility with v0.0.x -- we "map" our public and private + // addresses into a legacy structure field for the benefit of + // earlier software. + + if err != nil { + return s, err + } + + err = mapstructure.Decode(s.RawAddresses, &s.Addresses) + + return s, err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) CreateServer(ns NewServer) (*NewServer, error) { + var s *NewServer + + err := gsp.context.WithReauth(gsp.access, func() error { + ep := gsp.endpoint + "/servers" + return perigee.Post(ep, perigee.Options{ + ReqBody: &struct { + Server *NewServer `json:"server"` + }{&ns}, + Results: &struct{ Server **NewServer }{&s}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + OkCodes: []int{202}, + }) + }) + + return s, err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) DeleteServerById(id string) error { + err := gsp.context.WithReauth(gsp.access, func() error { + url := gsp.endpoint + "/servers/" + id + return perigee.Delete(url, perigee.Options{ + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + OkCodes: []int{204}, + }) + }) + return err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) SetAdminPassword(id, pw string) error { + err := gsp.context.WithReauth(gsp.access, func() error { + url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) + return perigee.Post(url, perigee.Options{ + ReqBody: &struct { + ChangePassword struct { + AdminPass string `json:"adminPass"` + } `json:"changePassword"` + }{ + struct { + AdminPass string `json:"adminPass"` + }{pw}, + }, + OkCodes: []int{202}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + }) + }) + return err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) ResizeServer(id, newName, newFlavor, newDiskConfig string) error { + err := gsp.context.WithReauth(gsp.access, func() error { + url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) + rr := ResizeRequest{ + Name: newName, + FlavorRef: newFlavor, + DiskConfig: newDiskConfig, + } + return perigee.Post(url, perigee.Options{ + ReqBody: &struct { + Resize ResizeRequest `json:"resize"` + }{rr}, + OkCodes: []int{202}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + }) + }) + return err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) RevertResize(id string) error { + err := gsp.context.WithReauth(gsp.access, func() error { + url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) + return perigee.Post(url, perigee.Options{ + ReqBody: &struct { + RevertResize *int `json:"revertResize"` + }{nil}, + OkCodes: []int{202}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + }) + }) + return err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) ConfirmResize(id string) error { + err := gsp.context.WithReauth(gsp.access, func() error { + url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) + return perigee.Post(url, perigee.Options{ + ReqBody: &struct { + ConfirmResize *int `json:"confirmResize"` + }{nil}, + OkCodes: []int{204}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + }) + }) + return err +} + +// See the CloudServersProvider interface for details +func (gsp *genericServersProvider) RebootServer(id string, hard bool) error { + return gsp.context.WithReauth(gsp.access, func() error { + url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) + types := map[bool]string{false: "SOFT", true: "HARD"} + return perigee.Post(url, perigee.Options{ + ReqBody: &struct { + Reboot struct { + Type string `json:"type"` + } `json:"reboot"` + }{ + struct { + Type string `json:"type"` + }{types[hard]}, + }, + OkCodes: []int{202}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + }) + }) +} + +// See the CloudServersProvider interface for details +func (gsp *genericServersProvider) RescueServer(id string) (string, error) { + var pw *string + + err := gsp.context.WithReauth(gsp.access, func() error { + url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) + return perigee.Post(url, perigee.Options{ + ReqBody: &struct { + Rescue string `json:"rescue"` + }{"none"}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + Results: &struct { + AdminPass **string `json:"adminPass"` + }{&pw}, + }) + }) + return *pw, err +} + +// See the CloudServersProvider interface for details +func (gsp *genericServersProvider) UnrescueServer(id string) error { + return gsp.context.WithReauth(gsp.access, func() error { + url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) + return perigee.Post(url, perigee.Options{ + ReqBody: &struct { + Unrescue *int `json:"unrescue"` + }{nil}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + OkCodes: []int{202}, + }) + }) +} + +// See the CloudServersProvider interface for details +func (gsp *genericServersProvider) UpdateServer(id string, changes NewServerSettings) (*Server, error) { + var svr *Server + err := gsp.context.WithReauth(gsp.access, func() error { + url := fmt.Sprintf("%s/servers/%s", gsp.endpoint, id) + return perigee.Put(url, perigee.Options{ + ReqBody: &struct { + Server NewServerSettings `json:"server"` + }{changes}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + Results: &struct { + Server **Server `json:"server"` + }{&svr}, + }) + }) + return svr, err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) RebuildServer(id string, ns NewServer) (*Server, error) { + var s *Server + + err := gsp.context.WithReauth(gsp.access, func() error { + ep := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) + return perigee.Post(ep, perigee.Options{ + ReqBody: &struct { + Rebuild *NewServer `json:"rebuild"` + }{&ns}, + Results: &struct{ Server **Server }{&s}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + OkCodes: []int{202}, + }) + }) + + return s, err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) ListAddresses(id string) (AddressSet, error) { + var pas *AddressSet + var statusCode int + + err := gsp.context.WithReauth(gsp.access, func() error { + ep := fmt.Sprintf("%s/servers/%s/ips", gsp.endpoint, id) + return perigee.Get(ep, perigee.Options{ + Results: &struct{ Addresses **AddressSet }{&pas}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + OkCodes: []int{200, 203}, + StatusCode: &statusCode, + }) + }) + + if err != nil { + if statusCode == 203 { + err = WarnUnauthoritative + } + } + + return *pas, err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) ListAddressesByNetwork(id, networkLabel string) (NetworkAddress, error) { + pas := make(NetworkAddress) + var statusCode int + + err := gsp.context.WithReauth(gsp.access, func() error { + ep := fmt.Sprintf("%s/servers/%s/ips/%s", gsp.endpoint, id, networkLabel) + return perigee.Get(ep, perigee.Options{ + Results: &pas, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + OkCodes: []int{200, 203}, + StatusCode: &statusCode, + }) + }) + + if err != nil { + if statusCode == 203 { + err = WarnUnauthoritative + } + } + + return pas, err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) CreateImage(id string, ci CreateImage) (string, error) { + response, err := gsp.context.ResponseWithReauth(gsp.access, func() (*perigee.Response, error) { + ep := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id) + return perigee.Request("POST", ep, perigee.Options{ + ReqBody: &struct { + CreateImage *CreateImage `json:"createImage"` + }{&ci}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + OkCodes: []int{200, 202}, + }) + }) + + if err != nil { + return "", err + } + location, err := response.HttpResponse.Location() + if err != nil { + return "", err + } + + // Return the last element of the location which is the image id + locationArr := strings.Split(location.Path, "/") + return locationArr[len(locationArr)-1], err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) ListSecurityGroups() ([]SecurityGroup, error) { + var sgs []SecurityGroup + + err := gsp.context.WithReauth(gsp.access, func() error { + ep := fmt.Sprintf("%s/os-security-groups", gsp.endpoint) + return perigee.Get(ep, perigee.Options{ + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + Results: &struct { + SecurityGroups *[]SecurityGroup `json:"security_groups"` + }{&sgs}, + }) + }) + return sgs, err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) CreateSecurityGroup(desired SecurityGroup) (*SecurityGroup, error) { + var actual *SecurityGroup + + err := gsp.context.WithReauth(gsp.access, func() error { + ep := fmt.Sprintf("%s/os-security-groups", gsp.endpoint) + return perigee.Post(ep, perigee.Options{ + ReqBody: struct { + AddSecurityGroup SecurityGroup `json:"security_group"` + }{desired}, + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + Results: &struct { + SecurityGroup **SecurityGroup `json:"security_group"` + }{&actual}, + }) + }) + return actual, err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) ListSecurityGroupsByServerId(id string) ([]SecurityGroup, error) { + var sgs []SecurityGroup + + err := gsp.context.WithReauth(gsp.access, func() error { + ep := fmt.Sprintf("%s/servers/%s/os-security-groups", gsp.endpoint, id) + return perigee.Get(ep, perigee.Options{ + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + Results: &struct { + SecurityGroups *[]SecurityGroup `json:"security_groups"` + }{&sgs}, + }) + }) + return sgs, err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) SecurityGroupById(id int) (*SecurityGroup, error) { + var actual *SecurityGroup + + err := gsp.context.WithReauth(gsp.access, func() error { + ep := fmt.Sprintf("%s/os-security-groups/%d", gsp.endpoint, id) + return perigee.Get(ep, perigee.Options{ + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + Results: &struct { + SecurityGroup **SecurityGroup `json:"security_group"` + }{&actual}, + }) + }) + return actual, err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) DeleteSecurityGroupById(id int) error { + err := gsp.context.WithReauth(gsp.access, func() error { + ep := fmt.Sprintf("%s/os-security-groups/%d", gsp.endpoint, id) + return perigee.Delete(ep, perigee.Options{ + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + OkCodes: []int{202}, + }) + }) + return err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) ListDefaultSGRules() ([]SGRule, error) { + var sgrs []SGRule + err := gsp.context.WithReauth(gsp.access, func() error { + ep := fmt.Sprintf("%s/os-security-group-default-rules", gsp.endpoint) + return perigee.Get(ep, perigee.Options{ + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + Results: &struct{ Security_group_default_rules *[]SGRule }{&sgrs}, + }) + }) + return sgrs, err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) CreateDefaultSGRule(r SGRule) (*SGRule, error) { + var sgr *SGRule + err := gsp.context.WithReauth(gsp.access, func() error { + ep := fmt.Sprintf("%s/os-security-group-default-rules", gsp.endpoint) + return perigee.Post(ep, perigee.Options{ + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + Results: &struct{ Security_group_default_rule **SGRule }{&sgr}, + ReqBody: struct { + Security_group_default_rule SGRule `json:"security_group_default_rule"` + }{r}, + }) + }) + return sgr, err +} + +// See the CloudServersProvider interface for details. +func (gsp *genericServersProvider) GetSGRule(id string) (*SGRule, error) { + var sgr *SGRule + err := gsp.context.WithReauth(gsp.access, func() error { + ep := fmt.Sprintf("%s/os-security-group-default-rules/%s", gsp.endpoint, id) + return perigee.Get(ep, perigee.Options{ + MoreHeaders: map[string]string{ + "X-Auth-Token": gsp.access.AuthToken(), + }, + Results: &struct{ Security_group_default_rule **SGRule }{&sgr}, + }) + }) + return sgr, err +} + +// SecurityGroup provides a description of a security group, including all its rules. +type SecurityGroup struct { + Description string `json:"description,omitempty"` + Id int `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Rules []SGRule `json:"rules,omitempty"` + TenantId string `json:"tenant_id,omitempty"` +} + +// SGRule encapsulates a single rule which applies to a security group. +// This definition is just a guess, based on the documentation found in another extension here: http://docs.openstack.org/api/openstack-compute/2/content/GET_os-security-group-default-rules-v2_listSecGroupDefaultRules_v2__tenant_id__os-security-group-rules_ext-os-security-group-default-rules.html +type SGRule struct { + FromPort int `json:"from_port,omitempty"` + Id int `json:"id,omitempty"` + IpProtocol string `json:"ip_protocol,omitempty"` + IpRange map[string]interface{} `json:"ip_range,omitempty"` + ToPort int `json:"to_port,omitempty"` +} + +// RaxBandwidth provides measurement of server bandwidth consumed over a given audit interval. +type RaxBandwidth struct { + AuditPeriodEnd string `json:"audit_period_end"` + AuditPeriodStart string `json:"audit_period_start"` + BandwidthInbound int64 `json:"bandwidth_inbound"` + BandwidthOutbound int64 `json:"bandwidth_outbound"` + Interface string `json:"interface"` +} + +// A VersionedAddress denotes either an IPv4 or IPv6 (depending on version indicated) +// address. +type VersionedAddress struct { + Addr string `json:"addr"` + Version int `json:"version"` +} + +// An AddressSet provides a set of public and private IP addresses for a resource. +// Each address has a version to identify if IPv4 or IPv6. +type AddressSet struct { + Public []VersionedAddress `json:"public"` + Private []VersionedAddress `json:"private"` +} + +type NetworkAddress map[string][]VersionedAddress + +// Server records represent (virtual) hardware instances (not configurations) accessible by the user. +// +// The AccessIPv4 / AccessIPv6 fields provides IP addresses for the server in the IPv4 or IPv6 format, respectively. +// +// Addresses provides addresses for any attached isolated networks. +// The version field indicates whether the IP address is version 4 or 6. +// Note: only public and private pools appear here. +// To get the complete set, use the AllAddressPools() method instead. +// +// Created tells when the server entity was created. +// +// The Flavor field includes the flavor ID and flavor links. +// +// The compute provisioning algorithm has an anti-affinity property that +// attempts to spread customer VMs across hosts. +// Under certain situations, +// VMs from the same customer might be placed on the same host. +// The HostId field represents the host your server runs on and +// can be used to determine this scenario if it is relevant to your application. +// Note that HostId is unique only per account; it is not globally unique. +// +// Id provides the server's unique identifier. +// This field must be treated opaquely. +// +// Image indicates which image is installed on the server. +// +// Links provides one or more means of accessing the server. +// +// Metadata provides a small key-value store for application-specific information. +// +// Name provides a human-readable name for the server. +// +// Progress indicates how far along it is towards being provisioned. +// 100 represents complete, while 0 represents just beginning. +// +// Status provides an indication of what the server's doing at the moment. +// A server will be in ACTIVE state if it's ready for use. +// +// OsDcfDiskConfig indicates the server's boot volume configuration. +// Valid values are: +// AUTO +// ---- +// The server is built with a single partition the size of the target flavor disk. +// The file system is automatically adjusted to fit the entire partition. +// This keeps things simple and automated. +// AUTO is valid only for images and servers with a single partition that use the EXT3 file system. +// This is the default setting for applicable Rackspace base images. +// +// MANUAL +// ------ +// The server is built using whatever partition scheme and file system is in the source image. +// If the target flavor disk is larger, +// the remaining disk space is left unpartitioned. +// This enables images to have non-EXT3 file systems, multiple partitions, and so on, +// and enables you to manage the disk configuration. +// +// RaxBandwidth provides measures of the server's inbound and outbound bandwidth per interface. +// +// OsExtStsPowerState provides an indication of the server's power. +// This field appears to be a set of flag bits: +// +// ... 4 3 2 1 0 +// +--//--+---+---+---+---+ +// | .... | 0 | S | 0 | I | +// +--//--+---+---+---+---+ +// | | +// | +--- 0=Instance is down. +// | 1=Instance is up. +// | +// +----------- 0=Server is switched ON. +// 1=Server is switched OFF. +// (note reverse logic.) +// +// Unused bits should be ignored when read, and written as 0 for future compatibility. +// +// OsExtStsTaskState and OsExtStsVmState work together +// to provide visibility in the provisioning process for the instance. +// Consult Rackspace documentation at +// http://docs.rackspace.com/servers/api/v2/cs-devguide/content/ch_extensions.html#ext_status +// for more details. It's too lengthy to include here. +type Server struct { + AccessIPv4 string `json:"accessIPv4"` + AccessIPv6 string `json:"accessIPv6"` + Addresses AddressSet + Created string `json:"created"` + Flavor FlavorLink `json:"flavor"` + HostId string `json:"hostId"` + Id string `json:"id"` + Image ImageLink `json:"image"` + Links []Link `json:"links"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + Progress int `json:"progress"` + Status string `json:"status"` + TenantId string `json:"tenant_id"` + Updated string `json:"updated"` + UserId string `json:"user_id"` + OsDcfDiskConfig string `json:"OS-DCF:diskConfig"` + RaxBandwidth []RaxBandwidth `json:"rax-bandwidth:bandwidth"` + OsExtStsPowerState int `json:"OS-EXT-STS:power_state"` + OsExtStsTaskState string `json:"OS-EXT-STS:task_state"` + OsExtStsVmState string `json:"OS-EXT-STS:vm_state"` + + RawAddresses map[string]interface{} `json:"addresses"` +} + +// AllAddressPools returns a complete set of address pools available on the server. +// The name of each pool supported keys the map. +// The value of the map contains the addresses provided in the corresponding pool. +func (s *Server) AllAddressPools() (map[string][]VersionedAddress, error) { + pools := make(map[string][]VersionedAddress, 0) + for pool, subtree := range s.RawAddresses { + addresses := make([]VersionedAddress, 0) + err := mapstructure.Decode(subtree, &addresses) + if err != nil { + return nil, err + } + pools[pool] = addresses + } + return pools, nil +} + +// NewServerSettings structures record those fields of the Server structure to change +// when updating a server (see UpdateServer method). +type NewServerSettings struct { + Name string `json:"name,omitempty"` + AccessIPv4 string `json:"accessIPv4,omitempty"` + AccessIPv6 string `json:"accessIPv6,omitempty"` +} + +// NewServer structures are used for both requests and responses. +// The fields discussed below are relevent for server-creation purposes. +// +// The Name field contains the desired name of the server. +// Note that (at present) Rackspace permits more than one server with the same name; +// however, software should not depend on this. +// Not only will Rackspace support thank you, so will your own devops engineers. +// A name is required. +// +// The ImageRef field contains the ID of the desired software image to place on the server. +// This ID must be found in the image slice returned by the Images() function. +// This field is required. +// +// The FlavorRef field contains the ID of the server configuration desired for deployment. +// This ID must be found in the flavor slice returned by the Flavors() function. +// This field is required. +// +// For OsDcfDiskConfig, refer to the Image or Server structure documentation. +// This field defaults to "AUTO" if not explicitly provided. +// +// Metadata contains a small key/value association of arbitrary data. +// Neither Rackspace nor OpenStack places significance on this field in any way. +// This field defaults to an empty map if not provided. +// +// Personality specifies the contents of certain files in the server's filesystem. +// The files and their contents are mapped through a slice of FileConfig structures. +// If not provided, all filesystem entities retain their image-specific configuration. +// +// Networks specifies an affinity for the server's various networks and interfaces. +// Networks are identified through UUIDs; see NetworkConfig structure documentation for more details. +// If not provided, network affinity is determined automatically. +// +// The AdminPass field may be used to provide a root- or administrator-password +// during the server provisioning process. +// If not provided, a random password will be automatically generated and returned in this field. +// +// The following fields are intended to be used to communicate certain results about the server being provisioned. +// When attempting to create a new server, these fields MUST not be provided. +// They'll be filled in by the response received from the Rackspace APIs. +// +// The Id field contains the server's unique identifier. +// The identifier's scope is best assumed to be bound by the user's account, unless other arrangements have been made with Rackspace. +// +// The SecurityGroup field allows the user to specify a security group at launch. +// +// Any Links provided are used to refer to the server specifically by URL. +// These links are useful for making additional REST calls not explicitly supported by Gorax. +type NewServer struct { + Name string `json:"name,omitempty"` + ImageRef string `json:"imageRef,omitempty"` + FlavorRef string `json:"flavorRef,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` + Personality []FileConfig `json:"personality,omitempty"` + Networks []NetworkConfig `json:"networks,omitempty"` + AdminPass string `json:"adminPass,omitempty"` + KeyPairName string `json:"key_name,omitempty"` + Id string `json:"id,omitempty"` + Links []Link `json:"links,omitempty"` + OsDcfDiskConfig string `json:"OS-DCF:diskConfig,omitempty"` + SecurityGroup []map[string]interface{} `json:"security_groups,omitempty"` + ConfigDrive bool `json:"config_drive"` + UserData string `json:"user_data"` +} + +// ResizeRequest structures are used internally to encode to JSON the parameters required to resize a server instance. +// Client applications will not use this structure (no API accepts an instance of this structure). +// See the Region method ResizeServer() for more details on how to resize a server. +type ResizeRequest struct { + Name string `json:"name,omitempty"` + FlavorRef string `json:"flavorRef"` + DiskConfig string `json:"OS-DCF:diskConfig,omitempty"` +} + +type CreateImage struct { + Name string `json:"name"` + Metadata map[string]string `json:"metadata,omitempty"` +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/servers_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/servers_test.go new file mode 100644 index 0000000000..60c71c889f --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/servers_test.go @@ -0,0 +1,54 @@ +package gophercloud + +import ( + "net/http" + "testing" +) + +type testAccess struct { + public, internal string + calledFirstEndpointByCriteria int +} + +func (ta *testAccess) FirstEndpointUrlByCriteria(ac ApiCriteria) string { + ta.calledFirstEndpointByCriteria++ + urls := []string{ta.public, ta.internal} + return urls[ac.UrlChoice] +} + +func (ta *testAccess) AuthToken() string { + return "" +} + +func (ta *testAccess) Revoke(string) error { + return nil +} + +func (ta *testAccess) Reauthenticate() error { + return nil +} + +func TestGetServersApi(t *testing.T) { + c := TestContext().UseCustomClient(&http.Client{Transport: newTransport().WithResponse("Hello")}) + + acc := &testAccess{ + public: "http://localhost:8080", + internal: "http://localhost:8086", + } + + _, err := c.ServersApi(acc, ApiCriteria{ + Name: "cloudComputeOpenStack", + Region: "dfw", + VersionId: "2", + }) + + if err != nil { + t.Error(err) + return + } + + if acc.calledFirstEndpointByCriteria != 1 { + t.Error("Expected FirstEndpointByCriteria to be called") + return + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_catalog.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_catalog.go new file mode 100644 index 0000000000..e6cf4a00ea --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_catalog.go @@ -0,0 +1,75 @@ +package gophercloud + +import ( + "os" + "strings" +) + +// ApiCriteria provides one or more criteria for the SDK to look for appropriate endpoints. +// Fields left unspecified or otherwise set to their zero-values are assumed to not be +// relevant, and do not participate in the endpoint search. +// +// Name specifies the desired service catalog entry name. +// Type specifies the desired service catalog entry type. +// Region specifies the desired endpoint region. +// If unset, Gophercloud will try to use the region set in the +// OS_REGION_NAME environment variable. If that's not set, +// region comparison will not occur. If OS_REGION_NAME is set +// and IgnoreEnvVars is also set, OS_REGION_NAME will be ignored. +// VersionId specifies the desired version of the endpoint. +// Note that this field is matched exactly, and is (at present) +// opaque to Gophercloud. Thus, requesting a version 2 +// endpoint will _not_ match a version 3 endpoint. +// The UrlChoice field inidicates whether or not gophercloud +// should use the public or internal endpoint URL if a +// candidate endpoint is found. +// IgnoreEnvVars instructs Gophercloud to ignore helpful environment variables. +type ApiCriteria struct { + Name string + Type string + Region string + VersionId string + UrlChoice int + IgnoreEnvVars bool +} + +// The choices available for UrlChoice. See the ApiCriteria structure for details. +const ( + PublicURL = iota + InternalURL +) + +// Given a set of criteria to match on, locate the first candidate endpoint +// in the provided service catalog. +// +// If nothing found, the result will be a zero-valued EntryEndpoint (all URLs +// set to ""). +func FindFirstEndpointByCriteria(entries []CatalogEntry, ac ApiCriteria) EntryEndpoint { + rgn := strings.ToUpper(ac.Region) + if (rgn == "") && !ac.IgnoreEnvVars { + rgn = os.Getenv("OS_REGION_NAME") + } + + for _, entry := range entries { + if (ac.Name != "") && (ac.Name != entry.Name) { + continue + } + + if (ac.Type != "") && (ac.Type != entry.Type) { + continue + } + + for _, endpoint := range entry.Endpoints { + if (rgn != "") && (rgn != strings.ToUpper(endpoint.Region)) { + continue + } + + if (ac.VersionId != "") && (ac.VersionId != endpoint.VersionId) { + continue + } + + return endpoint + } + } + return EntryEndpoint{} +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_catalog_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_catalog_test.go new file mode 100644 index 0000000000..b78f01fced --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/service_catalog_test.go @@ -0,0 +1,190 @@ +package gophercloud + +import ( + "os" + "testing" +) + +// TestFFEBCViaEnvVariable exercises only those calls where a region +// parameter is required, but is provided by an environment variable. +func TestFFEBCViaEnvVariable(t *testing.T) { + changeRegion("RGN") + + endpoint := FindFirstEndpointByCriteria( + catalog("test", "compute", "http://localhost", "", ""), + ApiCriteria{Name: "test"}, + ) + if endpoint.PublicURL != "" { + t.Error("If provided, the Region qualifier must exclude endpoints with missing or mismatching regions.") + return + } + + endpoint = FindFirstEndpointByCriteria( + catalog("test", "compute", "http://localhost", "rgn", ""), + ApiCriteria{Name: "test"}, + ) + if endpoint.PublicURL != "http://localhost" { + t.Error("Regions are case insensitive.") + return + } + + endpoint = FindFirstEndpointByCriteria( + catalog("test", "compute", "http://localhost", "rgn", ""), + ApiCriteria{Name: "test", VersionId: "2"}, + ) + if endpoint.PublicURL != "" { + t.Error("Missing version ID means no match.") + return + } + + endpoint = FindFirstEndpointByCriteria( + catalog("test", "compute", "http://localhost", "rgn", "3"), + ApiCriteria{Name: "test", VersionId: "2"}, + ) + if endpoint.PublicURL != "" { + t.Error("Mismatched version ID means no match.") + return + } + + endpoint = FindFirstEndpointByCriteria( + catalog("test", "compute", "http://localhost", "rgn", "2"), + ApiCriteria{Name: "test", VersionId: "2"}, + ) + if endpoint.PublicURL != "http://localhost" { + t.Error("All search criteria met; endpoint expected.") + return + } +} + +// TestFFEBCViaRegionOption exercises only those calls where a region +// parameter is specified explicitly. The region option overrides +// any defined OS_REGION_NAME environment setting. +func TestFFEBCViaRegionOption(t *testing.T) { + changeRegion("Starfleet Command") + + endpoint := FindFirstEndpointByCriteria( + catalog("test", "compute", "http://localhost", "", ""), + ApiCriteria{Name: "test", Region: "RGN"}, + ) + if endpoint.PublicURL != "" { + t.Error("If provided, the Region qualifier must exclude endpoints with missing or mismatching regions.") + return + } + + endpoint = FindFirstEndpointByCriteria( + catalog("test", "compute", "http://localhost", "rgn", ""), + ApiCriteria{Name: "test", Region: "RGN"}, + ) + if endpoint.PublicURL != "http://localhost" { + t.Error("Regions are case insensitive.") + return + } + + endpoint = FindFirstEndpointByCriteria( + catalog("test", "compute", "http://localhost", "rgn", ""), + ApiCriteria{Name: "test", Region: "RGN", VersionId: "2"}, + ) + if endpoint.PublicURL != "" { + t.Error("Missing version ID means no match.") + return + } + + endpoint = FindFirstEndpointByCriteria( + catalog("test", "compute", "http://localhost", "rgn", "3"), + ApiCriteria{Name: "test", Region: "RGN", VersionId: "2"}, + ) + if endpoint.PublicURL != "" { + t.Error("Mismatched version ID means no match.") + return + } + + endpoint = FindFirstEndpointByCriteria( + catalog("test", "compute", "http://localhost", "rgn", "2"), + ApiCriteria{Name: "test", Region: "RGN", VersionId: "2"}, + ) + if endpoint.PublicURL != "http://localhost" { + t.Error("All search criteria met; endpoint expected.") + return + } +} + +// TestFFEBCWithoutRegion exercises only those calls where a region +// is irrelevant. Just to make sure, though, we enforce Gophercloud +// from paying any attention to OS_REGION_NAME if it happens to be set. +func TestFindFirstEndpointByCriteria(t *testing.T) { + endpoint := FindFirstEndpointByCriteria([]CatalogEntry{}, ApiCriteria{Name: "test", IgnoreEnvVars: true}) + if endpoint.PublicURL != "" { + t.Error("Not expecting to find anything in an empty service catalog.") + return + } + + endpoint = FindFirstEndpointByCriteria( + []CatalogEntry{ + {Name: "test"}, + }, + ApiCriteria{Name: "test", IgnoreEnvVars: true}, + ) + if endpoint.PublicURL != "" { + t.Error("Even though we have a matching entry, no endpoints exist") + return + } + + endpoint = FindFirstEndpointByCriteria( + catalog("test", "compute", "http://localhost", "", ""), + ApiCriteria{Name: "test", IgnoreEnvVars: true}, + ) + if endpoint.PublicURL != "http://localhost" { + t.Error("Looking for an endpoint by name but without region or version ID should match first entry endpoint.") + return + } + + endpoint = FindFirstEndpointByCriteria( + catalog("test", "compute", "http://localhost", "", ""), + ApiCriteria{Type: "compute", IgnoreEnvVars: true}, + ) + if endpoint.PublicURL != "http://localhost" { + t.Error("Looking for an endpoint by type but without region or version ID should match first entry endpoint.") + return + } + + endpoint = FindFirstEndpointByCriteria( + catalog("test", "compute", "http://localhost", "", ""), + ApiCriteria{Type: "identity", IgnoreEnvVars: true}, + ) + if endpoint.PublicURL != "" { + t.Error("Returned mismatched type.") + return + } + + endpoint = FindFirstEndpointByCriteria( + catalog("test", "compute", "http://localhost", "ord", "2"), + ApiCriteria{Name: "test", VersionId: "2", IgnoreEnvVars: true}, + ) + if endpoint.PublicURL != "http://localhost" { + t.Error("Sometimes, you might not care what region your stuff is in.") + return + } +} + +func catalog(name, entry_type, url, region, version string) []CatalogEntry { + return []CatalogEntry{ + { + Name: name, + Type: entry_type, + Endpoints: []EntryEndpoint{ + { + PublicURL: url, + Region: region, + VersionId: version, + }, + }, + }, + } +} + +func changeRegion(r string) { + err := os.Setenv("OS_REGION_NAME", r) + if err != nil { + panic(err) + } +} diff --git a/Godeps/_workspace/src/github.com/rackspace/gophercloud/transport_double_test.go b/Godeps/_workspace/src/github.com/rackspace/gophercloud/transport_double_test.go new file mode 100644 index 0000000000..ef7f19a5d8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rackspace/gophercloud/transport_double_test.go @@ -0,0 +1,103 @@ +package gophercloud + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" +) + +type transport struct { + called int + response string + expectTenantId bool + tenantIdFound bool + status int +} + +func (t *transport) RoundTrip(req *http.Request) (rsp *http.Response, err error) { + var authContainer *AuthContainer + + t.called++ + + headers := make(http.Header) + headers.Add("Content-Type", "application/xml; charset=UTF-8") + + body := ioutil.NopCloser(strings.NewReader(t.response)) + + if t.status == 0 { + t.status = 200 + } + statusMsg := "OK" + if (t.status < 200) || (299 < t.status) { + statusMsg = "Error" + } + + rsp = &http.Response{ + Status: fmt.Sprintf("%d %s", t.status, statusMsg), + StatusCode: t.status, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: headers, + Body: body, + ContentLength: -1, + TransferEncoding: nil, + Close: true, + Trailer: nil, + Request: req, + } + + bytes, err := ioutil.ReadAll(req.Body) + if err != nil { + return nil, err + } + err = json.Unmarshal(bytes, &authContainer) + if err != nil { + return nil, err + } + t.tenantIdFound = (authContainer.Auth.TenantId != "") + + if t.tenantIdFound != t.expectTenantId { + rsp.Status = "500 Internal Server Error" + rsp.StatusCode = 500 + } + return +} + +func newTransport() *transport { + return &transport{} +} + +func (t *transport) IgnoreTenantId() *transport { + t.expectTenantId = false + return t +} + +func (t *transport) ExpectTenantId() *transport { + t.expectTenantId = true + return t +} + +func (t *transport) WithResponse(r string) *transport { + t.response = r + t.status = 200 + return t +} + +func (t *transport) WithError(code int) *transport { + t.response = fmt.Sprintf("Error %d", code) + t.status = code + return t +} + +func (t *transport) VerifyCalls(test *testing.T, n int) error { + if t.called != n { + err := fmt.Errorf("Expected Transport to be called %d times; found %d instead", n, t.called) + test.Error(err) + return err + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/.gitignore b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/.gitignore new file mode 100644 index 0000000000..1f0a99f2f2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/.gitignore @@ -0,0 +1,4 @@ +[568].out +_go* +_test* +_obj diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/License b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/License new file mode 100644 index 0000000000..480a328059 --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/License @@ -0,0 +1,19 @@ +Copyright 2012 Keith Rarick + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/Readme b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/Readme new file mode 100644 index 0000000000..c589fc622b --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/Readme @@ -0,0 +1,9 @@ +package pretty + + import "github.com/kr/pretty" + + Package pretty provides pretty-printing for Go values. + +Documentation + + http://godoc.org/github.com/kr/pretty diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/changelog b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/changelog new file mode 100644 index 0000000000..9350c05782 --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/changelog @@ -0,0 +1,5 @@ +golang-pretty (0.0~git20130613-1) unstable; urgency=low + + * Initial release. Closes: #722983 + + -- Tonnerre Lombard Wed, 11 Sep 2013 02:36:12 +0200 diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/compat b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/compat new file mode 100644 index 0000000000..ec635144f6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/compat @@ -0,0 +1 @@ +9 diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/control b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/control new file mode 100644 index 0000000000..a852fe7fc3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/control @@ -0,0 +1,22 @@ +Source: golang-pretty +Section: devel +Priority: extra +Maintainer: Tonnerre Lombard +Build-Depends: debhelper (>= 9), golang-go, dh-golang, + golang-text-dev +Standards-Version: 3.9.4 +Homepage: https://github.com/kr/pretty/ +Vcs-Git: git://anonscm.debian.org/pkg-go/packages/golang-pretty.git +Vcs-Browser: http://anonscm.debian.org/gitweb/?p=pkg-go/packages/golang-pretty.git;a=summary + +Package: golang-pretty-dev +Architecture: all +Depends: ${shlibs:Depends}, ${misc:Depends}, golang-text-dev +Description: Pretty printing for go values + Package pretty provides pretty-printing for Go values. This is useful + during debugging, to avoid wrapping long output lines in the + terminal. + . + It provides a function, Formatter, that can be used with any function + that accepts a format string. It also provides convenience wrappers + for functions in packages fmt and log. diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/copyright b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/copyright new file mode 100644 index 0000000000..80b0807ee7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/copyright @@ -0,0 +1,30 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: golang-pretty +Source: https://github.com/kr/pretty/ + +Files: * +Copyright: 2011, 2012, 2013 Keith Rarick +License: Expat + +Files: debian/* +Copyright: 2013 Tonnerre Lombard +License: Expat + +License: Expat + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/docs b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/docs new file mode 100644 index 0000000000..304360caba --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/docs @@ -0,0 +1 @@ +Readme diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/rules b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/rules new file mode 100644 index 0000000000..c48a3adc3f --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/rules @@ -0,0 +1,11 @@ +#!/usr/bin/make -f + +# Uncomment this to turn on verbose mode. +export DH_VERBOSE=1 + +# DH_GOPKG is the upstream path which you would normally “go get”. +# Using it allows us to build applications without patching locations. +export DH_GOPKG := github.com/kr/pretty + +%: + dh $@ --buildsystem=golang --with=golang diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/source/format b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/source/format new file mode 100644 index 0000000000..163aaf8d82 --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/diff.go b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/diff.go new file mode 100644 index 0000000000..64fac6403c --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/diff.go @@ -0,0 +1,148 @@ +package pretty + +import ( + "fmt" + "io" + "reflect" +) + +type sbuf []string + +func (s *sbuf) Write(b []byte) (int, error) { + *s = append(*s, string(b)) + return len(b), nil +} + +// Diff returns a slice where each element describes +// a difference between a and b. +func Diff(a, b interface{}) (desc []string) { + Fdiff((*sbuf)(&desc), a, b) + return desc +} + +// Fdiff writes to w a description of the differences between a and b. +func Fdiff(w io.Writer, a, b interface{}) { + diffWriter{w: w}.diff(reflect.ValueOf(a), reflect.ValueOf(b)) +} + +type diffWriter struct { + w io.Writer + l string // label +} + +func (w diffWriter) printf(f string, a ...interface{}) { + var l string + if w.l != "" { + l = w.l + ": " + } + fmt.Fprintf(w.w, l+f, a...) +} + +func (w diffWriter) diff(av, bv reflect.Value) { + if !av.IsValid() && bv.IsValid() { + w.printf("nil != %#v", bv.Interface()) + return + } + if av.IsValid() && !bv.IsValid() { + w.printf("%#v != nil", av.Interface()) + return + } + if !av.IsValid() && !bv.IsValid() { + return + } + + at := av.Type() + bt := bv.Type() + if at != bt { + w.printf("%v != %v", at, bt) + return + } + + // numeric types, including bool + if at.Kind() < reflect.Array { + a, b := av.Interface(), bv.Interface() + if a != b { + w.printf("%#v != %#v", a, b) + } + return + } + + switch at.Kind() { + case reflect.String: + a, b := av.Interface(), bv.Interface() + if a != b { + w.printf("%q != %q", a, b) + } + case reflect.Ptr: + switch { + case av.IsNil() && !bv.IsNil(): + w.printf("nil != %v", bv.Interface()) + case !av.IsNil() && bv.IsNil(): + w.printf("%v != nil", av.Interface()) + case !av.IsNil() && !bv.IsNil(): + w.diff(av.Elem(), bv.Elem()) + } + case reflect.Struct: + for i := 0; i < av.NumField(); i++ { + w.relabel(at.Field(i).Name).diff(av.Field(i), bv.Field(i)) + } + case reflect.Map: + ak, both, bk := keyDiff(av.MapKeys(), bv.MapKeys()) + for _, k := range ak { + w := w.relabel(fmt.Sprintf("[%#v]", k.Interface())) + w.printf("%q != (missing)", av.MapIndex(k)) + } + for _, k := range both { + w := w.relabel(fmt.Sprintf("[%#v]", k.Interface())) + w.diff(av.MapIndex(k), bv.MapIndex(k)) + } + for _, k := range bk { + w := w.relabel(fmt.Sprintf("[%#v]", k.Interface())) + w.printf("(missing) != %q", bv.MapIndex(k)) + } + case reflect.Interface: + w.diff(reflect.ValueOf(av.Interface()), reflect.ValueOf(bv.Interface())) + default: + if !reflect.DeepEqual(av.Interface(), bv.Interface()) { + w.printf("%# v != %# v", Formatter(av.Interface()), Formatter(bv.Interface())) + } + } +} + +func (d diffWriter) relabel(name string) (d1 diffWriter) { + d1 = d + if d.l != "" && name[0] != '[' { + d1.l += "." + } + d1.l += name + return d1 +} + +func keyDiff(a, b []reflect.Value) (ak, both, bk []reflect.Value) { + for _, av := range a { + inBoth := false + for _, bv := range b { + if reflect.DeepEqual(av.Interface(), bv.Interface()) { + inBoth = true + both = append(both, av) + break + } + } + if !inBoth { + ak = append(ak, av) + } + } + for _, bv := range b { + inBoth := false + for _, av := range a { + if reflect.DeepEqual(av.Interface(), bv.Interface()) { + inBoth = true + break + } + } + if !inBoth { + bk = append(bk, bv) + } + } + return +} diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/diff_test.go b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/diff_test.go new file mode 100644 index 0000000000..02d1953530 --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/diff_test.go @@ -0,0 +1,73 @@ +package pretty + +import ( + "testing" +) + +type difftest struct { + a interface{} + b interface{} + exp []string +} + +type S struct { + A int + S *S + I interface{} + C []int +} + +var diffs = []difftest{ + {a: nil, b: nil}, + {a: S{A: 1}, b: S{A: 1}}, + + {0, "", []string{`int != string`}}, + {0, 1, []string{`0 != 1`}}, + {S{}, new(S), []string{`pretty.S != *pretty.S`}}, + {"a", "b", []string{`"a" != "b"`}}, + {S{}, S{A: 1}, []string{`A: 0 != 1`}}, + {new(S), &S{A: 1}, []string{`A: 0 != 1`}}, + {S{S: new(S)}, S{S: &S{A: 1}}, []string{`S.A: 0 != 1`}}, + {S{}, S{I: 0}, []string{`I: nil != 0`}}, + {S{I: 1}, S{I: "x"}, []string{`I: int != string`}}, + {S{}, S{C: []int{1}}, []string{`C: []int(nil) != []int{1}`}}, + {S{C: []int{}}, S{C: []int{1}}, []string{`C: []int{} != []int{1}`}}, + {S{}, S{A: 1, S: new(S)}, []string{`A: 0 != 1`, `S: nil != &{0 []}`}}, +} + +func TestDiff(t *testing.T) { + for _, tt := range diffs { + got := Diff(tt.a, tt.b) + eq := len(got) == len(tt.exp) + if eq { + for i := range got { + eq = eq && got[i] == tt.exp[i] + } + } + if !eq { + t.Errorf("diffing % #v", tt.a) + t.Errorf("with % #v", tt.b) + diffdiff(t, got, tt.exp) + continue + } + } +} + +func diffdiff(t *testing.T, got, exp []string) { + minus(t, "unexpected:", got, exp) + minus(t, "missing:", exp, got) +} + +func minus(t *testing.T, s string, a, b []string) { + var i, j int + for i = 0; i < len(a); i++ { + for j = 0; j < len(b); j++ { + if a[i] == b[j] { + break + } + } + if j == len(b) { + t.Error(s, a[i]) + } + } +} diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/example_test.go b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/example_test.go new file mode 100644 index 0000000000..ecf40f3fcc --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/example_test.go @@ -0,0 +1,20 @@ +package pretty_test + +import ( + "fmt" + "github.com/kr/pretty" +) + +func Example() { + type myType struct { + a, b int + } + var x = []myType{{1, 2}, {3, 4}, {5, 6}} + fmt.Printf("%# v", pretty.Formatter(x)) + // output: + // []pretty_test.myType{ + // {a:1, b:2}, + // {a:3, b:4}, + // {a:5, b:6}, + // } +} diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/formatter.go b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/formatter.go new file mode 100644 index 0000000000..1161de7ee0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/formatter.go @@ -0,0 +1,300 @@ +package pretty + +import ( + "fmt" + "github.com/kr/text" + "io" + "reflect" + "strconv" + "text/tabwriter" +) + +const ( + limit = 50 +) + +type formatter struct { + x interface{} + force bool + quote bool +} + +// Formatter makes a wrapper, f, that will format x as go source with line +// breaks and tabs. Object f responds to the "%v" formatting verb when both the +// "#" and " " (space) flags are set, for example: +// +// fmt.Sprintf("%# v", Formatter(x)) +// +// If one of these two flags is not set, or any other verb is used, f will +// format x according to the usual rules of package fmt. +// In particular, if x satisfies fmt.Formatter, then x.Format will be called. +func Formatter(x interface{}) (f fmt.Formatter) { + return formatter{x: x, quote: true} +} + +func (fo formatter) String() string { + return fmt.Sprint(fo.x) // unwrap it +} + +func (fo formatter) passThrough(f fmt.State, c rune) { + s := "%" + for i := 0; i < 128; i++ { + if f.Flag(i) { + s += string(i) + } + } + if w, ok := f.Width(); ok { + s += fmt.Sprintf("%d", w) + } + if p, ok := f.Precision(); ok { + s += fmt.Sprintf(".%d", p) + } + s += string(c) + fmt.Fprintf(f, s, fo.x) +} + +func (fo formatter) Format(f fmt.State, c rune) { + if fo.force || c == 'v' && f.Flag('#') && f.Flag(' ') { + w := tabwriter.NewWriter(f, 4, 4, 1, ' ', 0) + p := &printer{tw: w, Writer: w} + p.printValue(reflect.ValueOf(fo.x), true, fo.quote) + w.Flush() + return + } + fo.passThrough(f, c) +} + +type printer struct { + io.Writer + tw *tabwriter.Writer +} + +func (p *printer) indent() *printer { + q := *p + q.tw = tabwriter.NewWriter(p.Writer, 4, 4, 1, ' ', 0) + q.Writer = text.NewIndentWriter(q.tw, []byte{'\t'}) + return &q +} + +func (p *printer) printInline(v reflect.Value, x interface{}, showType bool) { + if showType { + io.WriteString(p, v.Type().String()) + fmt.Fprintf(p, "(%#v)", x) + } else { + fmt.Fprintf(p, "%#v", x) + } +} + +func (p *printer) printValue(v reflect.Value, showType, quote bool) { + switch v.Kind() { + case reflect.Bool: + p.printInline(v, v.Bool(), showType) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + p.printInline(v, v.Int(), showType) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + p.printInline(v, v.Uint(), showType) + case reflect.Float32, reflect.Float64: + p.printInline(v, v.Float(), showType) + case reflect.Complex64, reflect.Complex128: + fmt.Fprintf(p, "%#v", v.Complex()) + case reflect.String: + p.fmtString(v.String(), quote) + case reflect.Map: + t := v.Type() + if showType { + io.WriteString(p, t.String()) + } + writeByte(p, '{') + if nonzero(v) { + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + keys := v.MapKeys() + for i := 0; i < v.Len(); i++ { + showTypeInStruct := true + k := keys[i] + mv := v.MapIndex(k) + pp.printValue(k, false, true) + writeByte(pp, ':') + if expand { + writeByte(pp, '\t') + } + showTypeInStruct = t.Elem().Kind() == reflect.Interface + pp.printValue(mv, showTypeInStruct, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.Len()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + } + writeByte(p, '}') + case reflect.Struct: + t := v.Type() + if showType { + io.WriteString(p, t.String()) + } + writeByte(p, '{') + if nonzero(v) { + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + for i := 0; i < v.NumField(); i++ { + showTypeInStruct := true + if f := t.Field(i); f.Name != "" { + io.WriteString(pp, f.Name) + writeByte(pp, ':') + if expand { + writeByte(pp, '\t') + } + showTypeInStruct = f.Type.Kind() == reflect.Interface + } + pp.printValue(getField(v, i), showTypeInStruct, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.NumField()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + } + writeByte(p, '}') + case reflect.Interface: + switch e := v.Elem(); { + case e.Kind() == reflect.Invalid: + io.WriteString(p, "nil") + case e.IsValid(): + p.printValue(e, showType, true) + default: + io.WriteString(p, v.Type().String()) + io.WriteString(p, "(nil)") + } + case reflect.Array, reflect.Slice: + t := v.Type() + if showType { + io.WriteString(p, t.String()) + } + if v.Kind() == reflect.Slice && v.IsNil() && showType { + io.WriteString(p, "(nil)") + break + } + if v.Kind() == reflect.Slice && v.IsNil() { + io.WriteString(p, "nil") + break + } + writeByte(p, '{') + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + for i := 0; i < v.Len(); i++ { + showTypeInSlice := t.Elem().Kind() == reflect.Interface + pp.printValue(v.Index(i), showTypeInSlice, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.Len()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + writeByte(p, '}') + case reflect.Ptr: + e := v.Elem() + if !e.IsValid() { + writeByte(p, '(') + io.WriteString(p, v.Type().String()) + io.WriteString(p, ")(nil)") + } else { + writeByte(p, '&') + p.printValue(e, true, true) + } + case reflect.Chan: + x := v.Pointer() + if showType { + writeByte(p, '(') + io.WriteString(p, v.Type().String()) + fmt.Fprintf(p, ")(%#v)", x) + } else { + fmt.Fprintf(p, "%#v", x) + } + case reflect.Func: + io.WriteString(p, v.Type().String()) + io.WriteString(p, " {...}") + case reflect.UnsafePointer: + p.printInline(v, v.Pointer(), showType) + case reflect.Invalid: + io.WriteString(p, "nil") + } +} + +func canInline(t reflect.Type) bool { + switch t.Kind() { + case reflect.Map: + return !canExpand(t.Elem()) + case reflect.Struct: + for i := 0; i < t.NumField(); i++ { + if canExpand(t.Field(i).Type) { + return false + } + } + return true + case reflect.Interface: + return false + case reflect.Array, reflect.Slice: + return !canExpand(t.Elem()) + case reflect.Ptr: + return false + case reflect.Chan, reflect.Func, reflect.UnsafePointer: + return false + } + return true +} + +func canExpand(t reflect.Type) bool { + switch t.Kind() { + case reflect.Map, reflect.Struct, + reflect.Interface, reflect.Array, reflect.Slice, + reflect.Ptr: + return true + } + return false +} + +func (p *printer) fmtString(s string, quote bool) { + if quote { + s = strconv.Quote(s) + } + io.WriteString(p, s) +} + +func tryDeepEqual(a, b interface{}) bool { + defer func() { recover() }() + return reflect.DeepEqual(a, b) +} + +func writeByte(w io.Writer, b byte) { + w.Write([]byte{b}) +} + +func getField(v reflect.Value, i int) reflect.Value { + val := v.Field(i) + if val.Kind() == reflect.Interface && !val.IsNil() { + val = val.Elem() + } + return val +} diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/formatter_test.go b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/formatter_test.go new file mode 100644 index 0000000000..303f033c43 --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/formatter_test.go @@ -0,0 +1,146 @@ +package pretty + +import ( + "fmt" + "io" + "testing" + "unsafe" +) + +type test struct { + v interface{} + s string +} + +type LongStructTypeName struct { + longFieldName interface{} + otherLongFieldName interface{} +} + +type SA struct { + t *T +} + +type T struct { + x, y int +} + +type F int + +func (f F) Format(s fmt.State, c rune) { + fmt.Fprintf(s, "F(%d)", int(f)) +} + +var long = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +var gosyntax = []test{ + {nil, `nil`}, + {"", `""`}, + {"a", `"a"`}, + {1, "int(1)"}, + {1.0, "float64(1)"}, + {[]int(nil), "[]int(nil)"}, + {[0]int{}, "[0]int{}"}, + {complex(1, 0), "(1+0i)"}, + //{make(chan int), "(chan int)(0x1234)"}, + {unsafe.Pointer(uintptr(1)), "unsafe.Pointer(0x1)"}, + {func(int) {}, "func(int) {...}"}, + {map[int]int{1: 1}, "map[int]int{1:1}"}, + {int32(1), "int32(1)"}, + {io.EOF, `&errors.errorString{s:"EOF"}`}, + {[]string{"a"}, `[]string{"a"}`}, + { + []string{long}, + `[]string{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}`, + }, + {F(5), "pretty.F(5)"}, + { + SA{&T{1, 2}}, + `pretty.SA{ + t: &pretty.T{x:1, y:2}, +}`, + }, + { + map[int][]byte{1: []byte{}}, + `map[int][]uint8{ + 1: {}, +}`, + }, + { + map[int]T{1: T{}}, + `map[int]pretty.T{ + 1: {}, +}`, + }, + { + long, + `"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"`, + }, + { + LongStructTypeName{ + longFieldName: LongStructTypeName{}, + otherLongFieldName: long, + }, + `pretty.LongStructTypeName{ + longFieldName: pretty.LongStructTypeName{}, + otherLongFieldName: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", +}`, + }, + { + &LongStructTypeName{ + longFieldName: &LongStructTypeName{}, + otherLongFieldName: (*LongStructTypeName)(nil), + }, + `&pretty.LongStructTypeName{ + longFieldName: &pretty.LongStructTypeName{}, + otherLongFieldName: (*pretty.LongStructTypeName)(nil), +}`, + }, + { + []LongStructTypeName{ + {nil, nil}, + {3, 3}, + {long, nil}, + }, + `[]pretty.LongStructTypeName{ + {}, + { + longFieldName: int(3), + otherLongFieldName: int(3), + }, + { + longFieldName: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + otherLongFieldName: nil, + }, +}`, + }, + { + []interface{}{ + LongStructTypeName{nil, nil}, + []byte{1, 2, 3}, + T{3, 4}, + LongStructTypeName{long, nil}, + }, + `[]interface {}{ + pretty.LongStructTypeName{}, + []uint8{0x1, 0x2, 0x3}, + pretty.T{x:3, y:4}, + pretty.LongStructTypeName{ + longFieldName: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + otherLongFieldName: nil, + }, +}`, + }, +} + +func TestGoSyntax(t *testing.T) { + for _, tt := range gosyntax { + s := fmt.Sprintf("%# v", Formatter(tt.v)) + if tt.s != s { + t.Errorf("expected %q", tt.s) + t.Errorf("got %q", s) + t.Errorf("expraw\n%s", tt.s) + t.Errorf("gotraw\n%s", s) + } + } +} diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/pretty.go b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/pretty.go new file mode 100644 index 0000000000..d3df8686ce --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/pretty.go @@ -0,0 +1,98 @@ +// Package pretty provides pretty-printing for Go values. This is +// useful during debugging, to avoid wrapping long output lines in +// the terminal. +// +// It provides a function, Formatter, that can be used with any +// function that accepts a format string. It also provides +// convenience wrappers for functions in packages fmt and log. +package pretty + +import ( + "fmt" + "io" + "log" +) + +// Errorf is a convenience wrapper for fmt.Errorf. +// +// Calling Errorf(f, x, y) is equivalent to +// fmt.Errorf(f, Formatter(x), Formatter(y)). +func Errorf(format string, a ...interface{}) error { + return fmt.Errorf(format, wrap(a, false)...) +} + +// Fprintf is a convenience wrapper for fmt.Fprintf. +// +// Calling Fprintf(w, f, x, y) is equivalent to +// fmt.Fprintf(w, f, Formatter(x), Formatter(y)). +func Fprintf(w io.Writer, format string, a ...interface{}) (n int, error error) { + return fmt.Fprintf(w, format, wrap(a, false)...) +} + +// Log is a convenience wrapper for log.Printf. +// +// Calling Log(x, y) is equivalent to +// log.Print(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Log(a ...interface{}) { + log.Print(wrap(a, true)...) +} + +// Logf is a convenience wrapper for log.Printf. +// +// Calling Logf(f, x, y) is equivalent to +// log.Printf(f, Formatter(x), Formatter(y)). +func Logf(format string, a ...interface{}) { + log.Printf(format, wrap(a, false)...) +} + +// Logln is a convenience wrapper for log.Printf. +// +// Calling Logln(x, y) is equivalent to +// log.Println(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Logln(a ...interface{}) { + log.Println(wrap(a, true)...) +} + +// Print pretty-prints its operands and writes to standard output. +// +// Calling Print(x, y) is equivalent to +// fmt.Print(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Print(a ...interface{}) (n int, errno error) { + return fmt.Print(wrap(a, true)...) +} + +// Printf is a convenience wrapper for fmt.Printf. +// +// Calling Printf(f, x, y) is equivalent to +// fmt.Printf(f, Formatter(x), Formatter(y)). +func Printf(format string, a ...interface{}) (n int, errno error) { + return fmt.Printf(format, wrap(a, false)...) +} + +// Println pretty-prints its operands and writes to standard output. +// +// Calling Print(x, y) is equivalent to +// fmt.Println(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Println(a ...interface{}) (n int, errno error) { + return fmt.Println(wrap(a, true)...) +} + +// Sprintf is a convenience wrapper for fmt.Sprintf. +// +// Calling Sprintf(f, x, y) is equivalent to +// fmt.Sprintf(f, Formatter(x), Formatter(y)). +func Sprintf(format string, a ...interface{}) string { + return fmt.Sprintf(format, wrap(a, false)...) +} + +func wrap(a []interface{}, force bool) []interface{} { + w := make([]interface{}, len(a)) + for i, x := range a { + w[i] = formatter{x: x, force: force} + } + return w +} diff --git a/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/zero.go b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/zero.go new file mode 100644 index 0000000000..abb5b6fc14 --- /dev/null +++ b/Godeps/_workspace/src/github.com/tonnerre/golang-pretty/zero.go @@ -0,0 +1,41 @@ +package pretty + +import ( + "reflect" +) + +func nonzero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Bool: + return v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() != 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() != 0 + case reflect.Float32, reflect.Float64: + return v.Float() != 0 + case reflect.Complex64, reflect.Complex128: + return v.Complex() != complex(0, 0) + case reflect.String: + return v.String() != "" + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + if nonzero(getField(v, i)) { + return true + } + } + return false + case reflect.Array: + for i := 0; i < v.Len(); i++ { + if nonzero(v.Index(i)) { + return true + } + } + return false + case reflect.Map, reflect.Interface, reflect.Slice, reflect.Ptr, reflect.Chan, reflect.Func: + return !v.IsNil() + case reflect.UnsafePointer: + return v.Pointer() != 0 + } + return true +} diff --git a/cmd/apiserver/plugins.go b/cmd/apiserver/plugins.go index 1e92d034ea..64908e509a 100644 --- a/cmd/apiserver/plugins.go +++ b/cmd/apiserver/plugins.go @@ -22,6 +22,7 @@ package main import ( _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/gce" + _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/openstack" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/ovirt" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/vagrant" ) diff --git a/pkg/cloudprovider/openstack/MAINTAINERS.md b/pkg/cloudprovider/openstack/MAINTAINERS.md new file mode 100644 index 0000000000..9e09608e16 --- /dev/null +++ b/pkg/cloudprovider/openstack/MAINTAINERS.md @@ -0,0 +1,3 @@ +# Maintainers + +* [Angus Lees](https://github.com/anguslees) diff --git a/pkg/cloudprovider/openstack/openstack.go b/pkg/cloudprovider/openstack/openstack.go new file mode 100644 index 0000000000..b1615bee39 --- /dev/null +++ b/pkg/cloudprovider/openstack/openstack.go @@ -0,0 +1,217 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 openstack + +import ( + "errors" + "fmt" + "io" + "net" + "net/url" + "regexp" + + "code.google.com/p/gcfg" + "github.com/rackspace/gophercloud" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +var ErrServerNotFound = errors.New("Server not found") +var ErrMultipleServersFound = errors.New("Multiple servers matched query") +var ErrFlavorNotFound = errors.New("Flavor not found") + +// OpenStack is an implementation of cloud provider Interface for OpenStack. +type OpenStack struct { + provider string + authOpt gophercloud.AuthOptions + region string + access *gophercloud.Access +} + +type Config struct { + Global struct { + AuthUrl string + Username, Password string + ApiKey string + TenantId, TenantName string + Region string + } +} + +func init() { + cloudprovider.RegisterCloudProvider("openstack", func(config io.Reader) (cloudprovider.Interface, error) { + cfg, err := readConfig(config) + if err != nil { + return nil, err + } + return newOpenStack(cfg) + }) +} + +func (cfg Config) toAuthOptions() gophercloud.AuthOptions { + return gophercloud.AuthOptions{ + Username: cfg.Global.Username, + Password: cfg.Global.Password, + ApiKey: cfg.Global.ApiKey, + TenantId: cfg.Global.TenantId, + TenantName: cfg.Global.TenantName, + + // Persistent service, so we need to be able to renew tokens + AllowReauth: true, + } +} + +func readConfig(config io.Reader) (Config, error) { + if config == nil { + err := fmt.Errorf("No OpenStack cloud provider config file given") + return Config{}, err + } + + var cfg Config + err := gcfg.ReadInto(&cfg, config) + return cfg, err +} + +func newOpenStack(cfg Config) (*OpenStack, error) { + os := OpenStack{ + provider: cfg.Global.AuthUrl, + authOpt: cfg.toAuthOptions(), + region: cfg.Global.Region, + } + + access, err := gophercloud.Authenticate(os.provider, os.authOpt) + os.access = access + + return &os, err +} + +type Instances struct { + servers gophercloud.CloudServersProvider + flavor_to_resource map[string]*api.NodeResources // keyed by flavor id +} + +// Instances returns an implementation of Instances for OpenStack. +func (os *OpenStack) Instances() (cloudprovider.Instances, bool) { + servers, err := gophercloud.ServersApi(os.access, gophercloud.ApiCriteria{ + Type: "compute", + UrlChoice: gophercloud.PublicURL, + Region: os.region, + }) + + if err != nil { + return nil, false + } + + flavors, err := servers.ListFlavors() + if err != nil { + return nil, false + } + flavor_to_resource := make(map[string]*api.NodeResources, len(flavors)) + for _, flavor := range flavors { + rsrc := api.NodeResources{ + Capacity: api.ResourceList{ + "cpu": util.NewIntOrStringFromInt(flavor.VCpus), + "memory": util.NewIntOrStringFromString(fmt.Sprintf("%dMiB", flavor.Ram)), + "openstack.org/disk": util.NewIntOrStringFromString(fmt.Sprintf("%dGB", flavor.Disk)), + "openstack.org/rxTxFactor": util.NewIntOrStringFromInt(int(flavor.RxTxFactor * 1000)), + "openstack.org/swap": util.NewIntOrStringFromString(fmt.Sprintf("%dMiB", flavor.Swap)), + }, + } + flavor_to_resource[flavor.Id] = &rsrc + } + + return &Instances{servers, flavor_to_resource}, true +} + +func (i *Instances) List(name_filter string) ([]string, error) { + filter := url.Values{} + filter.Set("name", name_filter) + filter.Set("status", "ACTIVE") + + servers, err := i.servers.ListServersByFilter(filter) + if err != nil { + return nil, err + } + + ret := make([]string, len(servers)) + for idx, srv := range servers { + ret[idx] = srv.Name + } + return ret, nil +} + +func getServerByName(api gophercloud.CloudServersProvider, name string) (*gophercloud.Server, error) { + filter := url.Values{} + filter.Set("name", fmt.Sprintf("^%s$", regexp.QuoteMeta(name))) + filter.Set("status", "ACTIVE") + + servers, err := api.ListServersByFilter(filter) + if err != nil { + return nil, err + } + + if len(servers) == 0 { + return nil, ErrServerNotFound + } else if len(servers) > 1 { + return nil, ErrMultipleServersFound + } + + return &servers[0], nil +} + +func (i *Instances) IPAddress(name string) (net.IP, error) { + srv, err := getServerByName(i.servers, name) + if err != nil { + return nil, err + } + + var s string + if len(srv.Addresses.Private) > 0 { + s = srv.Addresses.Private[0].Addr + } else if len(srv.Addresses.Public) > 0 { + s = srv.Addresses.Public[0].Addr + } else if srv.AccessIPv4 != "" { + s = srv.AccessIPv4 + } else { + s = srv.AccessIPv6 + } + return net.ParseIP(s), nil +} + +func (i *Instances) GetNodeResources(name string) (*api.NodeResources, error) { + srv, err := getServerByName(i.servers, name) + if err != nil { + return nil, err + } + + rsrc, ok := i.flavor_to_resource[srv.Flavor.Id] + if !ok { + return nil, ErrFlavorNotFound + } + + return rsrc, nil +} + +func (os *OpenStack) TCPLoadBalancer() (cloudprovider.TCPLoadBalancer, bool) { + return nil, false +} + +func (os *OpenStack) Zones() (cloudprovider.Zones, bool) { + return nil, false +} diff --git a/pkg/cloudprovider/openstack/openstack_test.go b/pkg/cloudprovider/openstack/openstack_test.go new file mode 100644 index 0000000000..eada1794b7 --- /dev/null +++ b/pkg/cloudprovider/openstack/openstack_test.go @@ -0,0 +1,135 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 openstack + +import ( + "os" + "strings" + "testing" +) + +func TestReadConfig(t *testing.T) { + _, err := readConfig(nil) + if err == nil { + t.Errorf("Should fail when no config is provided: %s", err) + } + + cfg, err := readConfig(strings.NewReader(` +[Global] +authurl = http://auth.url +username = user +`)) + if err != nil { + t.Fatalf("Should succeed when a valid config is provided: %s", err) + } + if cfg.Global.AuthUrl != "http://auth.url" { + t.Errorf("incorrect authurl: %s", cfg.Global.AuthUrl) + } +} + +func TestToAuthOptions(t *testing.T) { + cfg := Config{} + cfg.Global.Username = "user" + // etc. + + ao := cfg.toAuthOptions() + + if !ao.AllowReauth { + t.Errorf("Will need to be able to reauthenticate") + } + if ao.Username != cfg.Global.Username { + t.Errorf("Username %s != %s", ao.Username, cfg.Global.Username) + } +} + +// This allows testing against an existing OpenStack install, using the +// standard OS_* OpenStack client environment variables. +func configFromEnv() (cfg Config, ok bool) { + cfg.Global.AuthUrl = os.Getenv("OS_AUTH_URL") + // gophercloud wants "provider" to point specifically at tokens URL + if !strings.HasSuffix(cfg.Global.AuthUrl, "/tokens") { + cfg.Global.AuthUrl += "/tokens" + } + + cfg.Global.TenantId = os.Getenv("OS_TENANT_ID") + // Rax/nova _insists_ that we don't specify both tenant ID and name + if cfg.Global.TenantId == "" { + cfg.Global.TenantName = os.Getenv("OS_TENANT_NAME") + } + + cfg.Global.Username = os.Getenv("OS_USERNAME") + cfg.Global.Password = os.Getenv("OS_PASSWORD") + cfg.Global.ApiKey = os.Getenv("OS_API_KEY") + cfg.Global.Region = os.Getenv("OS_REGION_NAME") + + ok = (cfg.Global.AuthUrl != "" && + cfg.Global.Username != "" && + (cfg.Global.Password != "" || cfg.Global.ApiKey != "") && + (cfg.Global.TenantId != "" || cfg.Global.TenantName != "")) + + return +} + +func TestNewOpenStack(t *testing.T) { + cfg, ok := configFromEnv() + if !ok { + t.Skipf("No config found in environment") + } + + _, err := newOpenStack(cfg) + if err != nil { + t.Fatalf("Failed to construct/authenticate OpenStack: %s", err) + } +} + +func TestInstances(t *testing.T) { + cfg, ok := configFromEnv() + if !ok { + t.Skipf("No config found in environment") + } + + os, err := newOpenStack(cfg) + if err != nil { + t.Fatalf("Failed to construct/authenticate OpenStack: %s", err) + } + + i, ok := os.Instances() + if !ok { + t.Fatalf("Instances() returned false") + } + + srvs, err := i.List(".") + if err != nil { + t.Fatalf("Instances.List() failed: %s", err) + } + if len(srvs) == 0 { + t.Fatalf("Instances.List() returned zero servers") + } + t.Logf("Found servers (%d): %s\n", len(srvs), srvs) + + ip, err := i.IPAddress(srvs[0]) + if err != nil { + t.Fatalf("Instances.IPAddress(%s) failed: %s", srvs[0], err) + } + t.Logf("Found IPAddress(%s) = %s\n", srvs[0], ip) + + rsrcs, err := i.GetNodeResources(srvs[0]) + if err != nil { + t.Fatalf("Instances.GetNodeResources(%s) failed: %s", srvs[0], err) + } + t.Logf("Found GetNodeResources(%s) = %s\n", srvs[0], rsrcs) +}