Merge pull request #18596 from hashicorp/svh/f-remote-backend

backend/remote: add a new backend for storing state and running operations remotely
This commit is contained in:
Sander van Harmelen 2018-08-03 22:49:01 +02:00 committed by GitHub
commit f4da82a023
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
103 changed files with 9955 additions and 415 deletions

View File

@ -39,59 +39,14 @@ type Backend struct {
// schema is the schema for configuration, set by init
schema *schema.Backend
once sync.Once
// opLock locks operations
opLock sync.Mutex
}
func (b *Backend) Input(
ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
b.once.Do(b.init)
return b.schema.Input(ui, c)
}
func (b *Backend) Validate(c *terraform.ResourceConfig) ([]string, []error) {
b.once.Do(b.init)
return b.schema.Validate(c)
}
func (b *Backend) Configure(c *terraform.ResourceConfig) error {
b.once.Do(b.init)
return b.schema.Configure(c)
}
func (b *Backend) States() ([]string, error) {
return nil, backend.ErrNamedStatesNotSupported
}
func (b *Backend) DeleteState(name string) error {
return backend.ErrNamedStatesNotSupported
}
func (b *Backend) State(name string) (state.State, error) {
if name != backend.DefaultStateName {
return nil, backend.ErrNamedStatesNotSupported
}
return &remote.State{Client: b.stateClient}, nil
}
// Colorize returns the Colorize structure that can be used for colorizing
// output. This is gauranteed to always return a non-nil value and so is useful
// as a helper to wrap any potentially colored strings.
func (b *Backend) Colorize() *colorstring.Colorize {
if b.CLIColor != nil {
return b.CLIColor
}
return &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: true,
}
}
func (b *Backend) init() {
// New returns a new initialized Atlas backend.
func New() *Backend {
b := &Backend{}
b.schema = &schema.Backend{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
@ -115,11 +70,13 @@ func (b *Backend) init() {
},
},
ConfigureFunc: b.schemaConfigure,
ConfigureFunc: b.configure,
}
return b
}
func (b *Backend) schemaConfigure(ctx context.Context) error {
func (b *Backend) configure(ctx context.Context) error {
d := schema.FromContextBackendConfig(ctx)
// Parse the address
@ -153,6 +110,47 @@ func (b *Backend) schemaConfigure(ctx context.Context) error {
return nil
}
func (b *Backend) Input(ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
return b.schema.Input(ui, c)
}
func (b *Backend) Validate(c *terraform.ResourceConfig) ([]string, []error) {
return b.schema.Validate(c)
}
func (b *Backend) Configure(c *terraform.ResourceConfig) error {
return b.schema.Configure(c)
}
func (b *Backend) State(name string) (state.State, error) {
if name != backend.DefaultStateName {
return nil, backend.ErrNamedStatesNotSupported
}
return &remote.State{Client: b.stateClient}, nil
}
func (b *Backend) DeleteState(name string) error {
return backend.ErrNamedStatesNotSupported
}
func (b *Backend) States() ([]string, error) {
return nil, backend.ErrNamedStatesNotSupported
}
// Colorize returns the Colorize structure that can be used for colorizing
// output. This is gauranteed to always return a non-nil value and so is useful
// as a helper to wrap any potentially colored strings.
func (b *Backend) Colorize() *colorstring.Colorize {
if b.CLIColor != nil {
return b.CLIColor
}
return &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: true,
}
}
var schemaDescriptions = map[string]string{
"name": "Full name of the environment in Atlas, such as 'hashicorp/myenv'",
"access_token": "Access token to use to access Atlas. If ATLAS_TOKEN is set then\n" +

View File

@ -18,7 +18,7 @@ func TestConfigure_envAddr(t *testing.T) {
defer os.Setenv("ATLAS_ADDRESS", os.Getenv("ATLAS_ADDRESS"))
os.Setenv("ATLAS_ADDRESS", "http://foo.com")
b := &Backend{}
b := New()
err := b.Configure(terraform.NewResourceConfig(config.TestRawConfig(t, map[string]interface{}{
"name": "foo/bar",
})))
@ -35,7 +35,7 @@ func TestConfigure_envToken(t *testing.T) {
defer os.Setenv("ATLAS_TOKEN", os.Getenv("ATLAS_TOKEN"))
os.Setenv("ATLAS_TOKEN", "foo")
b := &Backend{}
b := New()
err := b.Configure(terraform.NewResourceConfig(config.TestRawConfig(t, map[string]interface{}{
"name": "foo/bar",
})))

View File

@ -20,7 +20,7 @@ import (
)
func testStateClient(t *testing.T, c map[string]interface{}) remote.Client {
b := backend.TestBackendConfig(t, &Backend{}, c)
b := backend.TestBackendConfig(t, New(), c)
raw, err := b.State(backend.DefaultStateName)
if err != nil {
t.Fatalf("err: %s", err)

View File

@ -15,14 +15,29 @@ import (
"github.com/hashicorp/terraform/terraform"
)
// This is the name of the default, initial state that every backend
// must have. This state cannot be deleted.
// DefaultStateName is the name of the default, initial state that every
// backend must have. This state cannot be deleted.
const DefaultStateName = "default"
// Error value to return when a named state operation isn't supported.
// This must be returned rather than a custom error so that the Terraform
// CLI can detect it and handle it appropriately.
var ErrNamedStatesNotSupported = errors.New("named states not supported")
var (
// ErrNamedStatesNotSupported is returned when a named state operation
// isn't supported.
ErrNamedStatesNotSupported = errors.New("named states not supported")
// ErrDefaultStateNotSupported is returned when an operation does not support
// using the default state, but requires a named state to be selected.
ErrDefaultStateNotSupported = errors.New("default state not supported\n\n" +
"You can create a new workspace wth the \"workspace new\" command")
// ErrOperationNotSupported is returned when an unsupported operation
// is detected by the configured backend.
ErrOperationNotSupported = errors.New("operation not supported")
)
// InitFn is used to initialize a new backend.
type InitFn func() Backend
// Backend is the minimal interface that must be implemented to enable Terraform.
type Backend interface {

View File

@ -22,7 +22,7 @@ import (
type CLI interface {
Backend
// CLIIinit is called once with options. The options passed to this
// CLIInit is called once with options. The options passed to this
// function may not be modified after calling this since they can be
// read/written at any time by the Backend implementation.
//

View File

@ -3,19 +3,22 @@
package init
import (
"os"
"sync"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/svchost/disco"
"github.com/hashicorp/terraform/terraform"
backendatlas "github.com/hashicorp/terraform/backend/atlas"
backendlegacy "github.com/hashicorp/terraform/backend/legacy"
backendlocal "github.com/hashicorp/terraform/backend/local"
backendAtlas "github.com/hashicorp/terraform/backend/atlas"
backendLegacy "github.com/hashicorp/terraform/backend/legacy"
backendLocal "github.com/hashicorp/terraform/backend/local"
backendRemote "github.com/hashicorp/terraform/backend/remote"
backendAzure "github.com/hashicorp/terraform/backend/remote-state/azure"
backendconsul "github.com/hashicorp/terraform/backend/remote-state/consul"
backendetcdv3 "github.com/hashicorp/terraform/backend/remote-state/etcdv3"
backendConsul "github.com/hashicorp/terraform/backend/remote-state/consul"
backendEtcdv3 "github.com/hashicorp/terraform/backend/remote-state/etcdv3"
backendGCS "github.com/hashicorp/terraform/backend/remote-state/gcs"
backendinmem "github.com/hashicorp/terraform/backend/remote-state/inmem"
backendInmem "github.com/hashicorp/terraform/backend/remote-state/inmem"
backendManta "github.com/hashicorp/terraform/backend/remote-state/manta"
backendS3 "github.com/hashicorp/terraform/backend/remote-state/s3"
backendSwift "github.com/hashicorp/terraform/backend/remote-state/swift"
@ -32,35 +35,49 @@ import (
// complex structures and supporting that over the plugin system is currently
// prohibitively difficult. For those wanting to implement a custom backend,
// they can do so with recompilation.
var backends map[string]func() backend.Backend
var backends map[string]backend.InitFn
var backendsLock sync.Mutex
func init() {
// Our hardcoded backends. We don't need to acquire a lock here
// since init() code is serial and can't spawn goroutines.
backends = map[string]func() backend.Backend{
"atlas": func() backend.Backend { return &backendatlas.Backend{} },
"local": func() backend.Backend { return &backendlocal.Local{} },
"consul": func() backend.Backend { return backendconsul.New() },
"inmem": func() backend.Backend { return backendinmem.New() },
"swift": func() backend.Backend { return backendSwift.New() },
// Init initializes the backends map with all our hardcoded backends.
func Init(services *disco.Disco) {
backendsLock.Lock()
defer backendsLock.Unlock()
backends = map[string]backend.InitFn{
// Enhanced backends.
"local": func() backend.Backend { return backendLocal.New() },
"remote": func() backend.Backend {
b := backendRemote.New(services)
if os.Getenv("TF_FORCE_LOCAL_BACKEND") != "" {
return backendLocal.NewWithBackend(b)
}
return b
},
// Remote State backends.
"atlas": func() backend.Backend { return backendAtlas.New() },
"azurerm": func() backend.Backend { return backendAzure.New() },
"consul": func() backend.Backend { return backendConsul.New() },
"etcdv3": func() backend.Backend { return backendEtcdv3.New() },
"gcs": func() backend.Backend { return backendGCS.New() },
"inmem": func() backend.Backend { return backendInmem.New() },
"manta": func() backend.Backend { return backendManta.New() },
"s3": func() backend.Backend { return backendS3.New() },
"swift": func() backend.Backend { return backendSwift.New() },
// Deprecated backends.
"azure": deprecateBackend(backendAzure.New(),
`Warning: "azure" name is deprecated, please use "azurerm"`),
"azurerm": func() backend.Backend { return backendAzure.New() },
"etcdv3": func() backend.Backend { return backendetcdv3.New() },
"gcs": func() backend.Backend { return backendGCS.New() },
"manta": func() backend.Backend { return backendManta.New() },
}
// Add the legacy remote backends that haven't yet been convertd to
// Add the legacy remote backends that haven't yet been converted to
// the new backend API.
backendlegacy.Init(backends)
backendLegacy.Init(backends)
}
// Backend returns the initialization factory for the given backend, or
// nil if none exists.
func Backend(name string) func() backend.Backend {
func Backend(name string) backend.InitFn {
backendsLock.Lock()
defer backendsLock.Unlock()
return backends[name]
@ -73,7 +90,7 @@ func Backend(name string) func() backend.Backend {
// This method sets this backend globally and care should be taken to do
// this only before Terraform is executing to prevent odd behavior of backends
// changing mid-execution.
func Set(name string, f func() backend.Backend) {
func Set(name string, f backend.InitFn) {
backendsLock.Lock()
defer backendsLock.Unlock()
@ -101,7 +118,7 @@ func (b deprecatedBackendShim) Validate(c *terraform.ResourceConfig) ([]string,
// DeprecateBackend can be used to wrap a backend to retrun a deprecation
// warning during validation.
func deprecateBackend(b backend.Backend, message string) func() backend.Backend {
func deprecateBackend(b backend.Backend, message string) backend.InitFn {
// Since a Backend wrapped by deprecatedBackendShim can no longer be
// asserted as an Enhanced or Local backend, disallow those types here
// entirely. If something other than a basic backend.Backend needs to be

110
backend/init/init_test.go Normal file
View File

@ -0,0 +1,110 @@
package init
import (
"os"
"reflect"
"testing"
backendLocal "github.com/hashicorp/terraform/backend/local"
)
func TestInit_backend(t *testing.T) {
// Initialize the backends map
Init(nil)
backends := []struct {
Name string
Type string
}{
{
"local",
"*local.Local",
}, {
"remote",
"*remote.Remote",
}, {
"atlas",
"*atlas.Backend",
}, {
"azurerm",
"*azure.Backend",
}, {
"consul",
"*consul.Backend",
}, {
"etcdv3",
"*etcd.Backend",
}, {
"gcs",
"*gcs.Backend",
}, {
"inmem",
"*inmem.Backend",
}, {
"manta",
"*manta.Backend",
}, {
"s3",
"*s3.Backend",
}, {
"swift",
"*swift.Backend",
}, {
"azure",
"init.deprecatedBackendShim",
},
}
// Make sure we get the requested backend
for _, b := range backends {
f := Backend(b.Name)
bType := reflect.TypeOf(f()).String()
if bType != b.Type {
t.Fatalf("expected backend %q to be %q, got: %q", b.Name, b.Type, bType)
}
}
}
func TestInit_forceLocalBackend(t *testing.T) {
// Initialize the backends map
Init(nil)
enhancedBackends := []struct {
Name string
Type string
}{
{
"local",
"nil",
}, {
"remote",
"*remote.Remote",
},
}
// Set the TF_FORCE_LOCAL_BACKEND flag so all enhanced backends will
// return a local.Local backend with themselves as embedded backend.
if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil {
t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err)
}
// Make sure we always get the local backend.
for _, b := range enhancedBackends {
f := Backend(b.Name)
local, ok := f().(*backendLocal.Local)
if !ok {
t.Fatalf("expected backend %q to be \"*local.Local\", got: %T", b.Name, f())
}
bType := "nil"
if local.Backend != nil {
bType = reflect.TypeOf(local.Backend).String()
}
if bType != b.Type {
t.Fatalf("expected local.Backend to be %s, got: %s", b.Type, bType)
}
}
}

View File

@ -12,8 +12,8 @@ import (
//
// If a type is already in the map, it will not be added. This will allow
// us to slowly convert the legacy types to first-class backends.
func Init(m map[string]func() backend.Backend) {
for k, _ := range remote.BuiltinClients {
func Init(m map[string]backend.InitFn) {
for k := range remote.BuiltinClients {
if _, ok := m[k]; !ok {
// Copy the "k" value since the variable "k" is reused for
// each key (address doesn't change).

View File

@ -8,7 +8,7 @@ import (
)
func TestInit(t *testing.T) {
m := make(map[string]func() backend.Backend)
m := make(map[string]backend.InitFn)
Init(m)
for k, _ := range remote.BuiltinClients {
@ -24,7 +24,7 @@ func TestInit(t *testing.T) {
}
func TestInit_ignoreExisting(t *testing.T) {
m := make(map[string]func() backend.Backend)
m := make(map[string]backend.InitFn)
m["local"] = nil
Init(m)

View File

@ -91,94 +91,106 @@ type Local struct {
schema *schema.Backend
opLock sync.Mutex
once sync.Once
}
func (b *Local) Input(
ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
b.once.Do(b.init)
// New returns a new initialized local backend.
func New() *Local {
return NewWithBackend(nil)
}
// NewWithBackend returns a new local backend initialized with a
// dedicated backend for non-enhanced behavior.
func NewWithBackend(backend backend.Backend) *Local {
b := &Local{
Backend: backend,
}
b.schema = &schema.Backend{
Schema: map[string]*schema.Schema{
"path": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
},
"workspace_dir": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
},
"environment_dir": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
ConflictsWith: []string{"workspace_dir"},
Deprecated: "workspace_dir should be used instead, with the same meaning",
},
},
ConfigureFunc: b.configure,
}
return b
}
func (b *Local) configure(ctx context.Context) error {
d := schema.FromContextBackendConfig(ctx)
// Set the path if it is set
pathRaw, ok := d.GetOk("path")
if ok {
path := pathRaw.(string)
if path == "" {
return fmt.Errorf("configured path is empty")
}
b.StatePath = path
b.StateOutPath = path
}
if raw, ok := d.GetOk("workspace_dir"); ok {
path := raw.(string)
if path != "" {
b.StateWorkspaceDir = path
}
}
// Legacy name, which ConflictsWith workspace_dir
if raw, ok := d.GetOk("environment_dir"); ok {
path := raw.(string)
if path != "" {
b.StateWorkspaceDir = path
}
}
return nil
}
func (b *Local) Input(ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
f := b.schema.Input
if b.Backend != nil {
f = b.Backend.Input
}
return f(ui, c)
}
func (b *Local) Validate(c *terraform.ResourceConfig) ([]string, []error) {
b.once.Do(b.init)
f := b.schema.Validate
if b.Backend != nil {
f = b.Backend.Validate
}
return f(c)
}
func (b *Local) Configure(c *terraform.ResourceConfig) error {
b.once.Do(b.init)
f := b.schema.Configure
if b.Backend != nil {
f = b.Backend.Configure
}
return f(c)
}
func (b *Local) States() ([]string, error) {
// If we have a backend handling state, defer to that.
if b.Backend != nil {
return b.Backend.States()
}
// the listing always start with "default"
envs := []string{backend.DefaultStateName}
entries, err := ioutil.ReadDir(b.stateWorkspaceDir())
// no error if there's no envs configured
if os.IsNotExist(err) {
return envs, nil
}
if err != nil {
return nil, err
}
var listed []string
for _, entry := range entries {
if entry.IsDir() {
listed = append(listed, filepath.Base(entry.Name()))
}
}
sort.Strings(listed)
envs = append(envs, listed...)
return envs, nil
}
// DeleteState removes a named state.
// The "default" state cannot be removed.
func (b *Local) DeleteState(name string) error {
// If we have a backend handling state, defer to that.
if b.Backend != nil {
return b.Backend.DeleteState(name)
}
if name == "" {
return errors.New("empty state name")
}
if name == backend.DefaultStateName {
return errors.New("cannot delete default state")
}
delete(b.states, name)
return os.RemoveAll(filepath.Join(b.stateWorkspaceDir(), name))
}
func (b *Local) State(name string) (state.State, error) {
statePath, stateOutPath, backupPath := b.StatePaths(name)
@ -216,6 +228,57 @@ func (b *Local) State(name string) (state.State, error) {
return s, nil
}
// DeleteState removes a named state.
// The "default" state cannot be removed.
func (b *Local) DeleteState(name string) error {
// If we have a backend handling state, defer to that.
if b.Backend != nil {
return b.Backend.DeleteState(name)
}
if name == "" {
return errors.New("empty state name")
}
if name == backend.DefaultStateName {
return errors.New("cannot delete default state")
}
delete(b.states, name)
return os.RemoveAll(filepath.Join(b.stateWorkspaceDir(), name))
}
func (b *Local) States() ([]string, error) {
// If we have a backend handling state, defer to that.
if b.Backend != nil {
return b.Backend.States()
}
// the listing always start with "default"
envs := []string{backend.DefaultStateName}
entries, err := ioutil.ReadDir(b.stateWorkspaceDir())
// no error if there's no envs configured
if os.IsNotExist(err) {
return envs, nil
}
if err != nil {
return nil, err
}
var listed []string
for _, entry := range entries {
if entry.IsDir() {
listed = append(listed, filepath.Base(entry.Name()))
}
}
sort.Strings(listed)
envs = append(envs, listed...)
return envs, nil
}
// Operation implements backend.Enhanced
//
// This will initialize an in-memory terraform.Context to perform the
@ -348,68 +411,6 @@ func (b *Local) Colorize() *colorstring.Colorize {
}
}
func (b *Local) init() {
b.schema = &schema.Backend{
Schema: map[string]*schema.Schema{
"path": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
},
"workspace_dir": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
},
"environment_dir": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
ConflictsWith: []string{"workspace_dir"},
Deprecated: "workspace_dir should be used instead, with the same meaning",
},
},
ConfigureFunc: b.schemaConfigure,
}
}
func (b *Local) schemaConfigure(ctx context.Context) error {
d := schema.FromContextBackendConfig(ctx)
// Set the path if it is set
pathRaw, ok := d.GetOk("path")
if ok {
path := pathRaw.(string)
if path == "" {
return fmt.Errorf("configured path is empty")
}
b.StatePath = path
b.StateOutPath = path
}
if raw, ok := d.GetOk("workspace_dir"); ok {
path := raw.(string)
if path != "" {
b.StateWorkspaceDir = path
}
}
// Legacy name, which ConflictsWith workspace_dir
if raw, ok := d.GetOk("environment_dir"); ok {
path := raw.(string)
if path != "" {
b.StateWorkspaceDir = path
}
}
return nil
}
// StatePaths returns the StatePath, StateOutPath, and StateBackupPath as
// configured from the CLI.
func (b *Local) StatePaths(name string) (string, string, string) {

View File

@ -5,15 +5,13 @@ import (
"errors"
"log"
"github.com/hashicorp/terraform/command/clistate"
"github.com/hashicorp/terraform/command/format"
"github.com/hashicorp/terraform/tfdiags"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/command/clistate"
"github.com/hashicorp/terraform/command/format"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
)
// backend.Local implementation.

View File

@ -15,14 +15,14 @@ import (
)
func TestLocal_impl(t *testing.T) {
var _ backend.Enhanced = new(Local)
var _ backend.Local = new(Local)
var _ backend.CLI = new(Local)
var _ backend.Enhanced = New()
var _ backend.Local = New()
var _ backend.CLI = New()
}
func TestLocal_backend(t *testing.T) {
defer testTmpDir(t)()
b := &Local{}
b := New()
backend.TestBackendStates(t, b)
backend.TestBackendStateLocks(t, b, b)
}
@ -49,7 +49,7 @@ func checkState(t *testing.T, path, expected string) {
}
func TestLocal_StatePaths(t *testing.T) {
b := &Local{}
b := New()
// Test the defaults
path, out, back := b.StatePaths("")
@ -94,7 +94,7 @@ func TestLocal_addAndRemoveStates(t *testing.T) {
dflt := backend.DefaultStateName
expectedStates := []string{dflt}
b := &Local{}
b := New()
states, err := b.States()
if err != nil {
t.Fatal(err)
@ -210,13 +210,11 @@ func (b *testDelegateBackend) DeleteState(name string) error {
// verify that the MultiState methods are dispatched to the correct Backend.
func TestLocal_multiStateBackend(t *testing.T) {
// assign a separate backend where we can read the state
b := &Local{
Backend: &testDelegateBackend{
b := NewWithBackend(&testDelegateBackend{
stateErr: true,
statesErr: true,
deleteErr: true,
},
}
})
if _, err := b.State("test"); err != errTestDelegateState {
t.Fatal("expected errTestDelegateState, got:", err)

View File

@ -18,13 +18,14 @@ import (
// public fields without any locks.
func TestLocal(t *testing.T) (*Local, func()) {
tempDir := testTempDir(t)
local := &Local{
StatePath: filepath.Join(tempDir, "state.tfstate"),
StateOutPath: filepath.Join(tempDir, "state.tfstate"),
StateBackupPath: filepath.Join(tempDir, "state.tfstate.bak"),
StateWorkspaceDir: filepath.Join(tempDir, "state.tfstate.d"),
ContextOpts: &terraform.ContextOpts{},
}
local := New()
local.StatePath = filepath.Join(tempDir, "state.tfstate")
local.StateOutPath = filepath.Join(tempDir, "state.tfstate")
local.StateBackupPath = filepath.Join(tempDir, "state.tfstate.bak")
local.StateWorkspaceDir = filepath.Join(tempDir, "state.tfstate.d")
local.ContextOpts = &terraform.ContextOpts{}
cleanup := func() {
if err := os.RemoveAll(tempDir); err != nil {
t.Fatal("error clecanup up test:", err)
@ -69,7 +70,7 @@ func TestLocalProvider(t *testing.T, b *Local, name string) *terraform.MockResou
// TestNewLocalSingle is a factory for creating a TestLocalSingleState.
// This function matches the signature required for backend/init.
func TestNewLocalSingle() backend.Backend {
return &TestLocalSingleState{}
return &TestLocalSingleState{Local: New()}
}
// TestLocalSingleState is a backend implementation that wraps Local
@ -79,7 +80,7 @@ func TestNewLocalSingle() backend.Backend {
// This isn't an actual use case, this is exported just to provide a
// easy way to test that behavior.
type TestLocalSingleState struct {
Local
*Local
}
func (b *TestLocalSingleState) State(name string) (state.State, error) {
@ -98,6 +99,50 @@ func (b *TestLocalSingleState) DeleteState(string) error {
return backend.ErrNamedStatesNotSupported
}
// TestNewLocalNoDefault is a factory for creating a TestLocalNoDefaultState.
// This function matches the signature required for backend/init.
func TestNewLocalNoDefault() backend.Backend {
return &TestLocalNoDefaultState{Local: New()}
}
// TestLocalNoDefaultState is a backend implementation that wraps
// Local and modifies it to support named states, but not the
// default state. It returns ErrDefaultStateNotSupported when the
// DefaultStateName is used.
type TestLocalNoDefaultState struct {
*Local
}
func (b *TestLocalNoDefaultState) State(name string) (state.State, error) {
if name == backend.DefaultStateName {
return nil, backend.ErrDefaultStateNotSupported
}
return b.Local.State(name)
}
func (b *TestLocalNoDefaultState) States() ([]string, error) {
states, err := b.Local.States()
if err != nil {
return nil, err
}
filtered := states[:0]
for _, name := range states {
if name != backend.DefaultStateName {
filtered = append(filtered, name)
}
}
return filtered, nil
}
func (b *TestLocalNoDefaultState) DeleteState(name string) error {
if name == backend.DefaultStateName {
return backend.ErrDefaultStateNotSupported
}
return b.Local.DeleteState(name)
}
func testTempDir(t *testing.T) string {
d, err := ioutil.TempDir("", "tf")
if err != nil {

View File

@ -18,10 +18,10 @@ import (
"google.golang.org/api/option"
)
// gcsBackend implements "backend".Backend for GCS.
// Backend implements "backend".Backend for GCS.
// Input(), Validate() and Configure() are implemented by embedding *schema.Backend.
// State(), DeleteState() and States() are implemented explicitly.
type gcsBackend struct {
type Backend struct {
*schema.Backend
storageClient *storage.Client
@ -38,9 +38,9 @@ type gcsBackend struct {
}
func New() backend.Backend {
be := &gcsBackend{}
be.Backend = &schema.Backend{
ConfigureFunc: be.configure,
b := &Backend{}
b.Backend = &schema.Backend{
ConfigureFunc: b.configure,
Schema: map[string]*schema.Schema{
"bucket": {
Type: schema.TypeString,
@ -91,10 +91,10 @@ func New() backend.Backend {
},
}
return be
return b
}
func (b *gcsBackend) configure(ctx context.Context) error {
func (b *Backend) configure(ctx context.Context) error {
if b.storageClient != nil {
return nil
}

View File

@ -21,7 +21,7 @@ const (
// States returns a list of names for the states found on GCS. The default
// state is always returned as the first element in the slice.
func (b *gcsBackend) States() ([]string, error) {
func (b *Backend) States() ([]string, error) {
states := []string{backend.DefaultStateName}
bucket := b.storageClient.Bucket(b.bucketName)
@ -54,7 +54,7 @@ func (b *gcsBackend) States() ([]string, error) {
}
// DeleteState deletes the named state. The "default" state cannot be deleted.
func (b *gcsBackend) DeleteState(name string) error {
func (b *Backend) DeleteState(name string) error {
if name == backend.DefaultStateName {
return fmt.Errorf("cowardly refusing to delete the %q state", name)
}
@ -68,7 +68,7 @@ func (b *gcsBackend) DeleteState(name string) error {
}
// client returns a remoteClient for the named state.
func (b *gcsBackend) client(name string) (*remoteClient, error) {
func (b *Backend) client(name string) (*remoteClient, error) {
if name == "" {
return nil, fmt.Errorf("%q is not a valid state name", name)
}
@ -85,7 +85,7 @@ func (b *gcsBackend) client(name string) (*remoteClient, error) {
// State reads and returns the named state from GCS. If the named state does
// not yet exist, a new state file is created.
func (b *gcsBackend) State(name string) (state.State, error) {
func (b *Backend) State(name string) (state.State, error) {
c, err := b.client(name)
if err != nil {
return nil, err
@ -144,14 +144,14 @@ func (b *gcsBackend) State(name string) (state.State, error) {
return st, nil
}
func (b *gcsBackend) stateFile(name string) string {
func (b *Backend) stateFile(name string) string {
if name == backend.DefaultStateName && b.defaultStateFile != "" {
return b.defaultStateFile
}
return path.Join(b.prefix, name+stateFileSuffix)
}
func (b *gcsBackend) lockFile(name string) string {
func (b *Backend) lockFile(name string) string {
if name == backend.DefaultStateName && b.defaultStateFile != "" {
return strings.TrimSuffix(b.defaultStateFile, stateFileSuffix) + lockFileSuffix
}

View File

@ -39,7 +39,7 @@ func TestStateFile(t *testing.T) {
{"state", "legacy.state", "test", "state/test.tfstate", "state/test.tflock"},
}
for _, c := range cases {
b := &gcsBackend{
b := &Backend{
prefix: c.prefix,
defaultStateFile: c.defaultStateFile,
}
@ -188,7 +188,7 @@ func setupBackend(t *testing.T, bucket, prefix, key string) backend.Backend {
}
b := backend.TestBackendConfig(t, New(), config)
be := b.(*gcsBackend)
be := b.(*Backend)
// create the bucket if it doesn't exist
bkt := be.storageClient.Bucket(bucket)
@ -213,7 +213,7 @@ func setupBackend(t *testing.T, bucket, prefix, key string) backend.Backend {
// teardownBackend deletes all states from be except the default state.
func teardownBackend(t *testing.T, be backend.Backend, prefix string) {
t.Helper()
gcsBE, ok := be.(*gcsBackend)
gcsBE, ok := be.(*Backend)
if !ok {
t.Fatalf("be is a %T, want a *gcsBackend", be)
}

453
backend/remote/backend.go Normal file
View File

@ -0,0 +1,453 @@
package remote
import (
"context"
"fmt"
"log"
"net/url"
"sort"
"strings"
"sync"
tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/svchost"
"github.com/hashicorp/terraform/svchost/disco"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/version"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
)
const (
defaultHostname = "app.terraform.io"
serviceID = "tfe.v2"
)
// Remote is an implementation of EnhancedBackend that performs all
// operations in a remote backend.
type Remote struct {
// CLI and Colorize control the CLI output. If CLI is nil then no CLI
// output will be done. If CLIColor is nil then no coloring will be done.
CLI cli.Ui
CLIColor *colorstring.Colorize
// ContextOpts are the base context options to set when initializing a
// new Terraform context. Many of these will be overridden or merged by
// Operation. See Operation for more details.
ContextOpts *terraform.ContextOpts
// client is the remote backend API client
client *tfe.Client
// hostname of the remote backend server
hostname string
// organization is the organization that contains the target workspaces
organization string
// workspace is used to map the default workspace to a remote workspace
workspace string
// prefix is used to filter down a set of workspaces that use a single
// configuration
prefix string
// schema defines the configuration for the backend
schema *schema.Backend
// services is used for service discovery
services *disco.Disco
// opLock locks operations
opLock sync.Mutex
}
// New creates a new initialized remote backend.
func New(services *disco.Disco) *Remote {
b := &Remote{
services: services,
}
b.schema = &schema.Backend{
Schema: map[string]*schema.Schema{
"hostname": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: schemaDescriptions["hostname"],
Default: defaultHostname,
},
"organization": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: schemaDescriptions["organization"],
},
"token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: schemaDescriptions["token"],
DefaultFunc: schema.EnvDefaultFunc("TFE_TOKEN", ""),
},
"workspaces": &schema.Schema{
Type: schema.TypeSet,
Required: true,
Description: schemaDescriptions["workspaces"],
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: schemaDescriptions["name"],
},
"prefix": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: schemaDescriptions["prefix"],
},
},
},
},
},
ConfigureFunc: b.configure,
}
return b
}
func (b *Remote) configure(ctx context.Context) error {
d := schema.FromContextBackendConfig(ctx)
// Get the hostname and organization.
b.hostname = d.Get("hostname").(string)
b.organization = d.Get("organization").(string)
// Get the workspaces configuration.
workspaces := d.Get("workspaces").(*schema.Set)
if workspaces.Len() != 1 {
return fmt.Errorf("only one 'workspaces' block allowed")
}
// After checking that we have exactly one workspace block, we can now get
// and assert that one workspace from the set.
workspace := workspaces.List()[0].(map[string]interface{})
// Get the default workspace name and prefix.
b.workspace = workspace["name"].(string)
b.prefix = workspace["prefix"].(string)
// Make sure that we have either a workspace name or a prefix.
if b.workspace == "" && b.prefix == "" {
return fmt.Errorf("either workspace 'name' or 'prefix' is required")
}
// Make sure that only one of workspace name or a prefix is configured.
if b.workspace != "" && b.prefix != "" {
return fmt.Errorf("only one of workspace 'name' or 'prefix' is allowed")
}
// Discover the service URL for this host to confirm that it provides
// a remote backend API and to discover the required base path.
service, err := b.discover(b.hostname)
if err != nil {
return err
}
// Retrieve the token for this host as configured in the credentials
// section of the CLI Config File.
token, err := b.token(b.hostname)
if err != nil {
return err
}
if token == "" {
token = d.Get("token").(string)
}
cfg := &tfe.Config{
Address: service.String(),
BasePath: service.Path,
Token: token,
}
// Create the remote backend API client.
b.client, err = tfe.NewClient(cfg)
if err != nil {
return err
}
return nil
}
// discover the remote backend API service URL and token.
func (b *Remote) discover(hostname string) (*url.URL, error) {
host, err := svchost.ForComparison(hostname)
if err != nil {
return nil, err
}
service := b.services.DiscoverServiceURL(host, serviceID)
if service == nil {
return nil, fmt.Errorf("host %s does not provide a remote backend API", host)
}
return service, nil
}
// token returns the token for this host as configured in the credentials
// section of the CLI Config File. If no token was configured, an empty
// string will be returned instead.
func (b *Remote) token(hostname string) (string, error) {
host, err := svchost.ForComparison(hostname)
if err != nil {
return "", err
}
creds, err := b.services.CredentialsForHost(host)
if err != nil {
log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err)
return "", nil
}
if creds != nil {
return creds.Token(), nil
}
return "", nil
}
// Input is called to ask the user for input for completing the configuration.
func (b *Remote) Input(ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
return b.schema.Input(ui, c)
}
// Validate is called once at the beginning with the raw configuration and
// can return a list of warnings and/or errors.
func (b *Remote) Validate(c *terraform.ResourceConfig) ([]string, []error) {
return b.schema.Validate(c)
}
// Configure configures the backend itself with the configuration given.
func (b *Remote) Configure(c *terraform.ResourceConfig) error {
return b.schema.Configure(c)
}
// State returns the latest state of the given remote workspace. The workspace
// will be created if it doesn't exist.
func (b *Remote) State(workspace string) (state.State, error) {
if b.workspace == "" && workspace == backend.DefaultStateName {
return nil, backend.ErrDefaultStateNotSupported
}
if b.prefix == "" && workspace != backend.DefaultStateName {
return nil, backend.ErrNamedStatesNotSupported
}
workspaces, err := b.states()
if err != nil {
return nil, fmt.Errorf("Error retrieving workspaces: %v", err)
}
exists := false
for _, name := range workspaces {
if workspace == name {
exists = true
break
}
}
// Configure the remote workspace name.
if workspace == backend.DefaultStateName {
workspace = b.workspace
} else if b.prefix != "" && !strings.HasPrefix(workspace, b.prefix) {
workspace = b.prefix + workspace
}
if !exists {
options := tfe.WorkspaceCreateOptions{
Name: tfe.String(workspace),
TerraformVersion: tfe.String(version.Version),
}
_, err = b.client.Workspaces.Create(context.Background(), b.organization, options)
if err != nil {
return nil, fmt.Errorf("Error creating workspace %s: %v", workspace, err)
}
}
client := &remoteClient{
client: b.client,
organization: b.organization,
workspace: workspace,
}
return &remote.State{Client: client}, nil
}
// DeleteState removes the remote workspace if it exists.
func (b *Remote) DeleteState(workspace string) error {
if b.workspace == "" && workspace == backend.DefaultStateName {
return backend.ErrDefaultStateNotSupported
}
if b.prefix == "" && workspace != backend.DefaultStateName {
return backend.ErrNamedStatesNotSupported
}
// Configure the remote workspace name.
if workspace == backend.DefaultStateName {
workspace = b.workspace
} else if b.prefix != "" && !strings.HasPrefix(workspace, b.prefix) {
workspace = b.prefix + workspace
}
// Check if the configured organization exists.
_, err := b.client.Organizations.Read(context.Background(), b.organization)
if err != nil {
if err == tfe.ErrResourceNotFound {
return fmt.Errorf("organization %s does not exist", b.organization)
}
return err
}
client := &remoteClient{
client: b.client,
organization: b.organization,
workspace: workspace,
}
return client.Delete()
}
// States returns a filtered list of remote workspace names.
func (b *Remote) States() ([]string, error) {
if b.prefix == "" {
return nil, backend.ErrNamedStatesNotSupported
}
return b.states()
}
func (b *Remote) states() ([]string, error) {
// Check if the configured organization exists.
_, err := b.client.Organizations.Read(context.Background(), b.organization)
if err != nil {
if err == tfe.ErrResourceNotFound {
return nil, fmt.Errorf("organization %s does not exist", b.organization)
}
return nil, err
}
options := tfe.WorkspaceListOptions{}
ws, err := b.client.Workspaces.List(context.Background(), b.organization, options)
if err != nil {
return nil, err
}
var names []string
for _, w := range ws {
if b.workspace != "" && w.Name == b.workspace {
names = append(names, backend.DefaultStateName)
continue
}
if b.prefix != "" && strings.HasPrefix(w.Name, b.prefix) {
names = append(names, strings.TrimPrefix(w.Name, b.prefix))
}
}
// Sort the result so we have consistent output.
sort.StringSlice(names).Sort()
return names, nil
}
// Operation implements backend.Enhanced
func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
// Configure the remote workspace name.
if op.Workspace == backend.DefaultStateName {
op.Workspace = b.workspace
} else if b.prefix != "" && !strings.HasPrefix(op.Workspace, b.prefix) {
op.Workspace = b.prefix + op.Workspace
}
// Determine the function to call for our operation
var f func(context.Context, context.Context, *backend.Operation, *backend.RunningOperation)
switch op.Type {
case backend.OperationTypePlan:
f = b.opPlan
default:
return nil, fmt.Errorf(
"\n\nThe \"remote\" backend currently only supports the \"plan\" operation.\n"+
"Please use the remote backend web UI for all other operations:\n"+
"https://%s/app/%s/%s", b.hostname, b.organization, op.Workspace)
// return nil, backend.ErrOperationNotSupported
}
// Lock
b.opLock.Lock()
// Build our running operation
// the runninCtx is only used to block until the operation returns.
runningCtx, done := context.WithCancel(context.Background())
runningOp := &backend.RunningOperation{
Context: runningCtx,
}
// stopCtx wraps the context passed in, and is used to signal a graceful Stop.
stopCtx, stop := context.WithCancel(ctx)
runningOp.Stop = stop
// cancelCtx is used to cancel the operation immediately, usually
// indicating that the process is exiting.
cancelCtx, cancel := context.WithCancel(context.Background())
runningOp.Cancel = cancel
// Do it
go func() {
defer done()
defer stop()
defer cancel()
defer b.opLock.Unlock()
f(stopCtx, cancelCtx, op, runningOp)
}()
// Return
return runningOp, nil
}
// Colorize returns the Colorize structure that can be used for colorizing
// output. This is gauranteed to always return a non-nil value and so is useful
// as a helper to wrap any potentially colored strings.
func (b *Remote) Colorize() *colorstring.Colorize {
if b.CLIColor != nil {
return b.CLIColor
}
return &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: true,
}
}
const generalErr = `
%s: %v
The "remote" backend encountered an unexpected error while communicating
with remote backend. In some cases this could be caused by a network
connection problem, in which case you could retry the command. If the issue
persists please open a support ticket to get help resolving the problem.
`
var schemaDescriptions = map[string]string{
"hostname": "The remote backend hostname to connect to (defaults to app.terraform.io).",
"organization": "The name of the organization containing the targeted workspace(s).",
"token": "The token used to authenticate with the remote backend. If TFE_TOKEN is set\n" +
"or credentials for the host are configured in the CLI Config File, then this\n" +
"this will override any saved value for this.",
"workspaces": "Workspaces contains arguments used to filter down to a set of workspaces\n" +
"to work on.",
"name": "A workspace name used to map the default workspace to a named remote workspace.\n" +
"When configured only the default workspace can be used. This option conflicts\n" +
"with \"prefix\"",
"prefix": "A prefix used to filter workspaces using a single configuration. New workspaces\n" +
"will automatically be prefixed with this prefix. If omitted only the default\n" +
"workspace can be used. This option conflicts with \"name\"",
}

View File

@ -0,0 +1,384 @@
package remote
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"io/ioutil"
"math/rand"
tfe "github.com/hashicorp/go-tfe"
)
type mockConfigurationVersions struct {
configVersions map[string]*tfe.ConfigurationVersion
uploadURLs map[string]*tfe.ConfigurationVersion
workspaces map[string]*tfe.ConfigurationVersion
}
func newMockConfigurationVersions() *mockConfigurationVersions {
return &mockConfigurationVersions{
configVersions: make(map[string]*tfe.ConfigurationVersion),
uploadURLs: make(map[string]*tfe.ConfigurationVersion),
workspaces: make(map[string]*tfe.ConfigurationVersion),
}
}
func (m *mockConfigurationVersions) List(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionListOptions) ([]*tfe.ConfigurationVersion, error) {
var cvs []*tfe.ConfigurationVersion
for _, cv := range m.configVersions {
cvs = append(cvs, cv)
}
return cvs, nil
}
func (m *mockConfigurationVersions) Create(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionCreateOptions) (*tfe.ConfigurationVersion, error) {
id := generateID("cv-")
url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
cv := &tfe.ConfigurationVersion{
ID: id,
Status: tfe.ConfigurationPending,
UploadURL: url,
}
m.configVersions[cv.ID] = cv
m.uploadURLs[url] = cv
m.workspaces[workspaceID] = cv
return cv, nil
}
func (m *mockConfigurationVersions) Read(ctx context.Context, cvID string) (*tfe.ConfigurationVersion, error) {
cv, ok := m.configVersions[cvID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
return cv, nil
}
func (m *mockConfigurationVersions) Upload(ctx context.Context, url, path string) error {
cv, ok := m.uploadURLs[url]
if !ok {
return errors.New("404 not found")
}
cv.Status = tfe.ConfigurationUploaded
return nil
}
type mockOrganizations struct {
organizations map[string]*tfe.Organization
}
func newMockOrganizations() *mockOrganizations {
return &mockOrganizations{
organizations: make(map[string]*tfe.Organization),
}
}
func (m *mockOrganizations) List(ctx context.Context, options tfe.OrganizationListOptions) ([]*tfe.Organization, error) {
var orgs []*tfe.Organization
for _, org := range m.organizations {
orgs = append(orgs, org)
}
return orgs, nil
}
func (m *mockOrganizations) Create(ctx context.Context, options tfe.OrganizationCreateOptions) (*tfe.Organization, error) {
org := &tfe.Organization{Name: *options.Name}
m.organizations[org.Name] = org
return org, nil
}
func (m *mockOrganizations) Read(ctx context.Context, name string) (*tfe.Organization, error) {
org, ok := m.organizations[name]
if !ok {
return nil, tfe.ErrResourceNotFound
}
return org, nil
}
func (m *mockOrganizations) Update(ctx context.Context, name string, options tfe.OrganizationUpdateOptions) (*tfe.Organization, error) {
org, ok := m.organizations[name]
if !ok {
return nil, tfe.ErrResourceNotFound
}
org.Name = *options.Name
return org, nil
}
func (m *mockOrganizations) Delete(ctx context.Context, name string) error {
delete(m.organizations, name)
return nil
}
type mockPlans struct {
logs map[string]string
plans map[string]*tfe.Plan
}
func newMockPlans() *mockPlans {
return &mockPlans{
logs: make(map[string]string),
plans: make(map[string]*tfe.Plan),
}
}
func (m *mockPlans) Read(ctx context.Context, planID string) (*tfe.Plan, error) {
p, ok := m.plans[planID]
if !ok {
url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", planID)
p = &tfe.Plan{
ID: planID,
LogReadURL: url,
Status: tfe.PlanFinished,
}
m.logs[url] = "plan/output.log"
m.plans[p.ID] = p
}
return p, nil
}
func (m *mockPlans) Logs(ctx context.Context, planID string) (io.Reader, error) {
p, err := m.Read(ctx, planID)
if err != nil {
return nil, err
}
logfile, ok := m.logs[p.LogReadURL]
if !ok {
return nil, tfe.ErrResourceNotFound
}
logs, err := ioutil.ReadFile("./test-fixtures/" + logfile)
if err != nil {
return nil, err
}
return bytes.NewBuffer(logs), nil
}
type mockRuns struct {
runs map[string]*tfe.Run
workspaces map[string][]*tfe.Run
}
func newMockRuns() *mockRuns {
return &mockRuns{
runs: make(map[string]*tfe.Run),
workspaces: make(map[string][]*tfe.Run),
}
}
func (m *mockRuns) List(ctx context.Context, workspaceID string, options tfe.RunListOptions) ([]*tfe.Run, error) {
var rs []*tfe.Run
for _, r := range m.workspaces[workspaceID] {
rs = append(rs, r)
}
return rs, nil
}
func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) {
id := generateID("run-")
p := &tfe.Plan{
ID: generateID("plan-"),
Status: tfe.PlanPending,
}
r := &tfe.Run{
ID: id,
Plan: p,
Status: tfe.RunPending,
}
m.runs[r.ID] = r
m.workspaces[options.Workspace.ID] = append(m.workspaces[options.Workspace.ID], r)
return r, nil
}
func (m *mockRuns) Read(ctx context.Context, runID string) (*tfe.Run, error) {
r, ok := m.runs[runID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
return r, nil
}
func (m *mockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error {
panic("not implemented")
}
func (m *mockRuns) Cancel(ctx context.Context, runID string, options tfe.RunCancelOptions) error {
panic("not implemented")
}
func (m *mockRuns) Discard(ctx context.Context, runID string, options tfe.RunDiscardOptions) error {
panic("not implemented")
}
type mockStateVersions struct {
states map[string][]byte
stateVersions map[string]*tfe.StateVersion
workspaces map[string][]string
}
func newMockStateVersions() *mockStateVersions {
return &mockStateVersions{
states: make(map[string][]byte),
stateVersions: make(map[string]*tfe.StateVersion),
workspaces: make(map[string][]string),
}
}
func (m *mockStateVersions) List(ctx context.Context, options tfe.StateVersionListOptions) ([]*tfe.StateVersion, error) {
var svs []*tfe.StateVersion
for _, sv := range m.stateVersions {
svs = append(svs, sv)
}
return svs, nil
}
func (m *mockStateVersions) Create(ctx context.Context, workspaceID string, options tfe.StateVersionCreateOptions) (*tfe.StateVersion, error) {
id := generateID("sv-")
url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
sv := &tfe.StateVersion{
ID: id,
DownloadURL: url,
Serial: *options.Serial,
}
state, err := base64.StdEncoding.DecodeString(*options.State)
if err != nil {
return nil, err
}
m.states[sv.DownloadURL] = state
m.stateVersions[sv.ID] = sv
m.workspaces[workspaceID] = append(m.workspaces[workspaceID], sv.ID)
return sv, nil
}
func (m *mockStateVersions) Read(ctx context.Context, svID string) (*tfe.StateVersion, error) {
sv, ok := m.stateVersions[svID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
return sv, nil
}
func (m *mockStateVersions) Current(ctx context.Context, workspaceID string) (*tfe.StateVersion, error) {
svs, ok := m.workspaces[workspaceID]
if !ok || len(svs) == 0 {
return nil, tfe.ErrResourceNotFound
}
sv, ok := m.stateVersions[svs[len(svs)-1]]
if !ok {
return nil, tfe.ErrResourceNotFound
}
return sv, nil
}
func (m *mockStateVersions) Download(ctx context.Context, url string) ([]byte, error) {
state, ok := m.states[url]
if !ok {
return nil, tfe.ErrResourceNotFound
}
return state, nil
}
type mockWorkspaces struct {
workspaceIDs map[string]*tfe.Workspace
workspaceNames map[string]*tfe.Workspace
}
func newMockWorkspaces() *mockWorkspaces {
return &mockWorkspaces{
workspaceIDs: make(map[string]*tfe.Workspace),
workspaceNames: make(map[string]*tfe.Workspace),
}
}
func (m *mockWorkspaces) List(ctx context.Context, organization string, options tfe.WorkspaceListOptions) ([]*tfe.Workspace, error) {
var ws []*tfe.Workspace
for _, w := range m.workspaceIDs {
ws = append(ws, w)
}
return ws, nil
}
func (m *mockWorkspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) {
id := generateID("ws-")
w := &tfe.Workspace{
ID: id,
Name: *options.Name,
}
m.workspaceIDs[w.ID] = w
m.workspaceNames[w.Name] = w
return w, nil
}
func (m *mockWorkspaces) Read(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) {
w, ok := m.workspaceNames[workspace]
if !ok {
return nil, tfe.ErrResourceNotFound
}
return w, nil
}
func (m *mockWorkspaces) Update(ctx context.Context, organization, workspace string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) {
w, ok := m.workspaceNames[workspace]
if !ok {
return nil, tfe.ErrResourceNotFound
}
w.Name = *options.Name
w.TerraformVersion = *options.TerraformVersion
delete(m.workspaceNames, workspace)
m.workspaceNames[w.Name] = w
return w, nil
}
func (m *mockWorkspaces) Delete(ctx context.Context, organization, workspace string) error {
if w, ok := m.workspaceNames[workspace]; ok {
delete(m.workspaceIDs, w.ID)
}
delete(m.workspaceNames, workspace)
return nil
}
func (m *mockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) {
panic("not implemented")
}
func (m *mockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
panic("not implemented")
}
func (m *mockWorkspaces) AssignSSHKey(ctx context.Context, workspaceID string, options tfe.WorkspaceAssignSSHKeyOptions) (*tfe.Workspace, error) {
panic("not implemented")
}
func (m *mockWorkspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
panic("not implemented")
}
const alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func generateID(s string) string {
b := make([]byte, 16)
for i := range b {
b[i] = alphanumeric[rand.Intn(len(alphanumeric))]
}
return s + string(b)
}

View File

@ -0,0 +1,206 @@
package remote
import (
"bufio"
"context"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strings"
"time"
tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform/backend"
)
func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation, runningOp *backend.RunningOperation) {
log.Printf("[INFO] backend/remote: starting Plan operation")
if op.Plan != nil {
runningOp.Err = fmt.Errorf(strings.TrimSpace(planErrPlanNotSupported))
return
}
if op.PlanOutPath != "" {
runningOp.Err = fmt.Errorf(strings.TrimSpace(planErrOutPathNotSupported))
return
}
if op.Targets != nil {
runningOp.Err = fmt.Errorf(strings.TrimSpace(planErrTargetsNotSupported))
return
}
if (op.Module == nil || op.Module.Config().Dir == "") && !op.Destroy {
runningOp.Err = fmt.Errorf(strings.TrimSpace(planErrNoConfig))
return
}
// Retrieve the workspace used to run this operation in.
w, err := b.client.Workspaces.Read(stopCtx, b.organization, op.Workspace)
if err != nil {
if err != context.Canceled {
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
generalErr, "error retrieving workspace", err)))
}
return
}
configOptions := tfe.ConfigurationVersionCreateOptions{
AutoQueueRuns: tfe.Bool(false),
Speculative: tfe.Bool(true),
}
cv, err := b.client.ConfigurationVersions.Create(stopCtx, w.ID, configOptions)
if err != nil {
if err != context.Canceled {
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
generalErr, "error creating configuration version", err)))
}
return
}
var configDir string
if op.Module != nil && op.Module.Config().Dir != "" {
configDir = op.Module.Config().Dir
} else {
configDir, err = ioutil.TempDir("", "tf")
if err != nil {
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
generalErr, "error creating temp directory", err)))
return
}
defer os.RemoveAll(configDir)
}
err = b.client.ConfigurationVersions.Upload(stopCtx, cv.UploadURL, configDir)
if err != nil {
if err != context.Canceled {
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
generalErr, "error uploading configuration files", err)))
}
return
}
uploaded := false
for i := 0; i < 60 && !uploaded; i++ {
select {
case <-stopCtx.Done():
return
case <-cancelCtx.Done():
return
case <-time.After(500 * time.Millisecond):
cv, err = b.client.ConfigurationVersions.Read(stopCtx, cv.ID)
if err != nil {
if err != context.Canceled {
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
generalErr, "error retrieving configuration version", err)))
}
return
}
if cv.Status == tfe.ConfigurationUploaded {
uploaded = true
}
}
}
if !uploaded {
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
generalErr, "error uploading configuration files", "operation timed out")))
return
}
runOptions := tfe.RunCreateOptions{
IsDestroy: tfe.Bool(op.Destroy),
Message: tfe.String("Queued manually using Terraform"),
ConfigurationVersion: cv,
Workspace: w,
}
r, err := b.client.Runs.Create(stopCtx, runOptions)
if err != nil {
if err != context.Canceled {
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
generalErr, "error creating run", err)))
}
return
}
r, err = b.client.Runs.Read(stopCtx, r.ID)
if err != nil {
if err != context.Canceled {
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
generalErr, "error retrieving run", err)))
}
return
}
if b.CLI != nil {
b.CLI.Output(b.Colorize().Color(strings.TrimSpace(fmt.Sprintf(
planDefaultHeader, b.hostname, b.organization, op.Workspace, r.ID)) + "\n"))
}
logs, err := b.client.Plans.Logs(stopCtx, r.Plan.ID)
if err != nil {
if err != context.Canceled {
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
generalErr, "error retrieving logs", err)))
}
return
}
scanner := bufio.NewScanner(logs)
for scanner.Scan() {
if b.CLI != nil {
b.CLI.Output(b.Colorize().Color(scanner.Text()))
}
}
if err := scanner.Err(); err != nil {
if err != context.Canceled && err != io.EOF {
runningOp.Err = fmt.Errorf("Error reading logs: %v", err)
}
return
}
}
const planErrPlanNotSupported = `
Displaying a saved plan is currently not supported!
The "remote" backend currently requires configuration to be present
and does not accept an existing saved plan as an argument at this time.
`
const planErrOutPathNotSupported = `
Saving a generated plan is currently not supported!
The "remote" backend does not support saving the generated execution
plan locally at this time.
`
const planErrTargetsNotSupported = `
Resource targeting is currently not supported!
The "remote" backend does not support resource targeting at this time.
`
const planErrNoConfig = `
No configuration files found!
Plan requires configuration to be present. Planning without a configuration
would mark everything for destruction, which is normally not what is desired.
If you would like to destroy everything, please run plan with the "-destroy"
flag or create a single empty configuration file. Otherwise, please create
a Terraform configuration file in the path being executed and try again.
`
const planDefaultHeader = `
[reset][yellow]Running plan in the remote backend. Output will stream here. Pressing Ctrl-C
will stop streaming the logs, but will not stop the plan running remotely.
To view this plan in a browser, visit:
https://%s/app/%s/%s/runs/%s[reset]
Waiting for the plan to start...
`

View File

@ -0,0 +1,181 @@
package remote
import (
"context"
"strings"
"testing"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
func testOperationPlan() *backend.Operation {
return &backend.Operation{
Type: backend.OperationTypePlan,
}
}
func TestRemote_planBasic(t *testing.T) {
b := testBackendDefault(t)
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
defer modCleanup()
op := testOperationPlan()
op.Module = mod
op.Workspace = backend.DefaultStateName
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("error starting operation: %v", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("error running operation: %v", run.Err)
}
output := b.CLI.(*cli.MockUi).OutputWriter.String()
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("missing plan summery in output: %s", output)
}
}
func TestRemote_planWithPlan(t *testing.T) {
b := testBackendDefault(t)
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
defer modCleanup()
op := testOperationPlan()
op.Module = mod
op.Plan = &terraform.Plan{}
op.Workspace = backend.DefaultStateName
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("error starting operation: %v", err)
}
<-run.Done()
if run.Err == nil {
t.Fatalf("expected a plan error, got: %v", run.Err)
}
if !strings.Contains(run.Err.Error(), "saved plan is currently not supported") {
t.Fatalf("expected a saved plan error, got: %v", run.Err)
}
}
func TestRemote_planWithPath(t *testing.T) {
b := testBackendDefault(t)
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
defer modCleanup()
op := testOperationPlan()
op.Module = mod
op.PlanOutPath = "./test-fixtures/plan"
op.Workspace = backend.DefaultStateName
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("error starting operation: %v", err)
}
<-run.Done()
if run.Err == nil {
t.Fatalf("expected a plan error, got: %v", run.Err)
}
if !strings.Contains(run.Err.Error(), "generated plan is currently not supported") {
t.Fatalf("expected a generated plan error, got: %v", run.Err)
}
}
func TestRemote_planWithTarget(t *testing.T) {
b := testBackendDefault(t)
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
defer modCleanup()
op := testOperationPlan()
op.Module = mod
op.Targets = []string{"null_resource.foo"}
op.Workspace = backend.DefaultStateName
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("error starting operation: %v", err)
}
<-run.Done()
if run.Err == nil {
t.Fatalf("expected a plan error, got: %v", run.Err)
}
if !strings.Contains(run.Err.Error(), "targeting is currently not supported") {
t.Fatalf("expected a targeting error, got: %v", run.Err)
}
}
func TestRemote_planNoConfig(t *testing.T) {
b := testBackendDefault(t)
op := testOperationPlan()
op.Module = nil
op.Workspace = backend.DefaultStateName
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("error starting operation: %v", err)
}
<-run.Done()
if run.Err == nil {
t.Fatalf("expected a plan error, got: %v", run.Err)
}
if !strings.Contains(run.Err.Error(), "configuration files found") {
t.Fatalf("expected configuration files error, got: %v", run.Err)
}
}
func TestRemote_planDestroy(t *testing.T) {
b := testBackendDefault(t)
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
defer modCleanup()
op := testOperationPlan()
op.Destroy = true
op.Module = mod
op.Workspace = backend.DefaultStateName
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("error starting operation: %v", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("unexpected plan error: %v", run.Err)
}
}
func TestRemote_planDestroyNoConfig(t *testing.T) {
b := testBackendDefault(t)
op := testOperationPlan()
op.Destroy = true
op.Module = nil
op.Workspace = backend.DefaultStateName
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("error starting operation: %v", err)
}
<-run.Done()
if run.Err != nil {
t.Fatalf("unexpected plan error: %v", run.Err)
}
}

View File

@ -0,0 +1,103 @@
package remote
import (
"bytes"
"context"
"crypto/md5"
"encoding/base64"
"fmt"
tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
)
type remoteClient struct {
client *tfe.Client
organization string
workspace string
}
// Get the remote state.
func (r *remoteClient) Get() (*remote.Payload, error) {
ctx := context.Background()
// Retrieve the workspace for which to create a new state.
w, err := r.client.Workspaces.Read(ctx, r.organization, r.workspace)
if err != nil {
if err == tfe.ErrResourceNotFound {
// If no state exists, then return nil.
return nil, nil
}
return nil, fmt.Errorf("Error retrieving workspace: %v", err)
}
sv, err := r.client.StateVersions.Current(ctx, w.ID)
if err != nil {
if err == tfe.ErrResourceNotFound {
// If no state exists, then return nil.
return nil, nil
}
return nil, fmt.Errorf("Error retrieving remote state: %v", err)
}
state, err := r.client.StateVersions.Download(ctx, sv.DownloadURL)
if err != nil {
return nil, fmt.Errorf("Error downloading remote state: %v", err)
}
// If the state is empty, then return nil.
if len(state) == 0 {
return nil, nil
}
// Get the MD5 checksum of the state.
sum := md5.Sum(state)
return &remote.Payload{
Data: state,
MD5: sum[:],
}, nil
}
// Put the remote state.
func (r *remoteClient) Put(state []byte) error {
ctx := context.Background()
// Retrieve the workspace for which to create a new state.
w, err := r.client.Workspaces.Read(ctx, r.organization, r.workspace)
if err != nil {
return fmt.Errorf("Error retrieving workspace: %v", err)
}
// the state into a buffer.
tfState, err := terraform.ReadState(bytes.NewReader(state))
if err != nil {
return fmt.Errorf("Error reading state: %s", err)
}
options := tfe.StateVersionCreateOptions{
Lineage: tfe.String(tfState.Lineage),
Serial: tfe.Int64(tfState.Serial),
MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))),
State: tfe.String(base64.StdEncoding.EncodeToString(state)),
}
// Create the new state.
_, err = r.client.StateVersions.Create(ctx, w.ID, options)
if err != nil {
return fmt.Errorf("Error creating remote state: %v", err)
}
return nil
}
// Delete the remote state.
func (r *remoteClient) Delete() error {
err := r.client.Workspaces.Delete(context.Background(), r.organization, r.workspace)
if err != nil && err != tfe.ErrResourceNotFound {
return fmt.Errorf("Error deleting workspace %s: %v", r.workspace, err)
}
return nil
}

View File

@ -0,0 +1,16 @@
package remote
import (
"testing"
"github.com/hashicorp/terraform/state/remote"
)
func TestRemoteClient_impl(t *testing.T) {
var _ remote.Client = new(remoteClient)
}
func TestRemoteClient(t *testing.T) {
client := testRemoteClient(t)
remote.TestClient(t, client)
}

View File

@ -0,0 +1,254 @@
package remote
import (
"errors"
"reflect"
"strings"
"testing"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
)
func TestRemote(t *testing.T) {
var _ backend.Enhanced = New(nil)
var _ backend.CLI = New(nil)
}
func TestRemote_config(t *testing.T) {
cases := map[string]struct {
config map[string]interface{}
err error
}{
"with_a_name": {
config: map[string]interface{}{
"organization": "hashicorp",
"workspaces": []interface{}{
map[string]interface{}{
"name": "prod",
},
},
},
err: nil,
},
"with_a_prefix": {
config: map[string]interface{}{
"organization": "hashicorp",
"workspaces": []interface{}{
map[string]interface{}{
"prefix": "my-app-",
},
},
},
err: nil,
},
"with_two_workspace_entries": {
config: map[string]interface{}{
"organization": "hashicorp",
"workspaces": []interface{}{
map[string]interface{}{
"name": "prod",
},
map[string]interface{}{
"prefix": "my-app-",
},
},
},
err: errors.New("only one 'workspaces' block allowed"),
},
"without_either_a_name_and_a_prefix": {
config: map[string]interface{}{
"organization": "hashicorp",
"workspaces": []interface{}{
map[string]interface{}{},
},
},
err: errors.New("either workspace 'name' or 'prefix' is required"),
},
"with_both_a_name_and_a_prefix": {
config: map[string]interface{}{
"organization": "hashicorp",
"workspaces": []interface{}{
map[string]interface{}{
"name": "prod",
"prefix": "my-app-",
},
},
},
err: errors.New("only one of workspace 'name' or 'prefix' is allowed"),
},
"with_an_unknown_host": {
config: map[string]interface{}{
"hostname": "nonexisting.local",
"organization": "hashicorp",
"workspaces": []interface{}{
map[string]interface{}{
"name": "prod",
},
},
},
err: errors.New("host nonexisting.local does not provide a remote backend API"),
},
}
for name, tc := range cases {
s := testServer(t)
b := New(testDisco(s))
// Get the proper config structure
rc, err := config.NewRawConfig(tc.config)
if err != nil {
t.Fatalf("%s: error creating raw config: %v", name, err)
}
conf := terraform.NewResourceConfig(rc)
// Validate
warns, errs := b.Validate(conf)
if len(warns) > 0 {
t.Fatalf("%s: validation warnings: %v", name, warns)
}
if len(errs) > 0 {
t.Fatalf("%s: validation errors: %v", name, errs)
}
// Configure
err = b.Configure(conf)
if err != tc.err && err != nil && tc.err != nil && err.Error() != tc.err.Error() {
t.Fatalf("%s: expected error %q, got: %q", name, tc.err, err)
}
}
}
func TestRemote_nonexistingOrganization(t *testing.T) {
msg := "does not exist"
b := testBackendNoDefault(t)
b.organization = "nonexisting"
if _, err := b.State("prod"); err == nil || !strings.Contains(err.Error(), msg) {
t.Fatalf("expected %q error, got: %v", msg, err)
}
if err := b.DeleteState("prod"); err == nil || !strings.Contains(err.Error(), msg) {
t.Fatalf("expected %q error, got: %v", msg, err)
}
if _, err := b.States(); err == nil || !strings.Contains(err.Error(), msg) {
t.Fatalf("expected %q error, got: %v", msg, err)
}
}
func TestRemote_backendDefault(t *testing.T) {
b := testBackendDefault(t)
backend.TestBackendStates(t, b)
backend.TestBackendStateLocks(t, b, b)
backend.TestBackendStateForceUnlock(t, b, b)
}
func TestRemote_backendNoDefault(t *testing.T) {
b := testBackendNoDefault(t)
backend.TestBackendStates(t, b)
}
func TestRemote_addAndRemoveStatesDefault(t *testing.T) {
b := testBackendDefault(t)
if _, err := b.States(); err != backend.ErrNamedStatesNotSupported {
t.Fatalf("expected error %v, got %v", backend.ErrNamedStatesNotSupported, err)
}
if _, err := b.State(backend.DefaultStateName); err != nil {
t.Fatalf("expected no error, got %v", err)
}
if _, err := b.State("prod"); err != backend.ErrNamedStatesNotSupported {
t.Fatalf("expected error %v, got %v", backend.ErrNamedStatesNotSupported, err)
}
if err := b.DeleteState(backend.DefaultStateName); err != nil {
t.Fatalf("expected no error, got %v", err)
}
if err := b.DeleteState("prod"); err != backend.ErrNamedStatesNotSupported {
t.Fatalf("expected error %v, got %v", backend.ErrNamedStatesNotSupported, err)
}
}
func TestRemote_addAndRemoveStatesNoDefault(t *testing.T) {
b := testBackendNoDefault(t)
states, err := b.States()
if err != nil {
t.Fatal(err)
}
expectedStates := []string(nil)
if !reflect.DeepEqual(states, expectedStates) {
t.Fatalf("expected states %#+v, got %#+v", expectedStates, states)
}
if _, err := b.State(backend.DefaultStateName); err != backend.ErrDefaultStateNotSupported {
t.Fatalf("expected error %v, got %v", backend.ErrDefaultStateNotSupported, err)
}
expectedA := "test_A"
if _, err := b.State(expectedA); err != nil {
t.Fatal(err)
}
states, err = b.States()
if err != nil {
t.Fatal(err)
}
expectedStates = append(expectedStates, expectedA)
if !reflect.DeepEqual(states, expectedStates) {
t.Fatalf("expected %#+v, got %#+v", expectedStates, states)
}
expectedB := "test_B"
if _, err := b.State(expectedB); err != nil {
t.Fatal(err)
}
states, err = b.States()
if err != nil {
t.Fatal(err)
}
expectedStates = append(expectedStates, expectedB)
if !reflect.DeepEqual(states, expectedStates) {
t.Fatalf("expected %#+v, got %#+v", expectedStates, states)
}
if err := b.DeleteState(backend.DefaultStateName); err != backend.ErrDefaultStateNotSupported {
t.Fatalf("expected error %v, got %v", backend.ErrDefaultStateNotSupported, err)
}
if err := b.DeleteState(expectedA); err != nil {
t.Fatal(err)
}
states, err = b.States()
if err != nil {
t.Fatal(err)
}
expectedStates = []string{expectedB}
if !reflect.DeepEqual(states, expectedStates) {
t.Fatalf("expected %#+v got %#+v", expectedStates, states)
}
if err := b.DeleteState(expectedB); err != nil {
t.Fatal(err)
}
states, err = b.States()
if err != nil {
t.Fatal(err)
}
expectedStates = []string(nil)
if !reflect.DeepEqual(states, expectedStates) {
t.Fatalf("expected %#+v, got %#+v", expectedStates, states)
}
}

13
backend/remote/cli.go Normal file
View File

@ -0,0 +1,13 @@
package remote
import (
"github.com/hashicorp/terraform/backend"
)
// CLIInit implements backend.CLI
func (b *Remote) CLIInit(opts *backend.CLIOpts) error {
b.CLI = opts.CLI
b.CLIColor = opts.CLIColor
b.ContextOpts = opts.ContextOpts
return nil
}

View File

@ -0,0 +1,10 @@
resource "test_instance" "foo" {
count = 3
ami = "bar"
# This is here because at some point it caused a test failure
network_interface {
device_index = 0
description = "Main network interface"
}
}

View File

@ -0,0 +1 @@
resource "null_resource" "foo" {}

View File

@ -0,0 +1,29 @@
Running plan in the remote backend. Output will stream here. Pressing Ctrl-C
will stop streaming the logs, but will not stop the plan running remotely.
To view this plan in a browser, visit:
https://atlas.local/app/demo1/my-app-web/runs/run-cPK6EnfTpqwy6ucU
Waiting for the plan to start...
Terraform v0.11.7
Configuring remote state backend...
Initializing Terraform configuration...
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
+ null_resource.foo
id: <computed>
Plan: 1 to add, 0 to change, 0 to destroy.

128
backend/remote/testing.go Normal file
View File

@ -0,0 +1,128 @@
package remote
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/svchost"
"github.com/hashicorp/terraform/svchost/auth"
"github.com/hashicorp/terraform/svchost/disco"
"github.com/mitchellh/cli"
)
const (
testCred = "test-auth-token"
)
var (
tfeHost = svchost.Hostname(defaultHostname)
credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{
tfeHost: {"token": testCred},
})
)
func testBackendDefault(t *testing.T) *Remote {
c := map[string]interface{}{
"organization": "hashicorp",
"workspaces": []interface{}{
map[string]interface{}{
"name": "prod",
},
},
}
return testBackend(t, c)
}
func testBackendNoDefault(t *testing.T) *Remote {
c := map[string]interface{}{
"organization": "hashicorp",
"workspaces": []interface{}{
map[string]interface{}{
"prefix": "my-app-",
},
},
}
return testBackend(t, c)
}
func testRemoteClient(t *testing.T) remote.Client {
b := testBackendDefault(t)
raw, err := b.State(backend.DefaultStateName)
if err != nil {
t.Fatalf("error: %v", err)
}
s := raw.(*remote.State)
return s.Client
}
func testBackend(t *testing.T, c map[string]interface{}) *Remote {
s := testServer(t)
b := New(testDisco(s))
// Configure the backend so the client is created.
backend.TestBackendConfig(t, b, c)
// Once the client exists, mock the services we use..
b.CLI = cli.NewMockUi()
b.client.ConfigurationVersions = newMockConfigurationVersions()
b.client.Organizations = newMockOrganizations()
b.client.Plans = newMockPlans()
b.client.Runs = newMockRuns()
b.client.StateVersions = newMockStateVersions()
b.client.Workspaces = newMockWorkspaces()
ctx := context.Background()
// Create the organization.
_, err := b.client.Organizations.Create(ctx, tfe.OrganizationCreateOptions{
Name: tfe.String(b.organization),
})
if err != nil {
t.Fatalf("error: %v", err)
}
// Create the default workspace if required.
if b.workspace != "" {
_, err = b.client.Workspaces.Create(ctx, b.organization, tfe.WorkspaceCreateOptions{
Name: tfe.String(b.workspace),
})
if err != nil {
t.Fatalf("error: %v", err)
}
}
return b
}
// testServer returns a *httptest.Server used for local testing.
func testServer(t *testing.T) *httptest.Server {
mux := http.NewServeMux()
// Respond to service discovery calls.
mux.HandleFunc("/well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
io.WriteString(w, `{"tfe.v2":"/api/v2/"}`)
})
return httptest.NewServer(mux)
}
// testDisco returns a *disco.Disco mapping app.terraform.io and
// localhost to a local test server.
func testDisco(s *httptest.Server) *disco.Disco {
services := map[string]interface{}{
"tfe.v2": fmt.Sprintf("%s/api/v2/", s.URL),
}
d := disco.NewWithCredentialsSource(credsSrc)
d.ForceHostServices(svchost.Hostname(defaultHostname), services)
d.ForceHostServices(svchost.Hostname("localhost"), services)
return d
}

View File

@ -47,15 +47,27 @@ func TestBackendConfig(t *testing.T, b Backend, c map[string]interface{}) Backen
func TestBackendStates(t *testing.T, b Backend) {
t.Helper()
noDefault := false
if _, err := b.State(DefaultStateName); err != nil {
if err == ErrDefaultStateNotSupported {
noDefault = true
} else {
t.Fatalf("error: %v", err)
}
}
states, err := b.States()
if err != nil {
if err == ErrNamedStatesNotSupported {
t.Logf("TestBackend: named states not supported in %T, skipping", b)
return
}
t.Fatalf("error: %v", err)
}
// Test it starts with only the default
if len(states) != 1 || states[0] != DefaultStateName {
t.Fatalf("should only have default to start: %#v", states)
if !noDefault && (len(states) != 1 || states[0] != DefaultStateName) {
t.Fatalf("should have default to start: %#v", states)
}
// Create a couple states
@ -175,6 +187,9 @@ func TestBackendStates(t *testing.T, b Backend) {
sort.Strings(states)
expected := []string{"bar", "default", "foo"}
if noDefault {
expected = []string{"bar", "foo"}
}
if !reflect.DeepEqual(states, expected) {
t.Fatalf("bad: %#v", states)
}
@ -218,6 +233,9 @@ func TestBackendStates(t *testing.T, b Backend) {
sort.Strings(states)
expected := []string{"bar", "default"}
if noDefault {
expected = []string{"bar"}
}
if !reflect.DeepEqual(states, expected) {
t.Fatalf("bad: %#v", states)
}

View File

@ -6,7 +6,7 @@ import (
"time"
"github.com/hashicorp/terraform/backend"
backendinit "github.com/hashicorp/terraform/backend/init"
backendInit "github.com/hashicorp/terraform/backend/init"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
@ -80,7 +80,7 @@ func dataSourceRemoteStateRead(d *schema.ResourceData, meta interface{}) error {
// Create the client to access our remote state
log.Printf("[DEBUG] Initializing remote state backend: %s", backendType)
f := backendinit.Backend(backendType)
f := backendInit.Backend(backendType)
if f == nil {
return fmt.Errorf("Unknown backend type: %s", backendType)
}

View File

@ -4,7 +4,7 @@ import (
"fmt"
"testing"
backendinit "github.com/hashicorp/terraform/backend/init"
backendInit "github.com/hashicorp/terraform/backend/init"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
@ -26,8 +26,8 @@ func TestState_basic(t *testing.T) {
}
func TestState_backends(t *testing.T) {
backendinit.Set("_ds_test", backendinit.Backend("local"))
defer backendinit.Set("_ds_test", nil)
backendInit.Set("_ds_test", backendInit.Backend("local"))
defer backendInit.Set("_ds_test", nil)
resource.UnitTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },

View File

@ -3,6 +3,7 @@ package terraform
import (
"testing"
backendInit "github.com/hashicorp/terraform/backend/init"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
@ -11,6 +12,9 @@ var testAccProviders map[string]terraform.ResourceProvider
var testAccProvider *schema.Provider
func init() {
// Initialize the backends
backendInit.Init(nil)
testAccProvider = Provider().(*schema.Provider)
testAccProviders = map[string]terraform.ResourceProvider{
"terraform": testAccProvider,

View File

@ -48,10 +48,6 @@ The "backend" in Terraform defines how Terraform operates. The default
backend performs all operations locally on your machine. Your configuration
is configured to use a non-local backend. This backend doesn't support this
operation.
If you want to use the state from the backend but force all other data
(configuration, variables, etc.) to come locally, you can force local
behavior with the "-local" flag.
`
// ModulePath returns the path to the root module from the CLI args.

View File

@ -19,6 +19,7 @@ import (
"syscall"
"testing"
backendInit "github.com/hashicorp/terraform/backend/init"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/helper/logging"
"github.com/hashicorp/terraform/terraform"
@ -33,6 +34,9 @@ var testingDir string
func init() {
test = true
// Initialize the backends
backendInit.Init(nil)
// Expand the fixture dir on init because we change the working
// directory in some tests.
var err error
@ -117,7 +121,7 @@ func testModule(t *testing.T, name string) *module.Tree {
t.Fatalf("err: %s", err)
}
s := module.NewStorage(tempDir(t), nil, nil)
s := module.NewStorage(tempDir(t), nil)
s.Mode = module.GetModeGet
if err := mod.Load(s); err != nil {
t.Fatalf("err: %s", err)

View File

@ -129,7 +129,7 @@ func (c *InitCommand) Run(args []string) int {
)))
header = true
s := module.NewStorage("", c.Services, c.Credentials)
s := module.NewStorage("", c.Services)
if err := s.GetModule(path, src); err != nil {
c.Ui.Error(fmt.Sprintf("Error copying source module: %s", err))
return 1
@ -138,11 +138,12 @@ func (c *InitCommand) Run(args []string) int {
// If our directory is empty, then we're done. We can't get or setup
// the backend with an empty directory.
if empty, err := config.IsEmptyDir(path); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error checking configuration: %s", err))
empty, err := config.IsEmptyDir(path)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error checking configuration: %s", err))
return 1
} else if empty {
}
if empty {
c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitEmpty)))
return 0
}
@ -227,14 +228,12 @@ func (c *InitCommand) Run(args []string) int {
if back != nil {
sMgr, err := back.State(c.Workspace())
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error loading state: %s", err))
c.Ui.Error(fmt.Sprintf("Error loading state: %s", err))
return 1
}
if err := sMgr.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error refreshing state: %s", err))
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
return 1
}

View File

@ -25,7 +25,6 @@ import (
"github.com/hashicorp/terraform/helper/experiment"
"github.com/hashicorp/terraform/helper/variables"
"github.com/hashicorp/terraform/helper/wrappedstreams"
"github.com/hashicorp/terraform/svchost/auth"
"github.com/hashicorp/terraform/svchost/disco"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
@ -51,10 +50,6 @@ type Meta struct {
// "terraform-native' services running at a specific user-facing hostname.
Services *disco.Disco
// Credentials provides access to credentials for "terraform-native"
// services, which are accessed by a service hostname.
Credentials auth.CredentialsSource
// RunningInAutomation indicates that commands are being run by an
// automated system rather than directly at a command prompt.
//
@ -410,7 +405,7 @@ func (m *Meta) flagSet(n string) *flag.FlagSet {
// moduleStorage returns the module.Storage implementation used to store
// modules for commands.
func (m *Meta) moduleStorage(root string, mode module.GetMode) *module.Storage {
s := module.NewStorage(filepath.Join(root, "modules"), m.Services, m.Credentials)
s := module.NewStorage(filepath.Join(root, "modules"), m.Services)
s.Ui = m.Ui
s.Mode = mode
return s

View File

@ -21,8 +21,8 @@ import (
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/mapstructure"
backendinit "github.com/hashicorp/terraform/backend/init"
backendlocal "github.com/hashicorp/terraform/backend/local"
backendInit "github.com/hashicorp/terraform/backend/init"
backendLocal "github.com/hashicorp/terraform/backend/local"
)
// BackendOpts are the options used to initialize a backend.Backend.
@ -94,7 +94,7 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, error) {
log.Printf("[INFO] command: backend initialized: %T", b)
}
// Setup the CLI opts we pass into backends that support it
// Setup the CLI opts we pass into backends that support it.
cliOpts := &backend.CLIOpts{
CLI: m.Ui,
CLIColor: m.Colorize(),
@ -136,7 +136,7 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, error) {
}
// Build the local backend
local := &backendlocal.Local{Backend: b}
local := backendLocal.NewWithBackend(b)
if err := local.CLIInit(cliOpts); err != nil {
// Local backend isn't allowed to fail. It would be a bug.
panic(err)
@ -149,7 +149,7 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, error) {
// for some checks that require a remote backend.
func (m *Meta) IsLocalBackend(b backend.Backend) bool {
// Is it a local backend?
bLocal, ok := b.(*backendlocal.Local)
bLocal, ok := b.(*backendLocal.Local)
// If it is, does it not have an alternate state backend?
if ok {
@ -231,7 +231,7 @@ func (m *Meta) backendConfig(opts *BackendOpts) (*config.Backend, error) {
rc, err := config.NewRawConfig(opts.ConfigExtra)
if err != nil {
return nil, fmt.Errorf(
"Error adding extra configuration file for backend: %s", err)
"Error adding extra backend configuration from CLI: %s", err)
}
// Merge in the configuration
@ -739,7 +739,7 @@ func (m *Meta) backend_c_R_s(
config := terraform.NewResourceConfig(rawC)
// Get the backend
f := backendinit.Backend(s.Remote.Type)
f := backendInit.Backend(s.Remote.Type)
if f == nil {
return nil, fmt.Errorf(strings.TrimSpace(errBackendLegacyUnknown), s.Remote.Type)
}
@ -937,8 +937,8 @@ func (m *Meta) backend_C_r_s(
// can get us here too. Don't delete our state if the old and new paths
// are the same.
erase := true
if newLocalB, ok := b.(*backendlocal.Local); ok {
if localB, ok := localB.(*backendlocal.Local); ok {
if newLocalB, ok := b.(*backendLocal.Local); ok {
if localB, ok := localB.(*backendLocal.Local); ok {
if newLocalB.StatePath == localB.StatePath {
erase = false
}
@ -1091,7 +1091,7 @@ func (m *Meta) backend_C_r_S_unchanged(
config := terraform.NewResourceConfig(rawC)
// Get the backend
f := backendinit.Backend(s.Backend.Type)
f := backendInit.Backend(s.Backend.Type)
if f == nil {
return nil, fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), s.Backend.Type)
}
@ -1208,7 +1208,7 @@ func (m *Meta) backendInitFromConfig(c *config.Backend) (backend.Backend, error)
config := terraform.NewResourceConfig(c.RawConfig)
// Get the backend
f := backendinit.Backend(c.Type)
f := backendInit.Backend(c.Type)
if f == nil {
return nil, fmt.Errorf(strings.TrimSpace(errBackendNewUnknown), c.Type)
}
@ -1265,7 +1265,7 @@ func (m *Meta) backendInitFromLegacy(s *terraform.RemoteState) (backend.Backend,
config := terraform.NewResourceConfig(rawC)
// Get the backend
f := backendinit.Backend(s.Type)
f := backendInit.Backend(s.Type)
if f == nil {
return nil, fmt.Errorf(strings.TrimSpace(errBackendLegacyUnknown), s.Type)
}
@ -1290,7 +1290,7 @@ func (m *Meta) backendInitFromSaved(s *terraform.BackendState) (backend.Backend,
config := terraform.NewResourceConfig(rawC)
// Get the backend
f := backendinit.Backend(s.Type)
f := backendInit.Backend(s.Type)
if f == nil {
return nil, fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), s.Type)
}

View File

@ -211,6 +211,13 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error {
stateTwo, err := opts.Two.State(opts.twoEnv)
if err != nil {
if err == backend.ErrDefaultStateNotSupported && stateOne.State() == nil {
// When using named workspaces it is common that the default
// workspace is not actually used. So we first check if there
// actually is a state to be migrated, if not we just return
// and silently ignore the unused default worksopace.
return nil
}
return fmt.Errorf(strings.TrimSpace(
errMigrateSingleLoadDefault), opts.TwoType, err)
}
@ -418,8 +425,8 @@ above error and try again.
`
const errMigrateMulti = `
Error migrating the workspace %q from the previous %q backend to the newly
configured %q backend:
Error migrating the workspace %q from the previous %q backend
to the newly configured %q backend:
%s
Terraform copies workspaces in alphabetical order. Any workspaces
@ -432,7 +439,8 @@ This will attempt to copy (with permission) all workspaces again.
`
const errBackendStateCopy = `
Error copying state from the previous %q backend to the newly configured %q backend:
Error copying state from the previous %q backend to the newly configured
%q backend:
%s
The state in the previous backend remains intact and unmodified. Please resolve

View File

@ -10,8 +10,8 @@ import (
"testing"
"github.com/hashicorp/terraform/backend"
backendinit "github.com/hashicorp/terraform/backend/init"
backendlocal "github.com/hashicorp/terraform/backend/local"
backendInit "github.com/hashicorp/terraform/backend/init"
backendLocal "github.com/hashicorp/terraform/backend/local"
"github.com/hashicorp/terraform/helper/copy"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
@ -994,8 +994,8 @@ func TestMetaBackend_reconfigureChange(t *testing.T) {
defer testChdir(t, td)()
// Register the single-state backend
backendinit.Set("local-single", backendlocal.TestNewLocalSingle)
defer backendinit.Set("local-single", nil)
backendInit.Set("local-single", backendLocal.TestNewLocalSingle)
defer backendInit.Set("local-single", nil)
// Setup the meta
m := testMetaBackend(t, nil)
@ -1093,8 +1093,8 @@ func TestMetaBackend_configuredChangeCopy_singleState(t *testing.T) {
defer testChdir(t, td)()
// Register the single-state backend
backendinit.Set("local-single", backendlocal.TestNewLocalSingle)
defer backendinit.Set("local-single", nil)
backendInit.Set("local-single", backendLocal.TestNewLocalSingle)
defer backendInit.Set("local-single", nil)
// Ask input
defer testInputMap(t, map[string]string{
@ -1149,8 +1149,8 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleDefault(t *testing.T) {
defer testChdir(t, td)()
// Register the single-state backend
backendinit.Set("local-single", backendlocal.TestNewLocalSingle)
defer backendinit.Set("local-single", nil)
backendInit.Set("local-single", backendLocal.TestNewLocalSingle)
defer backendInit.Set("local-single", nil)
// Ask input
defer testInputMap(t, map[string]string{
@ -1204,8 +1204,8 @@ func TestMetaBackend_configuredChangeCopy_multiToSingle(t *testing.T) {
defer testChdir(t, td)()
// Register the single-state backend
backendinit.Set("local-single", backendlocal.TestNewLocalSingle)
defer backendinit.Set("local-single", nil)
backendInit.Set("local-single", backendLocal.TestNewLocalSingle)
defer backendInit.Set("local-single", nil)
// Ask input
defer testInputMap(t, map[string]string{
@ -1250,7 +1250,7 @@ func TestMetaBackend_configuredChangeCopy_multiToSingle(t *testing.T) {
}
// Verify existing workspaces exist
envPath := filepath.Join(backendlocal.DefaultWorkspaceDir, "env2", backendlocal.DefaultStateFilename)
envPath := filepath.Join(backendLocal.DefaultWorkspaceDir, "env2", backendLocal.DefaultStateFilename)
if _, err := os.Stat(envPath); err != nil {
t.Fatal("env should exist")
}
@ -1271,8 +1271,8 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleCurrentEnv(t *testing.T)
defer testChdir(t, td)()
// Register the single-state backend
backendinit.Set("local-single", backendlocal.TestNewLocalSingle)
defer backendinit.Set("local-single", nil)
backendInit.Set("local-single", backendLocal.TestNewLocalSingle)
defer backendInit.Set("local-single", nil)
// Ask input
defer testInputMap(t, map[string]string{
@ -1322,7 +1322,7 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleCurrentEnv(t *testing.T)
}
// Verify existing workspaces exist
envPath := filepath.Join(backendlocal.DefaultWorkspaceDir, "env2", backendlocal.DefaultStateFilename)
envPath := filepath.Join(backendLocal.DefaultWorkspaceDir, "env2", backendLocal.DefaultStateFilename)
if _, err := os.Stat(envPath); err != nil {
t.Fatal("env should exist")
}
@ -1407,7 +1407,7 @@ func TestMetaBackend_configuredChangeCopy_multiToMulti(t *testing.T) {
{
// Verify existing workspaces exist
envPath := filepath.Join(backendlocal.DefaultWorkspaceDir, "env2", backendlocal.DefaultStateFilename)
envPath := filepath.Join(backendLocal.DefaultWorkspaceDir, "env2", backendLocal.DefaultStateFilename)
if _, err := os.Stat(envPath); err != nil {
t.Fatal("env should exist")
}
@ -1415,7 +1415,113 @@ func TestMetaBackend_configuredChangeCopy_multiToMulti(t *testing.T) {
{
// Verify new workspaces exist
envPath := filepath.Join("envdir-new", "env2", backendlocal.DefaultStateFilename)
envPath := filepath.Join("envdir-new", "env2", backendLocal.DefaultStateFilename)
if _, err := os.Stat(envPath); err != nil {
t.Fatal("env should exist")
}
}
}
// Changing a configured backend that supports multi-state to a
// backend that also supports multi-state, but doesn't allow a
// default state while the default state is non-empty.
func TestMetaBackend_configuredChangeCopy_multiToNoDefaultWithDefault(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("backend-change-multi-to-no-default-with-default"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
// Register the single-state backend
backendInit.Set("local-no-default", backendLocal.TestNewLocalNoDefault)
defer backendInit.Set("local-no-default", nil)
// Ask input
defer testInputMap(t, map[string]string{
"backend-migrate-to-new": "yes",
"backend-migrate-multistate-to-multistate": "yes",
})()
// Setup the meta
m := testMetaBackend(t, nil)
// Get the backend
_, err := m.Backend(&BackendOpts{Init: true})
if err == nil || !strings.Contains(err.Error(), "default state not supported") {
t.Fatalf("expected error to contain %q\ngot: %s", "default state not supported", err)
}
}
// Changing a configured backend that supports multi-state to a
// backend that also supports multi-state, but doesn't allow a
// default state while the default state is empty.
func TestMetaBackend_configuredChangeCopy_multiToNoDefaultWithoutDefault(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("backend-change-multi-to-no-default-without-default"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
// Register the single-state backend
backendInit.Set("local-no-default", backendLocal.TestNewLocalNoDefault)
defer backendInit.Set("local-no-default", nil)
// Ask input
defer testInputMap(t, map[string]string{
"backend-migrate-to-new": "yes",
"backend-migrate-multistate-to-multistate": "yes",
})()
// Setup the meta
m := testMetaBackend(t, nil)
// Get the backend
b, err := m.Backend(&BackendOpts{Init: true})
if err != nil {
t.Fatalf("bad: %s", err)
}
// Check resulting states
states, err := b.States()
if err != nil {
t.Fatalf("bad: %s", err)
}
sort.Strings(states)
expected := []string{"env2"}
if !reflect.DeepEqual(states, expected) {
t.Fatalf("bad: %#v", states)
}
{
// Check the named state
s, err := b.State("env2")
if err != nil {
t.Fatalf("bad: %s", err)
}
if err := s.RefreshState(); err != nil {
t.Fatalf("bad: %s", err)
}
state := s.State()
if state == nil {
t.Fatal("state should not be nil")
}
if state.Lineage != "backend-change-env2" {
t.Fatalf("bad: %#v", state)
}
}
{
// Verify existing workspaces exist
envPath := filepath.Join(backendLocal.DefaultWorkspaceDir, "env2", backendLocal.DefaultStateFilename)
if _, err := os.Stat(envPath); err != nil {
t.Fatal("env should exist")
}
}
{
// Verify new workspaces exist
envPath := filepath.Join("envdir-new", "env2", backendLocal.DefaultStateFilename)
if _, err := os.Stat(envPath); err != nil {
t.Fatal("env should exist")
}
@ -3351,7 +3457,7 @@ func TestMetaBackend_configureWithExtra(t *testing.T) {
}
// Check the state
s := testStateRead(t, filepath.Join(DefaultDataDir, backendlocal.DefaultStateFilename))
s := testStateRead(t, filepath.Join(DefaultDataDir, backendLocal.DefaultStateFilename))
if s.Backend.Hash != backendCfg.Hash {
t.Fatal("mismatched state and config backend hashes")
}
@ -3373,7 +3479,7 @@ func TestMetaBackend_configureWithExtra(t *testing.T) {
}
// Check the state
s = testStateRead(t, filepath.Join(DefaultDataDir, backendlocal.DefaultStateFilename))
s = testStateRead(t, filepath.Join(DefaultDataDir, backendLocal.DefaultStateFilename))
if s.Backend.Hash != backendCfg.Hash {
t.Fatal("mismatched state and config backend hashes")
}
@ -3442,7 +3548,7 @@ func TestMetaBackend_configToExtra(t *testing.T) {
}
// Check the state
s := testStateRead(t, filepath.Join(DefaultDataDir, backendlocal.DefaultStateFilename))
s := testStateRead(t, filepath.Join(DefaultDataDir, backendLocal.DefaultStateFilename))
backendHash := s.Backend.Hash
// init again but remove the path option from the config
@ -3463,7 +3569,7 @@ func TestMetaBackend_configToExtra(t *testing.T) {
t.Fatalf("bad: %s", err)
}
s = testStateRead(t, filepath.Join(DefaultDataDir, backendlocal.DefaultStateFilename))
s = testStateRead(t, filepath.Join(DefaultDataDir, backendLocal.DefaultStateFilename))
if s.Backend.Hash == backendHash {
t.Fatal("state.Backend.Hash was not updated")

View File

@ -5,7 +5,7 @@ import (
"fmt"
"time"
backendlocal "github.com/hashicorp/terraform/backend/local"
backendLocal "github.com/hashicorp/terraform/backend/local"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
)
@ -49,7 +49,7 @@ func (c *StateMeta) State() (state.State, error) {
// This should never fail
panic(err)
}
localB := localRaw.(*backendlocal.Local)
localB := localRaw.(*backendLocal.Local)
_, stateOutPath, _ = localB.StatePaths(env)
if err != nil {
return nil, err

View File

@ -0,0 +1,22 @@
{
"version": 3,
"serial": 0,
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
"backend": {
"type": "local",
"config": {
"path": "local-state.tfstate"
},
"hash": 9073424445967744180
},
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {},
"depends_on": []
}
]
}

View File

@ -0,0 +1,6 @@
{
"version": 3,
"terraform_version": "0.8.2",
"serial": 7,
"lineage": "backend-change"
}

View File

@ -0,0 +1,5 @@
terraform {
backend "local-no-default" {
environment_dir = "envdir-new"
}
}

View File

@ -0,0 +1,6 @@
{
"version": 3,
"terraform_version": "0.8.2",
"serial": 7,
"lineage": "backend-change-env2"
}

View File

@ -0,0 +1,22 @@
{
"version": 3,
"serial": 0,
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
"backend": {
"type": "local",
"config": {
"path": "local-state.tfstate"
},
"hash": 9073424445967744180
},
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {},
"depends_on": []
}
]
}

View File

@ -0,0 +1,5 @@
terraform {
backend "local-no-default" {
environment_dir = "envdir-new"
}
}

View File

@ -0,0 +1,6 @@
{
"version": 3,
"terraform_version": "0.8.2",
"serial": 7,
"lineage": "backend-change-env2"
}

View File

@ -30,15 +30,12 @@ const (
OutputPrefix = "o:"
)
func initCommands(config *Config) {
func initCommands(config *Config, services *disco.Disco) {
var inAutomation bool
if v := os.Getenv(runningInAutomationEnvName); v != "" {
inAutomation = true
}
credsSrc := credentialsSource(config)
services := disco.NewDisco()
services.SetCredentialsSource(credsSrc)
for userHost, hostConfig := range config.Hosts {
host, err := svchost.ForComparison(userHost)
if err != nil {
@ -58,7 +55,6 @@ func initCommands(config *Config) {
Ui: Ui,
Services: services,
Credentials: credsSrc,
RunningInAutomation: inAutomation,
PluginCacheDir: config.PluginCacheDir,

View File

@ -44,5 +44,5 @@ func testConfig(t *testing.T, n string) *config.Config {
func testStorage(t *testing.T, d *disco.Disco) *Storage {
t.Helper()
return NewStorage(tempDir(t), d, nil)
return NewStorage(tempDir(t), d)
}

View File

@ -11,7 +11,6 @@ import (
getter "github.com/hashicorp/go-getter"
"github.com/hashicorp/terraform/registry"
"github.com/hashicorp/terraform/registry/regsrc"
"github.com/hashicorp/terraform/svchost/auth"
"github.com/hashicorp/terraform/svchost/disco"
"github.com/mitchellh/cli"
)
@ -64,14 +63,10 @@ type Storage struct {
// StorageDir is the full path to the directory where all modules will be
// stored.
StorageDir string
// Services is a required *disco.Disco, which may have services and
// credentials pre-loaded.
Services *disco.Disco
// Creds optionally provides credentials for communicating with service
// providers.
Creds auth.CredentialsSource
// Ui is an optional cli.Ui for user output
Ui cli.Ui
// Mode is the GetMode that will be used for various operations.
Mode GetMode
@ -79,8 +74,8 @@ type Storage struct {
}
// NewStorage returns a new initialized Storage object.
func NewStorage(dir string, services *disco.Disco, creds auth.CredentialsSource) *Storage {
regClient := registry.NewClient(services, creds, nil)
func NewStorage(dir string, services *disco.Disco) *Storage {
regClient := registry.NewClient(services, nil)
return &Storage{
StorageDir: dir,

View File

@ -22,7 +22,7 @@ func TestGetModule(t *testing.T) {
t.Fatal(err)
}
defer os.RemoveAll(td)
storage := NewStorage(td, disco, nil)
storage := NewStorage(td, disco)
// this module exists in a test fixture, and is known by the test.Registry
// relative to our cwd.
@ -139,7 +139,7 @@ func TestAccRegistryDiscover(t *testing.T) {
t.Fatal(err)
}
s := NewStorage("/tmp", nil, nil)
s := NewStorage("/tmp", nil)
loc, err := s.registry.Location(module, "")
if err != nil {
t.Fatal(err)

View File

@ -5,7 +5,6 @@ import (
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/registry"
"github.com/hashicorp/terraform/svchost/auth"
"github.com/hashicorp/terraform/svchost/disco"
"github.com/spf13/afero"
)
@ -39,10 +38,6 @@ type Config struct {
// not supported, which should be true only in specialized circumstances
// such as in tests.
Services *disco.Disco
// Creds is a credentials store for communicating with remote module
// registry endpoints. If this is nil then no credentials will be used.
Creds auth.CredentialsSource
}
// NewLoader creates and returns a loader that reads configuration from the
@ -54,7 +49,7 @@ type Config struct {
func NewLoader(config *Config) (*Loader, error) {
fs := afero.NewOsFs()
parser := configs.NewParser(fs)
reg := registry.NewClient(config.Services, config.Creds, nil)
reg := registry.NewClient(config.Services, nil)
ret := &Loader{
parser: parser,
@ -63,7 +58,6 @@ func NewLoader(config *Config) (*Loader, error) {
CanInstall: true,
Dir: config.ModulesDir,
Services: config.Services,
Creds: config.Creds,
Registry: reg,
},
}

View File

@ -2,7 +2,6 @@ package configload
import (
"github.com/hashicorp/terraform/registry"
"github.com/hashicorp/terraform/svchost/auth"
"github.com/hashicorp/terraform/svchost/disco"
"github.com/spf13/afero"
)
@ -25,9 +24,6 @@ type moduleMgr struct {
// cached discovery information.
Services *disco.Disco
// Creds provides optional credentials for communicating with service hosts.
Creds auth.CredentialsSource
// Registry is a client for the module registry protocol, which is used
// when a module is requested from a registry source.
Registry *registry.Client

15
main.go
View File

@ -11,15 +11,16 @@ import (
"strings"
"sync"
"github.com/mitchellh/colorstring"
"github.com/hashicorp/go-plugin"
backendInit "github.com/hashicorp/terraform/backend/init"
"github.com/hashicorp/terraform/command/format"
"github.com/hashicorp/terraform/helper/logging"
"github.com/hashicorp/terraform/svchost/disco"
"github.com/hashicorp/terraform/terraform"
"github.com/mattn/go-colorable"
"github.com/mattn/go-shellwords"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
"github.com/mitchellh/panicwrap"
"github.com/mitchellh/prefixedio"
)
@ -142,9 +143,17 @@ func wrappedMain() int {
}
}
// Get any configured credentials from the config and initialize
// a service discovery object.
credsSrc := credentialsSource(config)
services := disco.NewWithCredentialsSource(credsSrc)
// Initialize the backends.
backendInit.Init(services)
// In tests, Commands may already be set to provide mock commands
if Commands == nil {
initCommands(config)
initCommands(config, services)
}
// Run checkpoint

View File

@ -15,7 +15,6 @@ import (
"github.com/hashicorp/terraform/registry/regsrc"
"github.com/hashicorp/terraform/registry/response"
"github.com/hashicorp/terraform/svchost"
"github.com/hashicorp/terraform/svchost/auth"
"github.com/hashicorp/terraform/svchost/disco"
"github.com/hashicorp/terraform/version"
)
@ -37,20 +36,14 @@ type Client struct {
// services is a required *disco.Disco, which may have services and
// credentials pre-loaded.
services *disco.Disco
// Creds optionally provides credentials for communicating with service
// providers.
creds auth.CredentialsSource
}
// NewClient returns a new initialized registry client.
func NewClient(services *disco.Disco, creds auth.CredentialsSource, client *http.Client) *Client {
func NewClient(services *disco.Disco, client *http.Client) *Client {
if services == nil {
services = disco.NewDisco()
services = disco.New()
}
services.SetCredentialsSource(creds)
if client == nil {
client = httpclient.New()
client.Timeout = requestTimeout
@ -61,7 +54,6 @@ func NewClient(services *disco.Disco, creds auth.CredentialsSource, client *http
return &Client{
client: client,
services: services,
creds: creds,
}
}
@ -138,11 +130,7 @@ func (c *Client) Versions(module *regsrc.Module) (*response.ModuleVersions, erro
}
func (c *Client) addRequestCreds(host svchost.Hostname, req *http.Request) {
if c.creds == nil {
return
}
creds, err := c.creds.ForHost(host)
creds, err := c.services.CredentialsForHost(host)
if err != nil {
log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err)
return

View File

@ -15,7 +15,7 @@ func TestLookupModuleVersions(t *testing.T) {
server := test.Registry()
defer server.Close()
client := NewClient(test.Disco(server), nil, nil)
client := NewClient(test.Disco(server), nil)
// test with and without a hostname
for _, src := range []string{
@ -59,7 +59,7 @@ func TestInvalidRegistry(t *testing.T) {
server := test.Registry()
defer server.Close()
client := NewClient(test.Disco(server), nil, nil)
client := NewClient(test.Disco(server), nil)
src := "non-existent.localhost.localdomain/test-versions/name/provider"
modsrc, err := regsrc.ParseModuleSource(src)
@ -76,7 +76,7 @@ func TestRegistryAuth(t *testing.T) {
server := test.Registry()
defer server.Close()
client := NewClient(test.Disco(server), nil, nil)
client := NewClient(test.Disco(server), nil)
src := "private/name/provider"
mod, err := regsrc.ParseModuleSource(src)
@ -84,6 +84,18 @@ func TestRegistryAuth(t *testing.T) {
t.Fatal(err)
}
_, err = client.Versions(mod)
if err != nil {
t.Fatal(err)
}
_, err = client.Location(mod, "1.0.0")
if err != nil {
t.Fatal(err)
}
// Also test without a credentials source
client.services.SetCredentialsSource(nil)
// both should fail without auth
_, err = client.Versions(mod)
if err == nil {
@ -93,24 +105,13 @@ func TestRegistryAuth(t *testing.T) {
if err == nil {
t.Fatal("expected error")
}
client = NewClient(test.Disco(server), test.Credentials, nil)
_, err = client.Versions(mod)
if err != nil {
t.Fatal(err)
}
_, err = client.Location(mod, "1.0.0")
if err != nil {
t.Fatal(err)
}
}
func TestLookupModuleLocationRelative(t *testing.T) {
server := test.Registry()
defer server.Close()
client := NewClient(test.Disco(server), nil, nil)
client := NewClient(test.Disco(server), nil)
src := "relative/foo/bar"
mod, err := regsrc.ParseModuleSource(src)
@ -133,7 +134,7 @@ func TestAccLookupModuleVersions(t *testing.T) {
if os.Getenv("TF_ACC") == "" {
t.Skip()
}
regDisco := disco.NewDisco()
regDisco := disco.New()
// test with and without a hostname
for _, src := range []string{
@ -145,7 +146,7 @@ func TestAccLookupModuleVersions(t *testing.T) {
t.Fatal(err)
}
s := NewClient(regDisco, nil, nil)
s := NewClient(regDisco, nil)
resp, err := s.Versions(modsrc)
if err != nil {
t.Fatal(err)
@ -179,7 +180,7 @@ func TestLookupLookupModuleError(t *testing.T) {
server := test.Registry()
defer server.Close()
client := NewClient(test.Disco(server), nil, nil)
client := NewClient(test.Disco(server), nil)
// this should not be found in teh registry
src := "bad/local/path"

View File

@ -27,7 +27,7 @@ func Disco(s *httptest.Server) *disco.Disco {
// TODO: add specific tests to enumerate both possibilities.
"modules.v1": fmt.Sprintf("%s/v1/modules", s.URL),
}
d := disco.NewDisco()
d := disco.NewWithCredentialsSource(credsSrc)
d.ForceHostServices(svchost.Hostname("registry.terraform.io"), services)
d.ForceHostServices(svchost.Hostname("localhost"), services)
@ -49,7 +49,7 @@ const (
var (
regHost = svchost.Hostname(regsrc.PublicRegistryHost.Normalized())
Credentials = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{
credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{
regHost: {"token": testCred},
})
)

View File

@ -26,7 +26,7 @@ func TestClient(t *testing.T, c Client) {
t.Fatalf("get: %s", err)
}
if !bytes.Equal(p.Data, data) {
t.Fatalf("bad: %#v", p)
t.Fatalf("expected full state %q\n\ngot: %q", string(p.Data), string(data))
}
if err := c.Delete(); err != nil {
@ -38,7 +38,7 @@ func TestClient(t *testing.T, c Client) {
t.Fatalf("get: %s", err)
}
if p != nil {
t.Fatalf("bad: %#v", p)
t.Fatalf("expected empty state, got: %q", string(p.Data))
}
}

View File

@ -42,6 +42,9 @@ type HostCredentials interface {
// receiving credentials. The usual behavior of this method is to
// add some sort of Authorization header to the request.
PrepareRequest(req *http.Request)
// Token returns the authentication token.
Token() string
}
// ForHost iterates over the contained CredentialsSource objects and

View File

@ -18,3 +18,8 @@ func (tc HostCredentialsToken) PrepareRequest(req *http.Request) {
}
req.Header.Set("Authorization", "Bearer "+string(tc))
}
// Token returns the authentication token.
func (tc HostCredentialsToken) Token() string {
return string(tc)
}

View File

@ -42,9 +42,15 @@ type Disco struct {
Transport http.RoundTripper
}
// NewDisco returns a new initialized Disco object.
func NewDisco() *Disco {
return &Disco{}
// New returns a new initialized discovery object.
func New() *Disco {
return NewWithCredentialsSource(nil)
}
// NewWithCredentialsSource returns a new discovery object initialized with
// the given credentials source.
func NewWithCredentialsSource(credsSrc auth.CredentialsSource) *Disco {
return &Disco{credsSrc: credsSrc}
}
// SetCredentialsSource provides a credentials source that will be used to
@ -56,6 +62,15 @@ func (d *Disco) SetCredentialsSource(src auth.CredentialsSource) {
d.credsSrc = src
}
// CredentialsForHost returns a non-nil HostCredentials if the embedded source has
// credentials available for the host, and a nil HostCredentials if it does not.
func (d *Disco) CredentialsForHost(host svchost.Hostname) (auth.HostCredentials, error) {
if d.credsSrc == nil {
return nil, nil
}
return d.credsSrc.ForHost(host)
}
// ForceHostServices provides a pre-defined set of services for a given
// host, which prevents the receiver from attempting network-based discovery
// for the given host. Instead, the given services map will be returned
@ -145,15 +160,10 @@ func (d *Disco) discover(host svchost.Hostname) Host {
URL: discoURL,
}
if d.credsSrc != nil {
creds, err := d.credsSrc.ForHost(host)
if err == nil {
if creds != nil {
creds.PrepareRequest(req) // alters req to include credentials
}
} else {
if creds, err := d.CredentialsForHost(host); err != nil {
log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err)
}
} else if creds != nil {
creds.PrepareRequest(req) // alters req to include credentials
}
log.Printf("[DEBUG] Service discovery for %s at %s", host, discoURL)

View File

@ -45,7 +45,7 @@ func TestDiscover(t *testing.T) {
t.Fatalf("test server hostname is invalid: %s", err)
}
d := NewDisco()
d := New()
discovered := d.Discover(host)
gotURL := discovered.ServiceURL("thingy.v1")
if gotURL == nil {
@ -80,7 +80,7 @@ func TestDiscover(t *testing.T) {
t.Fatalf("test server hostname is invalid: %s", err)
}
d := NewDisco()
d := New()
discovered := d.Discover(host)
gotURL := discovered.ServiceURL("wotsit.v2")
if gotURL == nil {
@ -107,7 +107,7 @@ func TestDiscover(t *testing.T) {
t.Fatalf("test server hostname is invalid: %s", err)
}
d := NewDisco()
d := New()
d.SetCredentialsSource(auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{
host: map[string]interface{}{
"token": "abc123",
@ -124,7 +124,7 @@ func TestDiscover(t *testing.T) {
"wotsit.v2": "/foo",
}
d := NewDisco()
d := New()
d.ForceHostServices(svchost.Hostname("example.com"), forced)
givenHost := "example.com"
@ -167,7 +167,7 @@ func TestDiscover(t *testing.T) {
t.Fatalf("test server hostname is invalid: %s", err)
}
d := NewDisco()
d := New()
discovered := d.Discover(host)
// result should be empty, which we can verify only by reaching into
@ -190,7 +190,7 @@ func TestDiscover(t *testing.T) {
t.Fatalf("test server hostname is invalid: %s", err)
}
d := NewDisco()
d := New()
discovered := d.Discover(host)
// result should be empty, which we can verify only by reaching into
@ -217,7 +217,7 @@ func TestDiscover(t *testing.T) {
t.Fatalf("test server hostname is invalid: %s", err)
}
d := NewDisco()
d := New()
discovered := d.Discover(host)
if discovered.services == nil {
@ -236,7 +236,7 @@ func TestDiscover(t *testing.T) {
t.Fatalf("test server hostname is invalid: %s", err)
}
d := NewDisco()
d := New()
discovered := d.Discover(host)
// result should be empty, which we can verify only by reaching into
@ -267,7 +267,7 @@ func TestDiscover(t *testing.T) {
t.Fatalf("test server hostname is invalid: %s", err)
}
d := NewDisco()
d := New()
discovered := d.Discover(host)
gotURL := discovered.ServiceURL("thingy.v1")

27
vendor/github.com/google/go-querystring/LICENSE generated vendored Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2013 Google. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

320
vendor/github.com/google/go-querystring/query/encode.go generated vendored Normal file
View File

@ -0,0 +1,320 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package query implements encoding of structs into URL query parameters.
//
// As a simple example:
//
// type Options struct {
// Query string `url:"q"`
// ShowAll bool `url:"all"`
// Page int `url:"page"`
// }
//
// opt := Options{ "foo", true, 2 }
// v, _ := query.Values(opt)
// fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2"
//
// The exact mapping between Go values and url.Values is described in the
// documentation for the Values() function.
package query
import (
"bytes"
"fmt"
"net/url"
"reflect"
"strconv"
"strings"
"time"
)
var timeType = reflect.TypeOf(time.Time{})
var encoderType = reflect.TypeOf(new(Encoder)).Elem()
// Encoder is an interface implemented by any type that wishes to encode
// itself into URL values in a non-standard way.
type Encoder interface {
EncodeValues(key string, v *url.Values) error
}
// Values returns the url.Values encoding of v.
//
// Values expects to be passed a struct, and traverses it recursively using the
// following encoding rules.
//
// Each exported struct field is encoded as a URL parameter unless
//
// - the field's tag is "-", or
// - the field is empty and its tag specifies the "omitempty" option
//
// The empty values are false, 0, any nil pointer or interface value, any array
// slice, map, or string of length zero, and any time.Time that returns true
// for IsZero().
//
// The URL parameter name defaults to the struct field name but can be
// specified in the struct field's tag value. The "url" key in the struct
// field's tag value is the key name, followed by an optional comma and
// options. For example:
//
// // Field is ignored by this package.
// Field int `url:"-"`
//
// // Field appears as URL parameter "myName".
// Field int `url:"myName"`
//
// // Field appears as URL parameter "myName" and the field is omitted if
// // its value is empty
// Field int `url:"myName,omitempty"`
//
// // Field appears as URL parameter "Field" (the default), but the field
// // is skipped if empty. Note the leading comma.
// Field int `url:",omitempty"`
//
// For encoding individual field values, the following type-dependent rules
// apply:
//
// Boolean values default to encoding as the strings "true" or "false".
// Including the "int" option signals that the field should be encoded as the
// strings "1" or "0".
//
// time.Time values default to encoding as RFC3339 timestamps. Including the
// "unix" option signals that the field should be encoded as a Unix time (see
// time.Unix())
//
// Slice and Array values default to encoding as multiple URL values of the
// same name. Including the "comma" option signals that the field should be
// encoded as a single comma-delimited value. Including the "space" option
// similarly encodes the value as a single space-delimited string. Including
// the "semicolon" option will encode the value as a semicolon-delimited string.
// Including the "brackets" option signals that the multiple URL values should
// have "[]" appended to the value name. "numbered" will append a number to
// the end of each incidence of the value name, example:
// name0=value0&name1=value1, etc.
//
// Anonymous struct fields are usually encoded as if their inner exported
// fields were fields in the outer struct, subject to the standard Go
// visibility rules. An anonymous struct field with a name given in its URL
// tag is treated as having that name, rather than being anonymous.
//
// Non-nil pointer values are encoded as the value pointed to.
//
// Nested structs are encoded including parent fields in value names for
// scoping. e.g:
//
// "user[name]=acme&user[addr][postcode]=1234&user[addr][city]=SFO"
//
// All other values are encoded using their default string representation.
//
// Multiple fields that encode to the same URL parameter name will be included
// as multiple URL values of the same name.
func Values(v interface{}) (url.Values, error) {
values := make(url.Values)
val := reflect.ValueOf(v)
for val.Kind() == reflect.Ptr {
if val.IsNil() {
return values, nil
}
val = val.Elem()
}
if v == nil {
return values, nil
}
if val.Kind() != reflect.Struct {
return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind())
}
err := reflectValue(values, val, "")
return values, err
}
// reflectValue populates the values parameter from the struct fields in val.
// Embedded structs are followed recursively (using the rules defined in the
// Values function documentation) breadth-first.
func reflectValue(values url.Values, val reflect.Value, scope string) error {
var embedded []reflect.Value
typ := val.Type()
for i := 0; i < typ.NumField(); i++ {
sf := typ.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
}
sv := val.Field(i)
tag := sf.Tag.Get("url")
if tag == "-" {
continue
}
name, opts := parseTag(tag)
if name == "" {
if sf.Anonymous && sv.Kind() == reflect.Struct {
// save embedded struct for later processing
embedded = append(embedded, sv)
continue
}
name = sf.Name
}
if scope != "" {
name = scope + "[" + name + "]"
}
if opts.Contains("omitempty") && isEmptyValue(sv) {
continue
}
if sv.Type().Implements(encoderType) {
if !reflect.Indirect(sv).IsValid() {
sv = reflect.New(sv.Type().Elem())
}
m := sv.Interface().(Encoder)
if err := m.EncodeValues(name, &values); err != nil {
return err
}
continue
}
if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
var del byte
if opts.Contains("comma") {
del = ','
} else if opts.Contains("space") {
del = ' '
} else if opts.Contains("semicolon") {
del = ';'
} else if opts.Contains("brackets") {
name = name + "[]"
}
if del != 0 {
s := new(bytes.Buffer)
first := true
for i := 0; i < sv.Len(); i++ {
if first {
first = false
} else {
s.WriteByte(del)
}
s.WriteString(valueString(sv.Index(i), opts))
}
values.Add(name, s.String())
} else {
for i := 0; i < sv.Len(); i++ {
k := name
if opts.Contains("numbered") {
k = fmt.Sprintf("%s%d", name, i)
}
values.Add(k, valueString(sv.Index(i), opts))
}
}
continue
}
for sv.Kind() == reflect.Ptr {
if sv.IsNil() {
break
}
sv = sv.Elem()
}
if sv.Type() == timeType {
values.Add(name, valueString(sv, opts))
continue
}
if sv.Kind() == reflect.Struct {
reflectValue(values, sv, name)
continue
}
values.Add(name, valueString(sv, opts))
}
for _, f := range embedded {
if err := reflectValue(values, f, scope); err != nil {
return err
}
}
return nil
}
// valueString returns the string representation of a value.
func valueString(v reflect.Value, opts tagOptions) string {
for v.Kind() == reflect.Ptr {
if v.IsNil() {
return ""
}
v = v.Elem()
}
if v.Kind() == reflect.Bool && opts.Contains("int") {
if v.Bool() {
return "1"
}
return "0"
}
if v.Type() == timeType {
t := v.Interface().(time.Time)
if opts.Contains("unix") {
return strconv.FormatInt(t.Unix(), 10)
}
return t.Format(time.RFC3339)
}
return fmt.Sprint(v.Interface())
}
// isEmptyValue checks if a value should be considered empty for the purposes
// of omitting fields with the "omitempty" option.
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
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.Interface, reflect.Ptr:
return v.IsNil()
}
if v.Type() == timeType {
return v.Interface().(time.Time).IsZero()
}
return false
}
// tagOptions is the string following a comma in a struct field's "url" tag, or
// the empty string. It does not include the leading comma.
type tagOptions []string
// parseTag splits a struct field's url tag into its name and comma-separated
// options.
func parseTag(tag string) (string, tagOptions) {
s := strings.Split(tag, ",")
return s[0], s[1:]
}
// Contains checks whether the tagOptions contains the specified option.
func (o tagOptions) Contains(option string) bool {
for _, s := range o {
if s == option {
return true
}
}
return false
}

373
vendor/github.com/hashicorp/go-slug/LICENSE generated vendored Normal file
View File

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

70
vendor/github.com/hashicorp/go-slug/README.md generated vendored Normal file
View File

@ -0,0 +1,70 @@
# go-slug
[![Build Status](https://travis-ci.org/hashicorp/go-slug.svg?branch=master)](https://travis-ci.org/hashicorp/go-slug)
[![GitHub license](https://img.shields.io/github/license/hashicorp/go-slug.svg)](https://github.com/hashicorp/go-slug/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/hashicorp/go-slug?status.svg)](https://godoc.org/github.com/hashicorp/go-slug)
[![Go Report Card](https://goreportcard.com/badge/github.com/hashicorp/go-slug)](https://goreportcard.com/report/github.com/hashicorp/go-slug)
[![GitHub issues](https://img.shields.io/github/issues/hashicorp/go-slug.svg)](https://github.com/hashicorp/go-slug/issues)
Package `go-slug` offers functions for packing and unpacking Terraform Enterprise
compatible slugs. Slugs are gzip compressed tar files containing Terraform configuration files.
## Installation
Installation can be done with a normal `go get`:
```
go get -u github.com/hashicorp/go-slug
```
## Documentation
For the complete usage of `go-slug`, see the full [package docs](https://godoc.org/github.com/hashicorp/go-slug).
## Example
Packing or unpacking a slug is pretty straight forward as shown in the
following example:
```go
package main
import (
"bytes"
"ioutil"
"log"
"os"
slug "github.com/hashicorp/go-slug"
)
func main() {
// First create a buffer for storing the slug.
slug := bytes.NewBuffer(nil)
// Then call the Pack function with a directory path containing the
// configuration files and an io.Writer to write the slug to.
if _, err := Pack("test-fixtures/archive-dir", slug); err != nil {
log.Fatal(err)
}
// Create a directory to unpack the slug contents into.
dst, err := ioutil.TempDir("", "slug")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(dst)
// Unpacking a slug is done by calling the Unpack function with an
// io.Reader to read the slug from and a directory path of an existing
// directory to store the unpacked configuration files.
if err := Unpack(slug, dst); err != nil {
log.Fatal(err)
}
}
```
## Issues and Contributing
If you find an issue with this package, please report an issue. If you'd like,
we welcome any contributions. Fork this repository and submit a pull request.

215
vendor/github.com/hashicorp/go-slug/slug.go generated vendored Normal file
View File

@ -0,0 +1,215 @@
package slug
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
)
// Meta provides detailed information about a slug.
type Meta struct {
// The list of files contained in the slug.
Files []string
// Total size of the slug in bytes.
Size int64
}
// Pack creates a slug from a directory src, and writes the new
// slug to w. Returns metadata about the slug and any error.
func Pack(src string, w io.Writer) (*Meta, error) {
// Gzip compress all the output data
gzipW := gzip.NewWriter(w)
// Tar the file contents
tarW := tar.NewWriter(gzipW)
// Track the metadata details as we go.
meta := &Meta{}
// Walk the tree of files
err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Check the file type and if we need to write the body
keepFile, writeBody := checkFileMode(info.Mode())
if !keepFile {
return nil
}
// Get the relative path from the unpack directory
subpath, err := filepath.Rel(src, path)
if err != nil {
return fmt.Errorf("Failed to get relative path for file %q: %v", path, err)
}
if subpath == "." {
return nil
}
// Read the symlink target. We don't track the error because
// it doesn't matter if there is an error.
target, _ := os.Readlink(path)
// Build the file header for the tar entry
header, err := tar.FileInfoHeader(info, target)
if err != nil {
return fmt.Errorf("Failed creating archive header for file %q: %v", path, err)
}
// Modify the header to properly be the full subpath
header.Name = subpath
if info.IsDir() {
header.Name += "/"
}
// Write the header first to the archive.
if err := tarW.WriteHeader(header); err != nil {
return fmt.Errorf("Failed writing archive header for file %q: %v", path, err)
}
// Account for the file in the list
meta.Files = append(meta.Files, header.Name)
// Skip writing file data for certain file types (above).
if !writeBody {
return nil
}
// Add the size since we are going to write the body.
meta.Size += info.Size()
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("Failed opening file %q for archiving: %v", path, err)
}
defer f.Close()
if _, err = io.Copy(tarW, f); err != nil {
return fmt.Errorf("Failed copying file %q to archive: %v", path, err)
}
return nil
})
if err != nil {
return nil, err
}
// Flush the tar writer
if err := tarW.Close(); err != nil {
return nil, fmt.Errorf("Failed to close the tar archive: %v", err)
}
// Flush the gzip writer
if err := gzipW.Close(); err != nil {
return nil, fmt.Errorf("Failed to close the gzip writer: %v", err)
}
return meta, nil
}
// Unpack is used to read and extract the contents of a slug to
// directory dst. Returns any error.
func Unpack(r io.Reader, dst string) error {
// Decompress as we read
uncompressed, err := gzip.NewReader(r)
if err != nil {
return fmt.Errorf("Failed to uncompress slug: %v", err)
}
// Untar as we read
untar := tar.NewReader(uncompressed)
// Unpackage all the contents into the directory
for {
header, err := untar.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("Failed to untar slug: %v", err)
}
// Get rid of absolute paths
path := header.Name
if path[0] == '/' {
path = path[1:]
}
path = filepath.Join(dst, path)
// Make the directories to the path
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("Failed to create directory %q: %v", dir, err)
}
// If we have a symlink, just link it.
if header.Typeflag == tar.TypeSymlink {
if err := os.Symlink(header.Linkname, path); err != nil {
return fmt.Errorf("Failed creating symlink %q => %q: %v",
path, header.Linkname, err)
}
continue
}
// Only unpack regular files from this point on
if header.Typeflag == tar.TypeDir {
continue
} else if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeRegA {
return fmt.Errorf("Failed creating %q: unsupported type %c", path,
header.Typeflag)
}
// Open a handle to the destination
fh, err := os.Create(path)
if err != nil {
// This mimics tar's behavior wrt the tar file containing duplicate files
// and it allowing later ones to clobber earlier ones even if the file
// has perms that don't allow overwriting
if os.IsPermission(err) {
os.Chmod(path, 0600)
fh, err = os.Create(path)
}
if err != nil {
return fmt.Errorf("Failed creating file %q: %v", path, err)
}
}
// Copy the contents
_, err = io.Copy(fh, untar)
fh.Close()
if err != nil {
return fmt.Errorf("Failed to copy slug file %q: %v", path, err)
}
// Restore the file mode. We have to do this after writing the file,
// since it is possible we have a read-only mode.
mode := header.FileInfo().Mode()
if err := os.Chmod(path, mode); err != nil {
return fmt.Errorf("Failed setting permissions on %q: %v", path, err)
}
}
return nil
}
// checkFileMode is used to examine an os.FileMode and determine if it should
// be included in the archive, and if it has a data body which needs writing.
func checkFileMode(m os.FileMode) (keep, body bool) {
switch {
case m.IsRegular():
return true, true
case m.IsDir():
return true, false
case m&os.ModeSymlink != 0:
return true, false
}
return false, false
}

354
vendor/github.com/hashicorp/go-tfe/LICENSE generated vendored Normal file
View File

@ -0,0 +1,354 @@
Mozilla Public License, version 2.0
1. Definitions
1.1. “Contributor”
means each individual or legal entity that creates, contributes to the
creation of, or owns Covered Software.
1.2. “Contributor Version”
means the combination of the Contributions of others (if any) used by a
Contributor and that particular Contributors Contribution.
1.3. “Contribution”
means Covered Software of a particular Contributor.
1.4. “Covered Software”
means Source Code Form to which the initial Contributor has attached the
notice in Exhibit A, the Executable Form of such Source Code Form, and
Modifications of such Source Code Form, in each case including portions
thereof.
1.5. “Incompatible With Secondary Licenses”
means
a. that the initial Contributor has attached the notice described in
Exhibit B to the Covered Software; or
b. that the Covered Software was made available under the terms of version
1.1 or earlier of the License, but not also under the terms of a
Secondary License.
1.6. “Executable Form”
means any form of the work other than Source Code Form.
1.7. “Larger Work”
means a work that combines Covered Software with other material, in a separate
file or files, that is not Covered Software.
1.8. “License”
means this document.
1.9. “Licensable”
means having the right to grant, to the maximum extent possible, whether at the
time of the initial grant or subsequently, any and all of the rights conveyed by
this License.
1.10. “Modifications”
means any of the following:
a. any file in Source Code Form that results from an addition to, deletion
from, or modification of the contents of Covered Software; or
b. any new file in Source Code Form that contains any Covered Software.
1.11. “Patent Claims” of a Contributor
means any patent claim(s), including without limitation, method, process,
and apparatus claims, in any patent Licensable by such Contributor that
would be infringed, but for the grant of the License, by the making,
using, selling, offering for sale, having made, import, or transfer of
either its Contributions or its Contributor Version.
1.12. “Secondary License”
means either the GNU General Public License, Version 2.0, the GNU Lesser
General Public License, Version 2.1, the GNU Affero General Public
License, Version 3.0, or any later versions of those licenses.
1.13. “Source Code Form”
means the form of the work preferred for making modifications.
1.14. “You” (or “Your”)
means an individual or a legal entity exercising rights under this
License. For legal entities, “You” includes any entity that controls, is
controlled by, or is under common control with You. For purposes of this
definition, “control” means (a) the power, direct or indirect, to cause
the direction or management of such entity, whether by contract or
otherwise, or (b) ownership of more than fifty percent (50%) of the
outstanding shares or beneficial ownership of such entity.
2. License Grants and Conditions
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
a. under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or as
part of a Larger Work; and
b. under Patent Claims of such Contributor to make, use, sell, offer for
sale, have made, import, and otherwise transfer either its Contributions
or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution become
effective for each Contribution on the date the Contributor first distributes
such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under this
License. No additional rights or licenses will be implied from the distribution
or licensing of Covered Software under this License. Notwithstanding Section
2.1(b) above, no patent license is granted by a Contributor:
a. for any code that a Contributor has removed from Covered Software; or
b. for infringements caused by: (i) Your and any other third partys
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
c. under Patent Claims infringed by Covered Software in the absence of its
Contributions.
This License does not grant any rights in the trademarks, service marks, or
logos of any Contributor (except as may be necessary to comply with the
notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this License
(see Section 10.2) or under the terms of a Secondary License (if permitted
under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its Contributions
are its original creation(s) or it has sufficient rights to grant the
rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under applicable
copyright doctrines of fair use, fair dealing, or other equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
Section 2.1.
3. Responsibilities
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under the
terms of this License. You must inform recipients that the Source Code Form
of the Covered Software is governed by the terms of this License, and how
they can obtain a copy of this License. You may not attempt to alter or
restrict the recipients rights in the Source Code Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
a. such Covered Software must also be made available in Source Code Form,
as described in Section 3.1, and You must inform recipients of the
Executable Form how they can obtain a copy of such Source Code Form by
reasonable means in a timely manner, at a charge no more than the cost
of distribution to the recipient; and
b. You may distribute such Executable Form under the terms of this License,
or sublicense it under different terms, provided that the license for
the Executable Form does not attempt to limit or alter the recipients
rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for the
Covered Software. If the Larger Work is a combination of Covered Software
with a work governed by one or more Secondary Licenses, and the Covered
Software is not Incompatible With Secondary Licenses, this License permits
You to additionally distribute such Covered Software under the terms of
such Secondary License(s), so that the recipient of the Larger Work may, at
their option, further distribute the Covered Software under the terms of
either this License or such Secondary License(s).
3.4. Notices
You may not remove or alter the substance of any license notices (including
copyright notices, patent notices, disclaimers of warranty, or limitations
of liability) contained within the Source Code Form of the Covered
Software, except that You may alter any license notices to the extent
required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on behalf
of any Contributor. You must make it absolutely clear that any such
warranty, support, indemnity, or liability obligation is offered by You
alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
If it is impossible for You to comply with any of the terms of this License
with respect to some or all of the Covered Software due to statute, judicial
order, or regulation then You must: (a) comply with the terms of this License
to the maximum extent possible; and (b) describe the limitations and the code
they affect. Such description must be placed in a text file included with all
distributions of the Covered Software under this License. Except to the
extent prohibited by statute or regulation, such description must be
sufficiently detailed for a recipient of ordinary skill to be able to
understand it.
5. Termination
5.1. The rights granted under this License will terminate automatically if You
fail to comply with any of its terms. However, if You become compliant,
then the rights granted under this License from a particular Contributor
are reinstated (a) provisionally, unless and until such Contributor
explicitly and finally terminates Your grants, and (b) on an ongoing basis,
if such Contributor fails to notify You of the non-compliance by some
reasonable means prior to 60 days after You have come back into compliance.
Moreover, Your grants from a particular Contributor are reinstated on an
ongoing basis if such Contributor notifies You of the non-compliance by
some reasonable means, this is the first time You have received notice of
non-compliance with this License from such Contributor, and You become
compliant prior to 30 days after Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions, counter-claims,
and cross-claims) alleging that a Contributor Version directly or
indirectly infringes any patent, then the rights granted to You by any and
all Contributors for the Covered Software under Section 2.1 of this License
shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
license agreements (excluding distributors and resellers) which have been
validly granted by You or Your distributors under this License prior to
termination shall survive termination.
6. Disclaimer of Warranty
Covered Software is provided under this License on an “as is” basis, without
warranty of any kind, either expressed, implied, or statutory, including,
without limitation, warranties that the Covered Software is free of defects,
merchantable, fit for a particular purpose or non-infringing. The entire
risk as to the quality and performance of the Covered Software is with You.
Should any Covered Software prove defective in any respect, You (not any
Contributor) assume the cost of any necessary servicing, repair, or
correction. This disclaimer of warranty constitutes an essential part of this
License. No use of any Covered Software is authorized under this License
except under this disclaimer.
7. Limitation of Liability
Under no circumstances and under no legal theory, whether tort (including
negligence), contract, or otherwise, shall any Contributor, or anyone who
distributes Covered Software as permitted above, be liable to You for any
direct, indirect, special, incidental, or consequential damages of any
character including, without limitation, damages for lost profits, loss of
goodwill, work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses, even if such party shall have been
informed of the possibility of such damages. This limitation of liability
shall not apply to liability for death or personal injury resulting from such
partys negligence to the extent applicable law prohibits such limitation.
Some jurisdictions do not allow the exclusion or limitation of incidental or
consequential damages, so this exclusion and limitation may not apply to You.
8. Litigation
Any litigation relating to this License may be brought only in the courts of
a jurisdiction where the defendant maintains its principal place of business
and such litigation shall be governed by laws of that jurisdiction, without
reference to its conflict-of-law provisions. Nothing in this Section shall
prevent a partys ability to bring cross-claims or counter-claims.
9. Miscellaneous
This License represents the complete agreement concerning the subject matter
hereof. If any provision of this License is held to be unenforceable, such
provision shall be reformed only to the extent necessary to make it
enforceable. Any law or regulation which provides that the language of a
contract shall be construed against the drafter shall not be used to construe
this License against a Contributor.
10. Versions of the License
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version of
the License under which You originally received the Covered Software, or
under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a modified
version of this License if you rename the license and remove any
references to the name of the license steward (except to note that such
modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular file, then
You may include the notice in a location (such as a LICENSE file in a relevant
directory) where a recipient would be likely to look for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - “Incompatible With Secondary Licenses” Notice
This Source Code Form is “Incompatible
With Secondary Licenses”, as defined by
the Mozilla Public License, v. 2.0.

131
vendor/github.com/hashicorp/go-tfe/README.md generated vendored Normal file
View File

@ -0,0 +1,131 @@
Terraform Enterprise Go Client
==============================
[![Build Status](https://travis-ci.org/hashicorp/go-tfe.svg?branch=master)](https://travis-ci.org/hashicorp/go-tfe)
[![GitHub license](https://img.shields.io/github/license/hashicorp/go-tfe.svg)](https://github.com/hashicorp/go-tfe/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/hashicorp/go-tfe?status.svg)](https://godoc.org/github.com/hashicorp/go-tfe)
[![Go Report Card](https://goreportcard.com/badge/github.com/hashicorp/go-tfe)](https://goreportcard.com/report/github.com/hashicorp/go-tfe)
[![GitHub issues](https://img.shields.io/github/issues/hashicorp/go-tfe.svg)](https://github.com/hashicorp/go-tfe/issues)
This is an API client for [Terraform Enterprise](https://www.hashicorp.com/products/terraform).
## NOTE
The Terraform Enterprise API endpoints are in beta and are subject to change!
So that means this API client is also in beta and is also subject to change. We
will indicate any breaking changes by releasing new versions. Until the release
of v1.0, any minor version changes will indicate possible breaking changes. Patch
version changes will be used for both bugfixes and non-breaking changes.
## Coverage
Currently the following endpoints are supported:
- [x] [Accounts](https://www.terraform.io/docs/enterprise/api/account.html)
- [x] [Configuration Versions](https://www.terraform.io/docs/enterprise/api/configuration-versions.html)
- [x] [OAuth Clients](https://www.terraform.io/docs/enterprise/api/oauth-clients.html)
- [x] [OAuth Tokens](https://www.terraform.io/docs/enterprise/api/oauth-tokens.html)
- [x] [Organizations](https://www.terraform.io/docs/enterprise/api/organizations.html)
- [x] [Organization Tokens](https://www.terraform.io/docs/enterprise/api/organization-tokens.html)
- [x] [Policies](https://www.terraform.io/docs/enterprise/api/policies.html)
- [x] [Policy Checks](https://www.terraform.io/docs/enterprise/api/policy-checks.html)
- [ ] [Registry Modules](https://www.terraform.io/docs/enterprise/api/modules.html)
- [x] [Runs](https://www.terraform.io/docs/enterprise/api/run.html)
- [x] [SSH Keys](https://www.terraform.io/docs/enterprise/api/ssh-keys.html)
- [x] [State Versions](https://www.terraform.io/docs/enterprise/api/state-versions.html)
- [x] [Team Access](https://www.terraform.io/docs/enterprise/api/team-access.html)
- [x] [Team Memberships](https://www.terraform.io/docs/enterprise/api/team-members.html)
- [x] [Team Tokens](https://www.terraform.io/docs/enterprise/api/team-tokens.html)
- [x] [Teams](https://www.terraform.io/docs/enterprise/api/teams.html)
- [x] [Variables](https://www.terraform.io/docs/enterprise/api/variables.html)
- [x] [Workspaces](https://www.terraform.io/docs/enterprise/api/workspaces.html)
- [ ] [Admin](https://www.terraform.io/docs/enterprise/api/admin/index.html)
## Installation
Installation can be done with a normal `go get`:
```
go get -u github.com/hashicorp/go-tfe
```
## Documentation
For complete usage of the API client, see the full [package docs](https://godoc.org/github.com/hashicorp/go-tfe).
## Usage
```go
import tfe "github.com/hashicorp/go-tfe"
```
Construct a new TFE client, then use the various endpoints on the client to
access different parts of the Terraform Enterprise API. For example, to list
all organizations:
```go
config := &tfe.Config{
Token: "insert-your-token-here",
}
client, err := tfe.NewClient(config)
if err != nil {
log.Fatal(err)
}
orgs, err := client.Organizations.List(context.Background(), OrganizationListOptions{})
if err != nil {
log.Fatal(err)
}
```
## Examples
The [examples](https://github.com/hashicorp/go-tfe/tree/master/examples) directory
contains a couple of examples. One of which is listed here as well:
```go
package main
import (
"log"
tfe "github.com/hashicorp/go-tfe"
)
func main() {
config := &tfe.Config{
Token: "insert-your-token-here",
}
client, err := tfe.NewClient(config)
if err != nil {
log.Fatal(err)
}
// Create a context
ctx := context.Background()
// Create a new organization
options := tfe.OrganizationCreateOptions{
Name: tfe.String("example"),
Email: tfe.String("info@example.com"),
}
org, err := client.Organizations.Create(ctx, options)
if err != nil {
log.Fatal(err)
}
// Delete an organization
err = client.Organizations.Delete(ctx, org.Name)
if err != nil {
log.Fatal(err)
}
}
```
## Issues and Contributing
If you find an issue with this package, please report an issue. If you'd like,
we welcome any contributions. Fork this repository and submit a pull request.

View File

@ -0,0 +1,193 @@
package tfe
import (
"bytes"
"context"
"errors"
"fmt"
"net/url"
"time"
slug "github.com/hashicorp/go-slug"
)
// Compile-time proof of interface implementation.
var _ ConfigurationVersions = (*configurationVersions)(nil)
// ConfigurationVersions describes all the configuration version related
// methods that the Terraform Enterprise API supports.
//
// TFE API docs:
// https://www.terraform.io/docs/enterprise/api/configuration-versions.html
type ConfigurationVersions interface {
// List returns all configuration versions of a workspace.
List(ctx context.Context, workspaceID string, options ConfigurationVersionListOptions) ([]*ConfigurationVersion, error)
// Create is used to create a new configuration version. The created
// configuration version will be usable once data is uploaded to it.
Create(ctx context.Context, workspaceID string, options ConfigurationVersionCreateOptions) (*ConfigurationVersion, error)
// Read a configuration version by its ID.
Read(ctx context.Context, cvID string) (*ConfigurationVersion, error)
// Upload packages and uploads Terraform configuration files. It requires
// the upload URL from a configuration version and the full path to the
// configuration files on disk.
Upload(ctx context.Context, url string, path string) error
}
// configurationVersions implements ConfigurationVersions.
type configurationVersions struct {
client *Client
}
// ConfigurationStatus represents a configuration version status.
type ConfigurationStatus string
//List all available configuration version statuses.
const (
ConfigurationErrored ConfigurationStatus = "errored"
ConfigurationPending ConfigurationStatus = "pending"
ConfigurationUploaded ConfigurationStatus = "uploaded"
)
// ConfigurationSource represents a source of a configuration version.
type ConfigurationSource string
// List all available configuration version sources.
const (
ConfigurationSourceAPI ConfigurationSource = "tfe-api"
ConfigurationSourceBitbucket ConfigurationSource = "bitbucket"
ConfigurationSourceGithub ConfigurationSource = "github"
ConfigurationSourceGitlab ConfigurationSource = "gitlab"
ConfigurationSourceTerraform ConfigurationSource = "terraform"
)
// ConfigurationVersion is a representation of an uploaded or ingressed
// Terraform configuration in TFE. A workspace must have at least one
// configuration version before any runs may be queued on it.
type ConfigurationVersion struct {
ID string `jsonapi:"primary,configuration-versions"`
AutoQueueRuns bool `jsonapi:"attr,auto-queue-runs"`
Error string `jsonapi:"attr,error"`
ErrorMessage string `jsonapi:"attr,error-message"`
Source ConfigurationSource `jsonapi:"attr,source"`
Speculative bool `jsonapi:"attr,speculative "`
Status ConfigurationStatus `jsonapi:"attr,status"`
StatusTimestamps *CVStatusTimestamps `jsonapi:"attr,status-timestamps"`
UploadURL string `jsonapi:"attr,upload-url"`
}
// CVStatusTimestamps holds the timestamps for individual configuration version
// statuses.
type CVStatusTimestamps struct {
FinishedAt time.Time `json:"finished-at"`
QueuedAt time.Time `json:"queued-at"`
StartedAt time.Time `json:"started-at"`
}
// ConfigurationVersionListOptions represents the options for listing
// configuration versions.
type ConfigurationVersionListOptions struct {
ListOptions
}
// List returns all configuration versions of a workspace.
func (s *configurationVersions) List(ctx context.Context, workspaceID string, options ConfigurationVersionListOptions) ([]*ConfigurationVersion, error) {
if !validStringID(&workspaceID) {
return nil, errors.New("Invalid value for workspace ID")
}
u := fmt.Sprintf("workspaces/%s/configuration-versions", url.QueryEscape(workspaceID))
req, err := s.client.newRequest("GET", u, &options)
if err != nil {
return nil, err
}
var cvs []*ConfigurationVersion
err = s.client.do(ctx, req, &cvs)
if err != nil {
return nil, err
}
return cvs, nil
}
// ConfigurationVersionCreateOptions represents the options for creating a
// configuration version.
type ConfigurationVersionCreateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,configuration-versions"`
// When true, runs are queued automatically when the configuration version
// is uploaded.
AutoQueueRuns *bool `jsonapi:"attr,auto-queue-runs,omitempty"`
// When true, this configuration version can only be used for planning.
Speculative *bool `jsonapi:"attr,speculative,omitempty"`
}
// Create is used to create a new configuration version. The created
// configuration version will be usable once data is uploaded to it.
func (s *configurationVersions) Create(ctx context.Context, workspaceID string, options ConfigurationVersionCreateOptions) (*ConfigurationVersion, error) {
if !validStringID(&workspaceID) {
return nil, errors.New("Invalid value for workspace ID")
}
// Make sure we don't send a user provided ID.
options.ID = ""
u := fmt.Sprintf("workspaces/%s/configuration-versions", url.QueryEscape(workspaceID))
req, err := s.client.newRequest("POST", u, &options)
if err != nil {
return nil, err
}
cv := &ConfigurationVersion{}
err = s.client.do(ctx, req, cv)
if err != nil {
return nil, err
}
return cv, nil
}
// Read a configuration version by its ID.
func (s *configurationVersions) Read(ctx context.Context, cvID string) (*ConfigurationVersion, error) {
if !validStringID(&cvID) {
return nil, errors.New("Invalid value for configuration version ID")
}
u := fmt.Sprintf("configuration-versions/%s", url.QueryEscape(cvID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
cv := &ConfigurationVersion{}
err = s.client.do(ctx, req, cv)
if err != nil {
return nil, err
}
return cv, nil
}
// Upload packages and uploads Terraform configuration files. It requires the
// upload URL from a configuration version and the path to the configuration
// files on disk.
func (s *configurationVersions) Upload(ctx context.Context, url, path string) error {
body := bytes.NewBuffer(nil)
_, err := slug.Pack(path, body)
if err != nil {
return err
}
req, err := s.client.newRequest("PUT", url, body)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}

127
vendor/github.com/hashicorp/go-tfe/oauth_client.go generated vendored Normal file
View File

@ -0,0 +1,127 @@
package tfe
import (
"context"
"errors"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ OAuthClients = (*oAuthClients)(nil)
// OAuthClients describes all the OAuth client related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://www.terraform.io/docs/enterprise/api/oauth-clients.html
type OAuthClients interface {
// Create a VCS connection between an organization and a VCS provider.
Create(ctx context.Context, organization string, options OAuthClientCreateOptions) (*OAuthClient, error)
}
// oAuthClients implements OAuthClients.
type oAuthClients struct {
client *Client
}
// ServiceProviderType represents a VCS type.
type ServiceProviderType string
// List of available VCS types.
const (
ServiceProviderBitbucket ServiceProviderType = "bitbucket_hosted"
ServiceProviderBitbucketServer ServiceProviderType = "bitbucket_server"
ServiceProviderGithub ServiceProviderType = "github"
ServiceProviderGithubEE ServiceProviderType = "github_enterprise"
ServiceProviderGitlab ServiceProviderType = "gitlab_hosted"
ServiceProviderGitlabCE ServiceProviderType = "gitlab_community_edition"
ServiceProviderGitlabEE ServiceProviderType = "gitlab_enterprise_edition"
)
// OAuthClient represents a connection between an organization and a VCS
// provider.
type OAuthClient struct {
ID string `jsonapi:"primary,oauth-clients"`
APIURL string `jsonapi:"attr,api-url"`
CallbackURL string `jsonapi:"attr,callback-url"`
ConnectPath string `jsonapi:"attr,connect-path"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
HTTPURL string `jsonapi:"attr,http-url"`
Key string `jsonapi:"attr,key"`
RSAPublicKey string `jsonapi:"attr,rsa-public-key"`
ServiceProvider ServiceProviderType `jsonapi:"attr,service-provider"`
ServiceProviderName string `jsonapi:"attr,service-provider-display-name"`
// Relations
Organization *Organization `jsonapi:"relation,organization"`
OAuthToken []*OAuthToken `jsonapi:"relation,oauth-token"`
}
// OAuthClientCreateOptions represents the options for creating an OAuth client.
type OAuthClientCreateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,oauth-clients"`
// The base URL of your VCS provider's API.
APIURL *string `jsonapi:"attr,api-url"`
// The homepage of your VCS provider.
HTTPURL *string `jsonapi:"attr,http-url"`
// The key you were given by your VCS provider.
Key *string `jsonapi:"attr,key"`
// The secret you were given by your VCS provider.
Secret *string `jsonapi:"attr,secret"`
// The VCS provider being connected with.
ServiceProvider *ServiceProviderType `jsonapi:"attr,service-provider"`
}
func (o OAuthClientCreateOptions) valid() error {
if !validString(o.APIURL) {
return errors.New("APIURL is required")
}
if !validString(o.HTTPURL) {
return errors.New("HTTPURL is required")
}
if !validString(o.Key) {
return errors.New("Key is required")
}
if !validString(o.Secret) {
return errors.New("Secret is required")
}
if o.ServiceProvider == nil {
return errors.New("ServiceProvider is required")
}
return nil
}
// Create a VCS connection between an organization and a VCS provider.
func (s *oAuthClients) Create(ctx context.Context, organization string, options OAuthClientCreateOptions) (*OAuthClient, error) {
if !validStringID(&organization) {
return nil, errors.New("Invalid value for organization")
}
if err := options.valid(); err != nil {
return nil, err
}
// Make sure we don't send a user provided ID.
options.ID = ""
u := fmt.Sprintf("organizations/%s/oauth-clients", url.QueryEscape(organization))
req, err := s.client.newRequest("POST", u, &options)
if err != nil {
return nil, err
}
oc := &OAuthClient{}
err = s.client.do(ctx, req, oc)
if err != nil {
return nil, err
}
return oc, nil
}

61
vendor/github.com/hashicorp/go-tfe/oauth_token.go generated vendored Normal file
View File

@ -0,0 +1,61 @@
package tfe
import (
"context"
"errors"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ OAuthTokens = (*oAuthTokens)(nil)
// OAuthTokens describes all the OAuth token related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://www.terraform.io/docs/enterprise/api/oauth-tokens.html
type OAuthTokens interface {
// List all the OAuth Tokens for a given organization.
List(ctx context.Context, organization string) ([]*OAuthToken, error)
}
// oAuthTokens implements OAuthTokens.
type oAuthTokens struct {
client *Client
}
// OAuthToken represents a VCS configuration including the associated
// OAuth token
type OAuthToken struct {
ID string `jsonapi:"primary,oauth-tokens"`
UID string `jsonapi:"attr,uid"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
HasSSHKey bool `jsonapi:"attr,has-ssh-key"`
ServiceProviderUser string `jsonapi:"attr,service-provider-user"`
// Relations
OAuthClient *OAuthClient `jsonapi:"relation,oauth-client"`
}
// List all the OAuth Tokens for a given organization.
func (s *oAuthTokens) List(ctx context.Context, organization string) ([]*OAuthToken, error) {
if !validStringID(&organization) {
return nil, errors.New("Invalid value for organization")
}
u := fmt.Sprintf("organizations/%s/oauth-tokens", url.QueryEscape(organization))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
var ots []*OAuthToken
err = s.client.do(ctx, req, &ots)
if err != nil {
return nil, err
}
return ots, nil
}

238
vendor/github.com/hashicorp/go-tfe/organization.go generated vendored Normal file
View File

@ -0,0 +1,238 @@
package tfe
import (
"context"
"errors"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ Organizations = (*organizations)(nil)
// Organizations describes all the organization related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://www.terraform.io/docs/enterprise/api/organizations.html
type Organizations interface {
// List all the organizations visible to the current user.
List(ctx context.Context, options OrganizationListOptions) ([]*Organization, error)
// Create a new organization with the given options.
Create(ctx context.Context, options OrganizationCreateOptions) (*Organization, error)
// Read an organization by its name.
Read(ctx context.Context, organization string) (*Organization, error)
// Update attributes of an existing organization.
Update(ctx context.Context, organization string, options OrganizationUpdateOptions) (*Organization, error)
// Delete an organization by its name.
Delete(ctx context.Context, organization string) error
}
// organizations implements Organizations.
type organizations struct {
client *Client
}
// AuthPolicyType represents an authentication policy type.
type AuthPolicyType string
// List of available authentication policies.
const (
AuthPolicyPassword AuthPolicyType = "password"
AuthPolicyTwoFactor AuthPolicyType = "two_factor_mandatory"
)
// EnterprisePlanType represents an enterprise plan type.
type EnterprisePlanType string
// List of available enterprise plan types.
const (
EnterprisePlanDisabled EnterprisePlanType = "disabled"
EnterprisePlanPremium EnterprisePlanType = "premium"
EnterprisePlanPro EnterprisePlanType = "pro"
EnterprisePlanTrial EnterprisePlanType = "trial"
)
// Organization represents a Terraform Enterprise organization.
type Organization struct {
Name string `jsonapi:"primary,organizations"`
CollaboratorAuthPolicy AuthPolicyType `jsonapi:"attr,collaborator-auth-policy"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Email string `jsonapi:"attr,email"`
EnterprisePlan EnterprisePlanType `jsonapi:"attr,enterprise-plan"`
OwnersTeamSamlRoleID string `jsonapi:"attr,owners-team-saml-role-id"`
Permissions *OrganizationPermissions `jsonapi:"attr,permissions"`
SAMLEnabled bool `jsonapi:"attr,saml-enabled"`
SessionRemember int `jsonapi:"attr,session-remember"`
SessionTimeout int `jsonapi:"attr,session-timeout"`
TrialExpiresAt time.Time `jsonapi:"attr,trial-expires-at,iso8601"`
TwoFactorConformant bool `jsonapi:"attr,two-factor-conformant"`
}
// OrganizationPermissions represents the organization permissions.
type OrganizationPermissions struct {
CanCreateTeam bool `json:"can-create-team"`
CanCreateWorkspace bool `json:"can-create-workspace"`
CanCreateWorkspaceMigration bool `json:"can-create-workspace-migration"`
CanDestroy bool `json:"can-destroy"`
CanTraverse bool `json:"can-traverse"`
CanUpdate bool `json:"can-update"`
CanUpdateAPIToken bool `json:"can-update-api-token"`
CanUpdateOAuth bool `json:"can-update-oauth"`
CanUpdateSentinel bool `json:"can-update-sentinel"`
}
// OrganizationListOptions represents the options for listing organizations.
type OrganizationListOptions struct {
ListOptions
}
// List all the organizations visible to the current user.
func (s *organizations) List(ctx context.Context, options OrganizationListOptions) ([]*Organization, error) {
req, err := s.client.newRequest("GET", "organizations", &options)
if err != nil {
return nil, err
}
var orgs []*Organization
err = s.client.do(ctx, req, &orgs)
if err != nil {
return nil, err
}
return orgs, nil
}
// OrganizationCreateOptions represents the options for creating an organization.
type OrganizationCreateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,organizations"`
// Name of the organization.
Name *string `jsonapi:"attr,name"`
// Admin email address.
Email *string `jsonapi:"attr,email"`
}
func (o OrganizationCreateOptions) valid() error {
if !validString(o.Name) {
return errors.New("Name is required")
}
if !validStringID(o.Name) {
return errors.New("Invalid value for name")
}
if !validString(o.Email) {
return errors.New("Email is required")
}
return nil
}
// Create a new organization with the given options.
func (s *organizations) Create(ctx context.Context, options OrganizationCreateOptions) (*Organization, error) {
if err := options.valid(); err != nil {
return nil, err
}
// Make sure we don't send a user provided ID.
options.ID = ""
req, err := s.client.newRequest("POST", "organizations", &options)
if err != nil {
return nil, err
}
org := &Organization{}
err = s.client.do(ctx, req, org)
if err != nil {
return nil, err
}
return org, nil
}
// Read an organization by its name.
func (s *organizations) Read(ctx context.Context, organization string) (*Organization, error) {
if !validStringID(&organization) {
return nil, errors.New("Invalid value for organization")
}
u := fmt.Sprintf("organizations/%s", url.QueryEscape(organization))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
org := &Organization{}
err = s.client.do(ctx, req, org)
if err != nil {
return nil, err
}
return org, nil
}
// OrganizationUpdateOptions represents the options for updating an organization.
type OrganizationUpdateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,organizations"`
// New name for the organization.
Name *string `jsonapi:"attr,name,omitempty"`
// New admin email address.
Email *string `jsonapi:"attr,email,omitempty"`
// Session expiration (minutes).
SessionRemember *int `jsonapi:"attr,session-remember,omitempty"`
// Session timeout after inactivity (minutes).
SessionTimeout *int `jsonapi:"attr,session-timeout,omitempty"`
// Authentication policy.
CollaboratorAuthPolicy *AuthPolicyType `jsonapi:"attr,collaborator-auth-policy,omitempty"`
}
// Update attributes of an existing organization.
func (s *organizations) Update(ctx context.Context, organization string, options OrganizationUpdateOptions) (*Organization, error) {
if !validStringID(&organization) {
return nil, errors.New("Invalid value for organization")
}
// Make sure we don't send a user provided ID.
options.ID = ""
u := fmt.Sprintf("organizations/%s", url.QueryEscape(organization))
req, err := s.client.newRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
org := &Organization{}
err = s.client.do(ctx, req, org)
if err != nil {
return nil, err
}
return org, nil
}
// Delete an organization by its name.
func (s *organizations) Delete(ctx context.Context, organization string) error {
if !validStringID(&organization) {
return errors.New("Invalid value for organization")
}
u := fmt.Sprintf("organizations/%s", url.QueryEscape(organization))
req, err := s.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}

View File

@ -0,0 +1,75 @@
package tfe
import (
"context"
"errors"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ OrganizationTokens = (*organizationTokens)(nil)
// OrganizationTokens describes all the organization token related methods
// that the Terraform Enterprise API supports.
//
// TFE API docs:
// https://www.terraform.io/docs/enterprise/api/organization-tokens.html
type OrganizationTokens interface {
// Generate a new organization token, replacing any existing token.
Generate(ctx context.Context, organization string) (*OrganizationToken, error)
// Delete an organization token.
Delete(ctx context.Context, organization string) error
}
// organizationTokens implements OrganizationTokens.
type organizationTokens struct {
client *Client
}
// OrganizationToken represents a Terraform Enterprise organization token.
type OrganizationToken struct {
ID string `jsonapi:"primary,authentication-tokens"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Description string `jsonapi:"attr,description"`
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
Token string `jsonapi:"attr,token"`
}
// Generate a new organization token, replacing any existing token.
func (s *organizationTokens) Generate(ctx context.Context, organization string) (*OrganizationToken, error) {
if !validStringID(&organization) {
return nil, errors.New("Invalid value for organization")
}
u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization))
req, err := s.client.newRequest("POST", u, nil)
if err != nil {
return nil, err
}
ot := &OrganizationToken{}
err = s.client.do(ctx, req, ot)
if err != nil {
return nil, err
}
return ot, err
}
// Delete an organization token.
func (s *organizationTokens) Delete(ctx context.Context, organization string) error {
if !validStringID(&organization) {
return errors.New("Invalid value for organization")
}
u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization))
req, err := s.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}

206
vendor/github.com/hashicorp/go-tfe/plan.go generated vendored Normal file
View File

@ -0,0 +1,206 @@
package tfe
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ Plans = (*plans)(nil)
// Plans describes all the plan related methods that the Terraform Enterprise
// API supports.
//
// TFE API docs: https://www.terraform.io/docs/enterprise/api/plan.html
type Plans interface {
// Read a plan by its ID.
Read(ctx context.Context, planID string) (*Plan, error)
// Logs retrieves the logs of a plan.
Logs(ctx context.Context, planID string) (io.Reader, error)
}
// plans implements Plans.
type plans struct {
client *Client
}
// PlanStatus represents a plan state.
type PlanStatus string
//List all available plan statuses.
const (
PlanCanceled PlanStatus = "canceled"
PlanCreated PlanStatus = "created"
PlanErrored PlanStatus = "errored"
PlanFinished PlanStatus = "finished"
PlanMFAWaiting PlanStatus = "mfa_waiting"
PlanPending PlanStatus = "pending"
PlanQueued PlanStatus = "queued"
PlanRunning PlanStatus = "running"
)
// Plan represents a Terraform Enterprise plan.
type Plan struct {
ID string `jsonapi:"primary,plans"`
HasChanges bool `jsonapi:"attr,has-changes"`
LogReadURL string `jsonapi:"attr,log-read-url"`
Status PlanStatus `jsonapi:"attr,status"`
StatusTimestamps *PlanStatusTimestamps `jsonapi:"attr,status-timestamps"`
}
// PlanStatusTimestamps holds the timestamps for individual plan statuses.
type PlanStatusTimestamps struct {
CanceledAt time.Time `json:"canceled-at"`
CreatedAt time.Time `json:"created-at"`
ErroredAt time.Time `json:"errored-at"`
FinishedAt time.Time `json:"finished-at"`
MFAWaitingAt time.Time `json:"mfa_waiting-at"`
PendingAt time.Time `json:"pending-at"`
QueuedAt time.Time `json:"queued-at"`
RunningAt time.Time `json:"running-at"`
}
// Read a plan by its ID.
func (s *plans) Read(ctx context.Context, planID string) (*Plan, error) {
if !validStringID(&planID) {
return nil, errors.New("Invalid value for plan ID")
}
u := fmt.Sprintf("plans/%s", url.QueryEscape(planID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
p := &Plan{}
err = s.client.do(ctx, req, p)
if err != nil {
return nil, err
}
return p, nil
}
// Logs retrieves the logs of a plan.
func (s *plans) Logs(ctx context.Context, planID string) (io.Reader, error) {
if !validStringID(&planID) {
return nil, errors.New("Invalid value for plan ID")
}
// Get the plan to make sure it exists.
p, err := s.Read(ctx, planID)
if err != nil {
return nil, err
}
// Return an error if the log URL is empty.
if p.LogReadURL == "" {
return nil, fmt.Errorf("Plan %s does not have a log URL", planID)
}
u, err := url.Parse(p.LogReadURL)
if err != nil {
return nil, fmt.Errorf("Invalid log URL: %v", err)
}
return &LogReader{
client: s.client,
ctx: ctx,
logURL: u,
plan: p,
}, nil
}
// LogReader implements io.Reader for streaming plan logs.
type LogReader struct {
client *Client
ctx context.Context
logURL *url.URL
offset int64
plan *Plan
reads uint64
}
func (r *LogReader) Read(l []byte) (int, error) {
if written, err := r.read(l); err != io.ErrNoProgress {
return written, err
}
// Loop until we can any data, the context is canceled or the plan
// is finsished running. If we would return right away without any
// data, we could and up causing a io.ErrNoProgress error.
for {
select {
case <-r.ctx.Done():
return 0, r.ctx.Err()
case <-time.After(500 * time.Millisecond):
if written, err := r.read(l); err != io.ErrNoProgress {
return written, err
}
}
}
}
func (r *LogReader) read(l []byte) (int, error) {
// Update the query string.
r.logURL.RawQuery = fmt.Sprintf("limit=%d&offset=%d", len(l), r.offset)
// Create a new request.
req, err := http.NewRequest("GET", r.logURL.String(), nil)
if err != nil {
return 0, err
}
req = req.WithContext(r.ctx)
// Retrieve the next chunk.
resp, err := r.client.http.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
// Basic response checking.
if err := checkResponseCode(resp); err != nil {
return 0, err
}
// Check if we need to continue the loop and wait 500 miliseconds
// before checking if there is a new chunk available or that the
// plan is finished and we are done reading all chunks.
if resp.ContentLength == 0 {
if r.reads%2 == 0 {
r.plan, err = r.client.Plans.Read(r.ctx, r.plan.ID)
if err != nil {
return 0, err
}
}
switch r.plan.Status {
case PlanCanceled, PlanErrored, PlanFinished:
return 0, io.EOF
default:
r.reads++
return 0, io.ErrNoProgress
}
}
// Read the retrieved chunk.
written, err := resp.Body.Read(l)
if err == io.EOF {
// Ignore io.EOF errors returned when reading from the response
// body as this indicates the end of the chunk and not the end
// of the logfile.
err = nil
}
// Update the offset for the next read.
r.offset += int64(written)
return written, err
}

251
vendor/github.com/hashicorp/go-tfe/policy.go generated vendored Normal file
View File

@ -0,0 +1,251 @@
package tfe
import (
"context"
"errors"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ Policies = (*policies)(nil)
// Policies describes all the policy related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://www.terraform.io/docs/enterprise/api/policies.html
type Policies interface {
// List all the policies for a given organization
List(ctx context.Context, organization string, options PolicyListOptions) ([]*Policy, error)
// Create a policy and associate it with an organization.
Create(ctx context.Context, organization string, options PolicyCreateOptions) (*Policy, error)
// Read a policy by its ID.
Read(ctx context.Context, policyID string) (*Policy, error)
// Upload the policy content of the policy.
Upload(ctx context.Context, policyID string, content []byte) error
// Update an existing policy.
Update(ctx context.Context, policyID string, options PolicyUpdateOptions) (*Policy, error)
// Delete a policy by its ID.
Delete(ctx context.Context, policyID string) error
}
// policies implements Policies.
type policies struct {
client *Client
}
// EnforcementLevel represents an enforcement level.
type EnforcementLevel string
// List the available enforcement types.
const (
EnforcementAdvisory EnforcementLevel = "advisory"
EnforcementHard EnforcementLevel = "hard-mandatory"
EnforcementSoft EnforcementLevel = "soft-mandatory"
)
// Policy represents a Terraform Enterprise policy.
type Policy struct {
ID string `jsonapi:"primary,policies"`
Name string `jsonapi:"attr,name"`
Enforce []*Enforcement `jsonapi:"attr,enforce"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
}
// Enforcement describes a enforcement.
type Enforcement struct {
Path string `json:"path"`
Mode EnforcementLevel `json:"mode"`
}
// PolicyListOptions represents the options for listing policies.
type PolicyListOptions struct {
ListOptions
}
// List all the policies for a given organization
func (s *policies) List(ctx context.Context, organization string, options PolicyListOptions) ([]*Policy, error) {
if !validStringID(&organization) {
return nil, errors.New("Invalid value for organization")
}
u := fmt.Sprintf("organizations/%s/policies", url.QueryEscape(organization))
req, err := s.client.newRequest("GET", u, &options)
if err != nil {
return nil, err
}
var ps []*Policy
err = s.client.do(ctx, req, &ps)
if err != nil {
return nil, err
}
return ps, nil
}
// PolicyCreateOptions represents the options for creating a new policy.
type PolicyCreateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,policies"`
// The name of the policy.
Name *string `jsonapi:"attr,name"`
// The enforcements of the policy.
Enforce []*EnforcementOptions `jsonapi:"attr,enforce"`
}
// EnforcementOptions represents the enforcement options of a policy.
type EnforcementOptions struct {
Path *string `json:"path,omitempty"`
Mode *EnforcementLevel `json:"mode"`
}
func (o PolicyCreateOptions) valid() error {
if !validString(o.Name) {
return errors.New("Name is required")
}
if !validStringID(o.Name) {
return errors.New("Invalid value for name")
}
if o.Enforce == nil {
return errors.New("Enforce is required")
}
for _, e := range o.Enforce {
if !validString(e.Path) {
return errors.New("Enforcement path is required")
}
if e.Mode == nil {
return errors.New("Enforcement mode is required")
}
}
return nil
}
// Create a policy and associate it with an organization.
func (s *policies) Create(ctx context.Context, organization string, options PolicyCreateOptions) (*Policy, error) {
if !validStringID(&organization) {
return nil, errors.New("Invalid value for organization")
}
if err := options.valid(); err != nil {
return nil, err
}
// Make sure we don't send a user provided ID.
options.ID = ""
u := fmt.Sprintf("organizations/%s/policies", url.QueryEscape(organization))
req, err := s.client.newRequest("POST", u, &options)
if err != nil {
return nil, err
}
p := &Policy{}
err = s.client.do(ctx, req, p)
if err != nil {
return nil, err
}
return p, err
}
// Read a policy by its ID.
func (s *policies) Read(ctx context.Context, policyID string) (*Policy, error) {
if !validStringID(&policyID) {
return nil, errors.New("Invalid value for policy ID")
}
u := fmt.Sprintf("policies/%s", url.QueryEscape(policyID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
p := &Policy{}
err = s.client.do(ctx, req, p)
if err != nil {
return nil, err
}
return p, err
}
// Upload the policy content of the policy.
func (s *policies) Upload(ctx context.Context, policyID string, content []byte) error {
if !validStringID(&policyID) {
return errors.New("Invalid value for policy ID")
}
u := fmt.Sprintf("policies/%s/upload", url.QueryEscape(policyID))
req, err := s.client.newRequest("PUT", u, content)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}
// PolicyUpdateOptions represents the options for updating a policy.
type PolicyUpdateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,policies"`
// The enforcements of the policy.
Enforce []*EnforcementOptions `jsonapi:"attr,enforce"`
}
func (o PolicyUpdateOptions) valid() error {
if o.Enforce == nil {
return errors.New("Enforce is required")
}
return nil
}
// Update an existing policy.
func (s *policies) Update(ctx context.Context, policyID string, options PolicyUpdateOptions) (*Policy, error) {
if !validStringID(&policyID) {
return nil, errors.New("Invalid value for policy ID")
}
if err := options.valid(); err != nil {
return nil, err
}
// Make sure we don't send a user provided ID.
options.ID = ""
u := fmt.Sprintf("policies/%s", url.QueryEscape(policyID))
req, err := s.client.newRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
p := &Policy{}
err = s.client.do(ctx, req, p)
if err != nil {
return nil, err
}
return p, err
}
// Delete a policy by its ID.
func (s *policies) Delete(ctx context.Context, policyID string) error {
if !validStringID(&policyID) {
return errors.New("Invalid value for policy ID")
}
u := fmt.Sprintf("policies/%s", url.QueryEscape(policyID))
req, err := s.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}

143
vendor/github.com/hashicorp/go-tfe/policy_check.go generated vendored Normal file
View File

@ -0,0 +1,143 @@
package tfe
import (
"context"
"errors"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ PolicyChecks = (*policyChecks)(nil)
// PolicyChecks describes all the policy check related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://www.terraform.io/docs/enterprise/api/policy-checks.html
type PolicyChecks interface {
// List all policy checks of the given run.
List(ctx context.Context, runID string, options PolicyCheckListOptions) ([]*PolicyCheck, error)
// Override a soft-mandatory or warning policy.
Override(ctx context.Context, policyCheckID string) (*PolicyCheck, error)
}
// policyChecks implements PolicyChecks.
type policyChecks struct {
client *Client
}
// PolicyScope represents a policy scope.
type PolicyScope string
// List all available policy scopes.
const (
PolicyScopeOrganization PolicyScope = "organization"
PolicyScopeWorkspace PolicyScope = "workspace"
)
// PolicyStatus represents a policy check state.
type PolicyStatus string
//List all available policy check statuses.
const (
PolicyErrored PolicyStatus = "errored"
PolicyHardFailed PolicyStatus = "hard_failed"
PolicyOverridden PolicyStatus = "overridden"
PolicyPasses PolicyStatus = "passed"
PolicyPending PolicyStatus = "pending"
PolicyQueued PolicyStatus = "queued"
PolicySoftFailed PolicyStatus = "soft_failed"
)
// PolicyCheck represents a Terraform Enterprise policy check..
type PolicyCheck struct {
ID string `jsonapi:"primary,policy-checks"`
Actions *PolicyActions `jsonapi:"attr,actions"`
Permissions *PolicyPermissions `jsonapi:"attr,permissions"`
Result *PolicyResult `jsonapi:"attr,result"`
Scope PolicyScope `jsonapi:"attr,source"`
Status PolicyStatus `jsonapi:"attr,status"`
StatusTimestamps *PolicyStatusTimestamps `jsonapi:"attr,status-timestamps"`
}
// PolicyActions represents the policy check actions.
type PolicyActions struct {
IsOverridable bool `json:"is-overridable"`
}
// PolicyPermissions represents the policy check permissions.
type PolicyPermissions struct {
CanOverride bool `json:"can-override"`
}
// PolicyResult represents the complete policy check result,
type PolicyResult struct {
AdvisoryFailed int `json:"advisory-failed"`
Duration int `json:"duration"`
HardFailed int `json:"hard-failed"`
Passed int `json:"passed"`
Result bool `json:"result"`
// Sentinel *sentinel.EvalResult `json:"sentinel"`
SoftFailed int `json:"soft-failed"`
TotalFailed int `json:"total-failed"`
}
// PolicyStatusTimestamps holds the timestamps for individual policy check
// statuses.
type PolicyStatusTimestamps struct {
ErroredAt time.Time `json:"errored-at"`
HardFailedAt time.Time `json:"hard-failed-at"`
PassedAt time.Time `json:"passed-at"`
QueuedAt time.Time `json:"queued-at"`
SoftFailedAt time.Time `json:"soft-failed-at"`
}
// PolicyCheckListOptions represents the options for listing policy checks.
type PolicyCheckListOptions struct {
ListOptions
}
// List all policy checks of the given run.
func (s *policyChecks) List(ctx context.Context, runID string, options PolicyCheckListOptions) ([]*PolicyCheck, error) {
if !validStringID(&runID) {
return nil, errors.New("Invalid value for run ID")
}
u := fmt.Sprintf("runs/%s/policy-checks", url.QueryEscape(runID))
req, err := s.client.newRequest("GET", u, &options)
if err != nil {
return nil, err
}
var pcs []*PolicyCheck
err = s.client.do(ctx, req, &pcs)
if err != nil {
return nil, err
}
return pcs, nil
}
// Override a soft-mandatory or warning policy.
func (s *policyChecks) Override(ctx context.Context, policyCheckID string) (*PolicyCheck, error) {
if !validStringID(&policyCheckID) {
return nil, errors.New("Invalid value for policy check ID")
}
u := fmt.Sprintf("policy-checks/%s/actions/override", url.QueryEscape(policyCheckID))
req, err := s.client.newRequest("POST", u, nil)
if err != nil {
return nil, err
}
pc := &PolicyCheck{}
err = s.client.do(ctx, req, pc)
if err != nil {
return nil, err
}
return pc, nil
}

273
vendor/github.com/hashicorp/go-tfe/run.go generated vendored Normal file
View File

@ -0,0 +1,273 @@
package tfe
import (
"context"
"errors"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ Runs = (*runs)(nil)
// Runs describes all the run related methods that the Terraform Enterprise
// API supports.
//
// TFE API docs: https://www.terraform.io/docs/enterprise/api/run.html
type Runs interface {
// List all the runs of the given workspace.
List(ctx context.Context, workspaceID string, options RunListOptions) ([]*Run, error)
// Create a new run with the given options.
Create(ctx context.Context, options RunCreateOptions) (*Run, error)
// Read a run by its ID.
Read(ctx context.Context, runID string) (*Run, error)
// Apply a run by its ID.
Apply(ctx context.Context, runID string, options RunApplyOptions) error
// Cancel a run by its ID.
Cancel(ctx context.Context, runID string, options RunCancelOptions) error
// Discard a run by its ID.
Discard(ctx context.Context, runID string, options RunDiscardOptions) error
}
// runs implements Runs.
type runs struct {
client *Client
}
// RunStatus represents a run state.
type RunStatus string
//List all available run statuses.
const (
RunApplied RunStatus = "applied"
RunApplying RunStatus = "applying"
RunCanceled RunStatus = "canceled"
RunConfirmed RunStatus = "confirmed"
RunDiscarded RunStatus = "discarded"
RunErrored RunStatus = "errored"
RunPending RunStatus = "pending"
RunPlanned RunStatus = "planned"
RunPlanning RunStatus = "planning"
RunPolicyChecked RunStatus = "policy_checked"
RunPolicyChecking RunStatus = "policy_checking"
RunPolicyOverride RunStatus = "policy_override"
)
// RunSource represents a source type of a run.
type RunSource string
// List all available run sources.
const (
RunSourceAPI RunSource = "tfe-api"
RunSourceConfigurationVersion RunSource = "tfe-configuration-version"
RunSourceUI RunSource = "tfe-ui"
)
// Run represents a Terraform Enterprise run.
type Run struct {
ID string `jsonapi:"primary,runs"`
Actions *RunActions `jsonapi:"attr,actions"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
HasChanges bool `jsonapi:"attr,has-changes"`
IsDestroy bool `jsonapi:"attr,is-destroy"`
Message string `jsonapi:"attr,message"`
Permissions *RunPermissions `jsonapi:"attr,permissions"`
Source RunSource `jsonapi:"attr,source"`
Status RunStatus `jsonapi:"attr,status"`
StatusTimestamps *RunStatusTimestamps `jsonapi:"attr,status-timestamps"`
// Relations
ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"`
Plan *Plan `jsonapi:"relation,plan"`
Workspace *Workspace `jsonapi:"relation,workspace"`
}
// RunActions represents the run actions.
type RunActions struct {
IsCancelable bool `json:"is-cancelable"`
IsComfirmable bool `json:"is-comfirmable"`
IsDiscardable bool `json:"is-discardable"`
}
// RunPermissions represents the run permissions.
type RunPermissions struct {
CanApply bool `json:"can-apply"`
CanCancel bool `json:"can-cancel"`
CanDiscard bool `json:"can-discard"`
CanForceExecute bool `json:"can-force-execute"`
}
// RunStatusTimestamps holds the timestamps for individual run statuses.
type RunStatusTimestamps struct {
ErroredAt time.Time `json:"errored-at"`
FinishedAt time.Time `json:"finished-at"`
QueuedAt time.Time `json:"queued-at"`
StartedAt time.Time `json:"started-at"`
}
// RunListOptions represents the options for listing runs.
type RunListOptions struct {
ListOptions
}
// List all the runs of the given workspace.
func (s *runs) List(ctx context.Context, workspaceID string, options RunListOptions) ([]*Run, error) {
if !validStringID(&workspaceID) {
return nil, errors.New("Invalid value for workspace ID")
}
u := fmt.Sprintf("workspaces/%s/runs", url.QueryEscape(workspaceID))
req, err := s.client.newRequest("GET", u, &options)
if err != nil {
return nil, err
}
var rs []*Run
err = s.client.do(ctx, req, &rs)
if err != nil {
return nil, err
}
return rs, nil
}
// RunCreateOptions represents the options for creating a new run.
type RunCreateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,runs"`
// Specifies if this plan is a destroy plan, which will destroy all
// provisioned resources.
IsDestroy *bool `jsonapi:"attr,is-destroy,omitempty"`
// Specifies the message to be associated with this run.
Message *string `jsonapi:"attr,message,omitempty"`
// Specifies the configuration version to use for this run. If the
// configuration version object is omitted, the run will be created using the
// workspace's latest configuration version.
ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"`
// Specifies the workspace where the run will be executed.
Workspace *Workspace `jsonapi:"relation,workspace"`
}
func (o RunCreateOptions) valid() error {
if o.Workspace == nil {
return errors.New("Workspace is required")
}
return nil
}
// Create a new run with the given options.
func (s *runs) Create(ctx context.Context, options RunCreateOptions) (*Run, error) {
if err := options.valid(); err != nil {
return nil, err
}
// Make sure we don't send a user provided ID.
options.ID = ""
req, err := s.client.newRequest("POST", "runs", &options)
if err != nil {
return nil, err
}
r := &Run{}
err = s.client.do(ctx, req, r)
if err != nil {
return nil, err
}
return r, nil
}
// Read a run by its ID.
func (s *runs) Read(ctx context.Context, runID string) (*Run, error) {
if !validStringID(&runID) {
return nil, errors.New("Invalid value for run ID")
}
u := fmt.Sprintf("runs/%s", url.QueryEscape(runID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
r := &Run{}
err = s.client.do(ctx, req, r)
if err != nil {
return nil, err
}
return r, nil
}
// RunApplyOptions represents the options for applying a run.
type RunApplyOptions struct {
// An optional comment about the run.
Comment *string `json:"comment,omitempty"`
}
// Apply a run by its ID.
func (s *runs) Apply(ctx context.Context, runID string, options RunApplyOptions) error {
if !validStringID(&runID) {
return errors.New("Invalid value for run ID")
}
u := fmt.Sprintf("runs/%s/actions/apply", url.QueryEscape(runID))
req, err := s.client.newRequest("POST", u, &options)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}
// RunCancelOptions represents the options for canceling a run.
type RunCancelOptions struct {
// An optional explanation for why the run was canceled.
Comment *string `json:"comment,omitempty"`
}
// Cancel a run by its ID.
func (s *runs) Cancel(ctx context.Context, runID string, options RunCancelOptions) error {
if !validStringID(&runID) {
return errors.New("Invalid value for run ID")
}
u := fmt.Sprintf("runs/%s/actions/cancel", url.QueryEscape(runID))
req, err := s.client.newRequest("POST", u, &options)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}
// RunDiscardOptions represents the options for discarding a run.
type RunDiscardOptions struct {
// An optional explanation for why the run was discarded.
Comment *string `json:"comment,omitempty"`
}
// Discard a run by its ID.
func (s *runs) Discard(ctx context.Context, runID string, options RunDiscardOptions) error {
if !validStringID(&runID) {
return errors.New("Invalid value for run ID")
}
u := fmt.Sprintf("runs/%s/actions/discard", url.QueryEscape(runID))
req, err := s.client.newRequest("POST", u, &options)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}

192
vendor/github.com/hashicorp/go-tfe/ssh_key.go generated vendored Normal file
View File

@ -0,0 +1,192 @@
package tfe
import (
"context"
"errors"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ SSHKeys = (*sshKeys)(nil)
// SSHKeys describes all the SSH key related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs:
// https://www.terraform.io/docs/enterprise/api/ssh-keys.html
type SSHKeys interface {
// List all the SSH keys for a given organization
List(ctx context.Context, organization string, options SSHKeyListOptions) ([]*SSHKey, error)
// Create an SSH key and associate it with an organization.
Create(ctx context.Context, organization string, options SSHKeyCreateOptions) (*SSHKey, error)
// Read an SSH key by its ID.
Read(ctx context.Context, sshKeyID string) (*SSHKey, error)
// Update an SSH key by its ID.
Update(ctx context.Context, sshKeyID string, options SSHKeyUpdateOptions) (*SSHKey, error)
// Delete an SSH key by its ID.
Delete(ctx context.Context, sshKeyID string) error
}
// sshKeys implements SSHKeys.
type sshKeys struct {
client *Client
}
// SSHKey represents a SSH key.
type SSHKey struct {
ID string `jsonapi:"primary,ssh-keys"`
Name string `jsonapi:"attr,name"`
}
// SSHKeyListOptions represents the options for listing SSH keys.
type SSHKeyListOptions struct {
ListOptions
}
// List all the SSH keys for a given organization
func (s *sshKeys) List(ctx context.Context, organization string, options SSHKeyListOptions) ([]*SSHKey, error) {
if !validStringID(&organization) {
return nil, errors.New("Invalid value for organization")
}
u := fmt.Sprintf("organizations/%s/ssh-keys", url.QueryEscape(organization))
req, err := s.client.newRequest("GET", u, &options)
if err != nil {
return nil, err
}
var ks []*SSHKey
err = s.client.do(ctx, req, &ks)
if err != nil {
return nil, err
}
return ks, nil
}
// SSHKeyCreateOptions represents the options for creating an SSH key.
type SSHKeyCreateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,ssh-keys"`
// A name to identify the SSH key.
Name *string `jsonapi:"attr,name"`
// The content of the SSH private key.
Value *string `jsonapi:"attr,value"`
}
func (o SSHKeyCreateOptions) valid() error {
if !validString(o.Name) {
return errors.New("Name is required")
}
if !validString(o.Value) {
return errors.New("Value is required")
}
return nil
}
// Create an SSH key and associate it with an organization.
func (s *sshKeys) Create(ctx context.Context, organization string, options SSHKeyCreateOptions) (*SSHKey, error) {
if !validStringID(&organization) {
return nil, errors.New("Invalid value for organization")
}
if err := options.valid(); err != nil {
return nil, err
}
// Make sure we don't send a user provided ID.
options.ID = ""
u := fmt.Sprintf("organizations/%s/ssh-keys", url.QueryEscape(organization))
req, err := s.client.newRequest("POST", u, &options)
if err != nil {
return nil, err
}
k := &SSHKey{}
err = s.client.do(ctx, req, k)
if err != nil {
return nil, err
}
return k, nil
}
// Read an SSH key by its ID.
func (s *sshKeys) Read(ctx context.Context, sshKeyID string) (*SSHKey, error) {
if !validStringID(&sshKeyID) {
return nil, errors.New("Invalid value for SSH key ID")
}
u := fmt.Sprintf("ssh-keys/%s", url.QueryEscape(sshKeyID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
k := &SSHKey{}
err = s.client.do(ctx, req, k)
if err != nil {
return nil, err
}
return k, nil
}
// SSHKeyUpdateOptions represents the options for updating an SSH key.
type SSHKeyUpdateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,ssh-keys"`
// A new name to identify the SSH key.
Name *string `jsonapi:"attr,name,omitempty"`
// Updated content of the SSH private key.
Value *string `jsonapi:"attr,value,omitempty"`
}
// Update an SSH key by its ID.
func (s *sshKeys) Update(ctx context.Context, sshKeyID string, options SSHKeyUpdateOptions) (*SSHKey, error) {
if !validStringID(&sshKeyID) {
return nil, errors.New("Invalid value for SSH key ID")
}
// Make sure we don't send a user provided ID.
options.ID = ""
u := fmt.Sprintf("ssh-keys/%s", url.QueryEscape(sshKeyID))
req, err := s.client.newRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
k := &SSHKey{}
err = s.client.do(ctx, req, k)
if err != nil {
return nil, err
}
return k, nil
}
// Delete an SSH key by its ID.
func (s *sshKeys) Delete(ctx context.Context, sshKeyID string) error {
if !validStringID(&sshKeyID) {
return errors.New("Invalid value for SSH key ID")
}
u := fmt.Sprintf("ssh-keys/%s", url.QueryEscape(sshKeyID))
req, err := s.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}

207
vendor/github.com/hashicorp/go-tfe/state_version.go generated vendored Normal file
View File

@ -0,0 +1,207 @@
package tfe
import (
"bytes"
"context"
"errors"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ StateVersions = (*stateVersions)(nil)
// StateVersions describes all the state version related methods that
// the Terraform Enterprise API supports.
//
// TFE API docs:
// https://www.terraform.io/docs/enterprise/api/state-versions.html
type StateVersions interface {
// List all the state versions for a given workspace.
List(ctx context.Context, options StateVersionListOptions) ([]*StateVersion, error)
// Create a new state version for the given workspace.
Create(ctx context.Context, workspaceID string, options StateVersionCreateOptions) (*StateVersion, error)
// Read a state version by its ID.
Read(ctx context.Context, svID string) (*StateVersion, error)
// Current reads the latest available state from the given workspace.
Current(ctx context.Context, workspaceID string) (*StateVersion, error)
// Download retrieves the actual stored state of a state version
Download(ctx context.Context, url string) ([]byte, error)
}
// stateVersions implements StateVersions.
type stateVersions struct {
client *Client
}
// StateVersion represents a Terraform Enterprise state version.
type StateVersion struct {
ID string `jsonapi:"primary,state-versions"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
DownloadURL string `jsonapi:"attr,hosted-state-download-url"`
Serial int64 `jsonapi:"attr,serial"`
VCSCommitSHA string `jsonapi:"attr,vcs-commit-sha"`
VCSCommitURL string `jsonapi:"attr,vcs-commit-url"`
// Relations
Run *Run `jsonapi:"relation,run"`
}
// StateVersionListOptions represents the options for listing state versions.
type StateVersionListOptions struct {
ListOptions
Organization *string `url:"filter[organization][name]"`
Workspace *string `url:"filter[workspace][name]"`
}
func (o StateVersionListOptions) valid() error {
if !validString(o.Organization) {
return errors.New("Organization is required")
}
if !validString(o.Workspace) {
return errors.New("Workspace is required")
}
return nil
}
// List all the state versions for a given workspace.
func (s *stateVersions) List(ctx context.Context, options StateVersionListOptions) ([]*StateVersion, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := s.client.newRequest("GET", "state-versions", &options)
if err != nil {
return nil, err
}
var svs []*StateVersion
err = s.client.do(ctx, req, &svs)
if err != nil {
return nil, err
}
return svs, nil
}
// StateVersionCreateOptions represents the options for creating a state version.
type StateVersionCreateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,state-versions"`
// The lineage of the state.
Lineage *string `jsonapi:"attr,lineage,omitempty"`
// The MD5 hash of the state version.
MD5 *string `jsonapi:"attr,md5"`
// The serial of the state.
Serial *int64 `jsonapi:"attr,serial"`
// The base64 encoded state.
State *string `jsonapi:"attr,state"`
}
func (o StateVersionCreateOptions) valid() error {
if !validString(o.MD5) {
return errors.New("MD5 is required")
}
if o.Serial == nil {
return errors.New("Serial is required")
}
if !validString(o.State) {
return errors.New("State is required")
}
return nil
}
// Create a new state version for the given workspace.
func (s *stateVersions) Create(ctx context.Context, workspaceID string, options StateVersionCreateOptions) (*StateVersion, error) {
if !validStringID(&workspaceID) {
return nil, errors.New("Invalid value for workspace ID")
}
if err := options.valid(); err != nil {
return nil, err
}
// Make sure we don't send a user provided ID.
options.ID = ""
u := fmt.Sprintf("workspaces/%s/state-versions", url.QueryEscape(workspaceID))
req, err := s.client.newRequest("POST", u, &options)
if err != nil {
return nil, err
}
sv := &StateVersion{}
err = s.client.do(ctx, req, sv)
if err != nil {
return nil, err
}
return sv, nil
}
// Read a state version by its ID.
func (s *stateVersions) Read(ctx context.Context, svID string) (*StateVersion, error) {
if !validStringID(&svID) {
return nil, errors.New("Invalid value for state version ID")
}
u := fmt.Sprintf("state-versions/%s", url.QueryEscape(svID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
sv := &StateVersion{}
err = s.client.do(ctx, req, sv)
if err != nil {
return nil, err
}
return sv, nil
}
// Current reads the latest available state from the given workspace.
func (s *stateVersions) Current(ctx context.Context, workspaceID string) (*StateVersion, error) {
if !validStringID(&workspaceID) {
return nil, errors.New("Invalid value for workspace ID")
}
u := fmt.Sprintf("workspaces/%s/current-state-version", url.QueryEscape(workspaceID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
sv := &StateVersion{}
err = s.client.do(ctx, req, sv)
if err != nil {
return nil, err
}
return sv, nil
}
// Download retrieves the actual stored state of a state version
func (s *stateVersions) Download(ctx context.Context, url string) ([]byte, error) {
req, err := s.client.newRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
var buf bytes.Buffer
err = s.client.do(ctx, req, &buf)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}

159
vendor/github.com/hashicorp/go-tfe/team.go generated vendored Normal file
View File

@ -0,0 +1,159 @@
package tfe
import (
"context"
"errors"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ Teams = (*teams)(nil)
// Teams describes all the team related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://www.terraform.io/docs/enterprise/api/teams.html
type Teams interface {
// List all the teams of the given organization.
List(ctx context.Context, organization string, options TeamListOptions) ([]*Team, error)
// Create a new team with the given options.
Create(ctx context.Context, organization string, options TeamCreateOptions) (*Team, error)
// Read a team by its ID.
Read(ctx context.Context, teamID string) (*Team, error)
// Delete a team by its ID.
Delete(ctx context.Context, teamID string) error
}
// teams implements Teams.
type teams struct {
client *Client
}
// Team represents a Terraform Enterprise team.
type Team struct {
ID string `jsonapi:"primary,teams"`
Name string `jsonapi:"attr,name"`
Permissions *TeamPermissions `jsonapi:"attr,permissions"`
UserCount int `jsonapi:"attr,users-count"`
// Relations
//User []*User `jsonapi:"relation,users"`
}
// TeamPermissions represents the team permissions.
type TeamPermissions struct {
CanDestroy bool `json:"can-destroy"`
CanUpdateMembership bool `json:"can-update-membership"`
}
// TeamListOptions represents the options for listing teams.
type TeamListOptions struct {
ListOptions
}
// List all the teams of the given organization.
func (s *teams) List(ctx context.Context, organization string, options TeamListOptions) ([]*Team, error) {
if !validStringID(&organization) {
return nil, errors.New("Invalid value for organization")
}
u := fmt.Sprintf("organizations/%s/teams", url.QueryEscape(organization))
req, err := s.client.newRequest("GET", u, &options)
if err != nil {
return nil, err
}
var ts []*Team
err = s.client.do(ctx, req, &ts)
if err != nil {
return nil, err
}
return ts, nil
}
// TeamCreateOptions represents the options for creating a team.
type TeamCreateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,teams"`
// Name of the team.
Name *string `jsonapi:"attr,name"`
}
func (o TeamCreateOptions) valid() error {
if !validString(o.Name) {
return errors.New("Name is required")
}
if !validStringID(o.Name) {
return errors.New("Invalid value for name")
}
return nil
}
// Create a new team with the given options.
func (s *teams) Create(ctx context.Context, organization string, options TeamCreateOptions) (*Team, error) {
if !validStringID(&organization) {
return nil, errors.New("Invalid value for organization")
}
if err := options.valid(); err != nil {
return nil, err
}
// Make sure we don't send a user provided ID.
options.ID = ""
u := fmt.Sprintf("organizations/%s/teams", url.QueryEscape(organization))
req, err := s.client.newRequest("POST", u, &options)
if err != nil {
return nil, err
}
t := &Team{}
err = s.client.do(ctx, req, t)
if err != nil {
return nil, err
}
return t, nil
}
// Read a single team by its ID.
func (s *teams) Read(ctx context.Context, teamID string) (*Team, error) {
if !validStringID(&teamID) {
return nil, errors.New("Invalid value for team ID")
}
u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
t := &Team{}
err = s.client.do(ctx, req, t)
if err != nil {
return nil, err
}
return t, nil
}
// Delete a team by its ID.
func (s *teams) Delete(ctx context.Context, teamID string) error {
if !validStringID(&teamID) {
return errors.New("Invalid value for team ID")
}
u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID))
req, err := s.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}

178
vendor/github.com/hashicorp/go-tfe/team_access.go generated vendored Normal file
View File

@ -0,0 +1,178 @@
package tfe
import (
"context"
"errors"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ TeamAccesses = (*teamAccesses)(nil)
// TeamAccesses describes all the team access related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://www.terraform.io/docs/enterprise/api/team-access.html
type TeamAccesses interface {
// List all the team accesses for a given workspace.
List(ctx context.Context, options TeamAccessListOptions) ([]*TeamAccess, error)
// Add team access for a workspace.
Add(ctx context.Context, options TeamAccessAddOptions) (*TeamAccess, error)
// Read a team access by its ID.
Read(ctx context.Context, teamAccessID string) (*TeamAccess, error)
// Remove team access from a workspace.
Remove(ctx context.Context, teamAccessID string) error
}
// teamAccesses implements TeamAccesses.
type teamAccesses struct {
client *Client
}
// TeamAccessType represents a team access type.
type TeamAccessType string
// List all available team access types.
const (
TeamAccessAdmin TeamAccessType = "admin"
TeamAccessRead TeamAccessType = "read"
TeamAccessWrite TeamAccessType = "write"
)
// TeamAccess represents the workspace access for a team.
type TeamAccess struct {
ID string `jsonapi:"primary,team-workspaces"`
Access TeamAccessType `jsonapi:"attr,access"`
// Relations
Team *Team `jsonapi:"relation,team"`
Workspace *Workspace `jsonapi:"relation,workspace"`
}
// TeamAccessListOptions represents the options for listing team accesses.
type TeamAccessListOptions struct {
ListOptions
WorkspaceID *string `url:"filter[workspace][id],omitempty"`
}
func (o TeamAccessListOptions) valid() error {
if !validString(o.WorkspaceID) {
return errors.New("Workspace ID is required")
}
if !validStringID(o.WorkspaceID) {
return errors.New("Invalid value for workspace ID")
}
return nil
}
// List all the team accesses for a given workspace.
func (s *teamAccesses) List(ctx context.Context, options TeamAccessListOptions) ([]*TeamAccess, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := s.client.newRequest("GET", "team-workspaces", &options)
if err != nil {
return nil, err
}
var tas []*TeamAccess
err = s.client.do(ctx, req, &tas)
if err != nil {
return nil, err
}
return tas, nil
}
// TeamAccessAddOptions represents the options for adding team access.
type TeamAccessAddOptions struct {
// For internal use only!
ID string `jsonapi:"primary,team-workspaces"`
// The type of access to grant.
Access *TeamAccessType `jsonapi:"attr,access"`
// The team to add to the workspace
Team *Team `jsonapi:"relation,team"`
// The workspace to which the team is to be added.
Workspace *Workspace `jsonapi:"relation,workspace"`
}
func (o TeamAccessAddOptions) valid() error {
if o.Access == nil {
return errors.New("Access is required")
}
if o.Team == nil {
return errors.New("Team is required")
}
if o.Workspace == nil {
return errors.New("Workspace is required")
}
return nil
}
// Add team access for a workspace.
func (s *teamAccesses) Add(ctx context.Context, options TeamAccessAddOptions) (*TeamAccess, error) {
if err := options.valid(); err != nil {
return nil, err
}
// Make sure we don't send a user provided ID.
options.ID = ""
req, err := s.client.newRequest("POST", "team-workspaces", &options)
if err != nil {
return nil, err
}
ta := &TeamAccess{}
err = s.client.do(ctx, req, ta)
if err != nil {
return nil, err
}
return ta, nil
}
// Read a team access by its ID.
func (s *teamAccesses) Read(ctx context.Context, teamAccessID string) (*TeamAccess, error) {
if !validStringID(&teamAccessID) {
return nil, errors.New("Invalid value for team access ID")
}
u := fmt.Sprintf("team-workspaces/%s", url.QueryEscape(teamAccessID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
ta := &TeamAccess{}
err = s.client.do(ctx, req, ta)
if err != nil {
return nil, err
}
return ta, nil
}
// Remove team access from a workspace.
func (s *teamAccesses) Remove(ctx context.Context, teamAccessID string) error {
if !validStringID(&teamAccessID) {
return errors.New("Invalid value for team access ID")
}
u := fmt.Sprintf("team-workspaces/%s", url.QueryEscape(teamAccessID))
req, err := s.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}

109
vendor/github.com/hashicorp/go-tfe/team_member.go generated vendored Normal file
View File

@ -0,0 +1,109 @@
package tfe
import (
"context"
"errors"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ TeamMembers = (*teamMembers)(nil)
// TeamMembers describes all the team member related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://www.terraform.io/docs/enterprise/api/team-members.html
type TeamMembers interface {
// Add multiple users to a team.
Add(ctx context.Context, teamID string, options TeamMemberAddOptions) error
// Remove multiple users from a team.
Remove(ctx context.Context, teamID string, options TeamMemberRemoveOptions) error
}
// teamMembers implements TeamMembers.
type teamMembers struct {
client *Client
}
type teamMember struct {
Username string `jsonapi:"primary,users"`
}
// TeamMemberAddOptions represents the options for adding team members.
type TeamMemberAddOptions struct {
Usernames []string
}
func (o *TeamMemberAddOptions) valid() error {
if o.Usernames == nil {
return errors.New("Usernames is required")
}
if len(o.Usernames) == 0 {
return errors.New("Invalid value for usernames")
}
return nil
}
// Add multiple users to a team.
func (s *teamMembers) Add(ctx context.Context, teamID string, options TeamMemberAddOptions) error {
if !validStringID(&teamID) {
return errors.New("Invalid value for team ID")
}
if err := options.valid(); err != nil {
return err
}
var tms []*teamMember
for _, name := range options.Usernames {
tms = append(tms, &teamMember{Username: name})
}
u := fmt.Sprintf("teams/%s/relationships/users", url.QueryEscape(teamID))
req, err := s.client.newRequest("POST", u, tms)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}
// TeamMemberRemoveOptions represents the options for deleting team members.
type TeamMemberRemoveOptions struct {
Usernames []string
}
func (o *TeamMemberRemoveOptions) valid() error {
if o.Usernames == nil {
return errors.New("Usernames is required")
}
if len(o.Usernames) == 0 {
return errors.New("Invalid value for usernames")
}
return nil
}
// Remove multiple users from a team.
func (s *teamMembers) Remove(ctx context.Context, teamID string, options TeamMemberRemoveOptions) error {
if !validStringID(&teamID) {
return errors.New("Invalid value for team ID")
}
if err := options.valid(); err != nil {
return err
}
var tms []*teamMember
for _, name := range options.Usernames {
tms = append(tms, &teamMember{Username: name})
}
u := fmt.Sprintf("teams/%s/relationships/users", url.QueryEscape(teamID))
req, err := s.client.newRequest("DELETE", u, tms)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}

75
vendor/github.com/hashicorp/go-tfe/team_token.go generated vendored Normal file
View File

@ -0,0 +1,75 @@
package tfe
import (
"context"
"errors"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ TeamTokens = (*teamTokens)(nil)
// TeamTokens describes all the team token related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://www.terraform.io/docs/enterprise/api/team-tokens.html
type TeamTokens interface {
// Generate a new team token, replacing any existing token.
Generate(ctx context.Context, teamID string) (*TeamToken, error)
// Delete a team token by its ID.
Delete(ctx context.Context, teamID string) error
}
// teamTokens implements TeamTokens.
type teamTokens struct {
client *Client
}
// TeamToken represents a Terraform Enterprise team token.
type TeamToken struct {
ID string `jsonapi:"primary,authentication-tokens"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Description string `jsonapi:"attr,description"`
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
Token string `jsonapi:"attr,token"`
}
// Generate a new team token, replacing any existing token.
func (s *teamTokens) Generate(ctx context.Context, teamID string) (*TeamToken, error) {
if !validStringID(&teamID) {
return nil, errors.New("Invalid value for team ID")
}
u := fmt.Sprintf("teams/%s/authentication-token", url.QueryEscape(teamID))
req, err := s.client.newRequest("POST", u, nil)
if err != nil {
return nil, err
}
tt := &TeamToken{}
err = s.client.do(ctx, req, tt)
if err != nil {
return nil, err
}
return tt, err
}
// Delete a team token by its ID.
func (s *teamTokens) Delete(ctx context.Context, teamID string) error {
if !validStringID(&teamID) {
return errors.New("Invalid value for team ID")
}
u := fmt.Sprintf("teams/%s/authentication-token", url.QueryEscape(teamID))
req, err := s.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}

349
vendor/github.com/hashicorp/go-tfe/tfe.go generated vendored Normal file
View File

@ -0,0 +1,349 @@
package tfe
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"reflect"
"strings"
"github.com/google/go-querystring/query"
"github.com/hashicorp/go-cleanhttp"
"github.com/svanharmelen/jsonapi"
)
const (
// DefaultAddress of Terraform Enterprise.
DefaultAddress = "https://app.terraform.io"
// DefaultBasePath on which the API is served.
DefaultBasePath = "/api/v2/"
)
const (
userAgent = "go-tfe"
)
var (
// ErrUnauthorized is returned when a receiving a 401.
ErrUnauthorized = errors.New("unauthorized")
// ErrResourceNotFound is returned when a receiving a 404.
ErrResourceNotFound = errors.New("resource not found")
)
// Config provides configuration details to the API client.
type Config struct {
// The address of the Terraform Enterprise API.
Address string
// The base path on which the API is served.
BasePath string
// API token used to access the Terraform Enterprise API.
Token string
// A custom HTTP client to use.
HTTPClient *http.Client
}
// DefaultConfig returns a default config structure.
func DefaultConfig() *Config {
config := &Config{
Address: os.Getenv("TFE_ADDRESS"),
BasePath: DefaultBasePath,
Token: os.Getenv("TFE_TOKEN"),
HTTPClient: cleanhttp.DefaultClient(),
}
// Set the default address if none is given.
if config.Address == "" {
config.Address = DefaultAddress
}
return config
}
// Client is the Terraform Enterprise API client. It provides the basic
// connectivity and configuration for accessing the TFE API.
type Client struct {
baseURL *url.URL
token string
http *http.Client
userAgent string
ConfigurationVersions ConfigurationVersions
OAuthClients OAuthClients
OAuthTokens OAuthTokens
Organizations Organizations
OrganizationTokens OrganizationTokens
Plans Plans
Policies Policies
PolicyChecks PolicyChecks
Runs Runs
SSHKeys SSHKeys
StateVersions StateVersions
Teams Teams
TeamAccess TeamAccesses
TeamMembers TeamMembers
TeamTokens TeamTokens
Users Users
Variables Variables
Workspaces Workspaces
}
// NewClient creates a new Terraform Enterprise API client.
func NewClient(cfg *Config) (*Client, error) {
config := DefaultConfig()
// Layer in the provided config for any non-blank values.
if cfg != nil {
if cfg.Address != "" {
config.Address = cfg.Address
}
if cfg.BasePath != "" {
config.BasePath = cfg.BasePath
}
if cfg.Token != "" {
config.Token = cfg.Token
}
if cfg.HTTPClient != nil {
config.HTTPClient = cfg.HTTPClient
}
}
// Parse the address to make sure its a valid URL.
baseURL, err := url.Parse(config.Address)
if err != nil {
return nil, fmt.Errorf("Invalid address: %v", err)
}
baseURL.Path = config.BasePath
if !strings.HasSuffix(baseURL.Path, "/") {
baseURL.Path += "/"
}
// This value must be provided by the user.
if config.Token == "" {
return nil, fmt.Errorf("Missing API token")
}
// Create the client.
client := &Client{
baseURL: baseURL,
token: config.Token,
http: config.HTTPClient,
userAgent: userAgent,
}
// Create the services.
client.ConfigurationVersions = &configurationVersions{client: client}
client.OAuthClients = &oAuthClients{client: client}
client.OAuthTokens = &oAuthTokens{client: client}
client.Organizations = &organizations{client: client}
client.OrganizationTokens = &organizationTokens{client: client}
client.Plans = &plans{client: client}
client.Policies = &policies{client: client}
client.PolicyChecks = &policyChecks{client: client}
client.Runs = &runs{client: client}
client.SSHKeys = &sshKeys{client: client}
client.StateVersions = &stateVersions{client: client}
client.Teams = &teams{client: client}
client.TeamAccess = &teamAccesses{client: client}
client.TeamMembers = &teamMembers{client: client}
client.TeamTokens = &teamTokens{client: client}
client.Users = &users{client: client}
client.Variables = &variables{client: client}
client.Workspaces = &workspaces{client: client}
return client, nil
}
// ListOptions is used to specify pagination options when making API requests.
// Pagination allows breaking up large result sets into chunks, or "pages".
type ListOptions struct {
// The page number to request. The results vary based on the PageSize.
PageNumber int `url:"page[number],omitempty"`
// The number of elements returned in a single page.
PageSize int `url:"page[size],omitempty"`
}
// newRequest creates an API request. A relative URL path can be provided in
// path, in which case it is resolved relative to the apiVersionPath of the
// Client. Relative URL paths should always be specified without a preceding
// slash.
// If v is supplied, the value will be JSONAPI encoded and included as the
// request body. If the method is GET, the value will be parsed and added as
// query parameters.
func (c *Client) newRequest(method, path string, v interface{}) (*http.Request, error) {
u, err := c.baseURL.Parse(path)
if err != nil {
return nil, err
}
req := &http.Request{
Method: method,
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
Host: u.Host,
}
switch method {
case "GET":
req.Header.Set("Accept", "application/vnd.api+json")
if v != nil {
q, err := query.Values(v)
if err != nil {
return nil, err
}
u.RawQuery = q.Encode()
}
case "PATCH", "POST":
req.Header.Set("Accept", "application/vnd.api+json")
req.Header.Set("Content-Type", "application/vnd.api+json")
if v != nil {
var body bytes.Buffer
if err := jsonapi.MarshalPayloadWithoutIncluded(&body, v); err != nil {
return nil, err
}
req.Body = ioutil.NopCloser(&body)
req.ContentLength = int64(body.Len())
}
case "PUT":
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/octet-stream")
if v != nil {
switch v := v.(type) {
case *bytes.Buffer:
req.Body = ioutil.NopCloser(v)
req.ContentLength = int64(v.Len())
case []byte:
req.Body = ioutil.NopCloser(bytes.NewReader(v))
req.ContentLength = int64(len(v))
default:
return nil, fmt.Errorf("Unexpected type: %T", v)
}
}
}
// Set required headers.
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("User-Agent", c.userAgent)
return req, nil
}
// do sends an API request and returns the API response. The API response is
// JSONAPI decoded and stored in the value pointed to by v, or returned as an
// error if an API error has occurred.
// If v implements the io.Writer interface, the raw response body will be
// written to v, without attempting to first decode it.
// The provided ctx must be non-nil. If it is canceled or times out, ctx.Err()
// will be returned.
func (c *Client) do(ctx context.Context, req *http.Request, v interface{}) error {
// Add the context to the request.
req = req.WithContext(ctx)
// Execute the request and check the response.
resp, err := c.http.Do(req)
if err != nil {
// If we got an error, and the context has been canceled,
// the context's error is probably more useful.
select {
case <-ctx.Done():
return ctx.Err()
default:
return err
}
}
defer resp.Body.Close()
// Basic response checking.
if err := checkResponseCode(resp); err != nil {
return err
}
// Return here if decoding the response isn't needed.
if v == nil {
return nil
}
// If v implements io.Writer, write the raw response body.
if w, ok := v.(io.Writer); ok {
_, err = io.Copy(w, resp.Body)
return err
}
// Get the value of v so we can test if it's a slice.
dst := reflect.Indirect(reflect.ValueOf(v))
// Unmarshal a single value if v isn't a slice.
if dst.Type().Kind() != reflect.Slice {
return jsonapi.UnmarshalPayload(resp.Body, v)
}
// Unmarshal as a list of values if v is a slice.
raw, err := jsonapi.UnmarshalManyPayload(resp.Body, dst.Type().Elem())
if err != nil {
return err
}
// Make a new slice to hold the results.
sliceType := reflect.SliceOf(dst.Type().Elem())
result := reflect.MakeSlice(sliceType, 0, len(raw))
// Add all of the results to the new slice.
for _, v := range raw {
result = reflect.Append(result, reflect.ValueOf(v))
}
// Pointer-swap the result.
dst.Set(result)
return nil
}
// checkResponseCode can be used to check the status code of an HTTP request.
func checkResponseCode(r *http.Response) error {
if r.StatusCode >= 200 && r.StatusCode <= 299 {
return nil
}
switch r.StatusCode {
case 401:
return ErrUnauthorized
case 404:
return ErrResourceNotFound
}
// Decode the error payload.
errPayload := &jsonapi.ErrorsPayload{}
err := json.NewDecoder(r.Body).Decode(errPayload)
if err != nil || len(errPayload.Errors) == 0 {
return fmt.Errorf(r.Status)
}
// Parse and format the errors.
var errs []string
for _, e := range errPayload.Errors {
if e.Detail == "" {
errs = append(errs, e.Title)
} else {
errs = append(errs, fmt.Sprintf("%s %s", e.Title, e.Detail))
}
}
return fmt.Errorf(strings.Join(errs, "\n"))
}

41
vendor/github.com/hashicorp/go-tfe/type_helpers.go generated vendored Normal file
View File

@ -0,0 +1,41 @@
package tfe
// Access returns a pointer to the given team access type.
func Access(v TeamAccessType) *TeamAccessType {
return &v
}
// AuthPolicy returns a pointer to the given authentication poliy.
func AuthPolicy(v AuthPolicyType) *AuthPolicyType {
return &v
}
// Bool returns a pointer to the given bool
func Bool(v bool) *bool {
return &v
}
// Category returns a pointer to the given category type.
func Category(v CategoryType) *CategoryType {
return &v
}
// EnforcementMode returns a pointer to the given enforcement level.
func EnforcementMode(v EnforcementLevel) *EnforcementLevel {
return &v
}
// Int64 returns a pointer to the given int64.
func Int64(v int64) *int64 {
return &v
}
// ServiceProvider returns a pointer to the given service provider type.
func ServiceProvider(v ServiceProviderType) *ServiceProviderType {
return &v
}
// String returns a pointer to the given string.
func String(v string) *string {
return &v
}

93
vendor/github.com/hashicorp/go-tfe/user.go generated vendored Normal file
View File

@ -0,0 +1,93 @@
package tfe
import (
"context"
)
// Compile-time proof of interface implementation.
var _ Users = (*users)(nil)
// Users describes all the user related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://www.terraform.io/docs/enterprise/api/user.html
type Users interface {
// ReadCurrent reads the details of the currently authenticated user.
ReadCurrent(ctx context.Context) (*User, error)
// Update attributes of the currently authenticated user.
Update(ctx context.Context, options UserUpdateOptions) (*User, error)
}
// users implements Users.
type users struct {
client *Client
}
// User represents a Terraform Enterprise user.
type User struct {
ID string `jsonapi:"primary,users"`
AvatarURL string `jsonapi:"attr,avatar-url"`
Email string `jsonapi:"attr,email"`
IsServiceAccount bool `jsonapi:"attr,is-service-account"`
TwoFactor *TwoFactor `jsonapi:"attr,two-factor"`
UnconfirmedEmail string `jsonapi:"attr,unconfirmed-email"`
Username string `jsonapi:"attr,username"`
V2Only bool `jsonapi:"attr,v2-only"`
// Relations
// AuthenticationTokens *AuthenticationTokens `jsonapi:"relation,authentication-tokens"`
}
// TwoFactor represents the organization permissions.
type TwoFactor struct {
Enabled bool `json:"enabled"`
Verified bool `json:"verified"`
}
// ReadCurrent reads the details of the currently authenticated user.
func (s *users) ReadCurrent(ctx context.Context) (*User, error) {
req, err := s.client.newRequest("GET", "account/details", nil)
if err != nil {
return nil, err
}
u := &User{}
err = s.client.do(ctx, req, u)
if err != nil {
return nil, err
}
return u, nil
}
// UserUpdateOptions represents the options for updating a user.
type UserUpdateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,users"`
// New username.
Username *string `jsonapi:"attr,username,omitempty"`
// New email address (must be consumed afterwards to take effect).
Email *string `jsonapi:"attr,email,omitempty"`
}
// Update attributes of the currently authenticated user.
func (s *users) Update(ctx context.Context, options UserUpdateOptions) (*User, error) {
// Make sure we don't send a user provided ID.
options.ID = ""
req, err := s.client.newRequest("PATCH", "account/update", &options)
if err != nil {
return nil, err
}
u := &User{}
err = s.client.do(ctx, req, u)
if err != nil {
return nil, err
}
return u, nil
}

19
vendor/github.com/hashicorp/go-tfe/validations.go generated vendored Normal file
View File

@ -0,0 +1,19 @@
package tfe
import (
"regexp"
)
// A regular expression used to validate common string ID patterns.
var reStringID = regexp.MustCompile(`^[a-zA-Z0-9\-\._]+$`)
// validString checks if the given input is present and non-empty.
func validString(v *string) bool {
return v != nil && *v != ""
}
// validStringID checks if the given string pointer is non-nil and
// contains a typical string identifier.
func validStringID(v *string) bool {
return v != nil && reStringID.MatchString(*v)
}

216
vendor/github.com/hashicorp/go-tfe/variable.go generated vendored Normal file
View File

@ -0,0 +1,216 @@
package tfe
import (
"context"
"errors"
"fmt"
"net/url"
)
// Compile-time proof of interface implementation.
var _ Variables = (*variables)(nil)
// Variables describes all the variable related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://www.terraform.io/docs/enterprise/api/variables.html
type Variables interface {
// List all the variables associated with the given workspace.
List(ctx context.Context, options VariableListOptions) ([]*Variable, error)
// Create is used to create a new variable.
Create(ctx context.Context, options VariableCreateOptions) (*Variable, error)
// Update values of an existing variable.
Update(ctx context.Context, variableID string, options VariableUpdateOptions) (*Variable, error)
// Delete a variable by its ID.
Delete(ctx context.Context, variableID string) error
}
// variables implements Variables.
type variables struct {
client *Client
}
// CategoryType represents a category type.
type CategoryType string
//List all available categories.
const (
CategoryEnv CategoryType = "env"
CategoryTerraform CategoryType = "terraform"
)
// Variable represents a Terraform Enterprise variable.
type Variable struct {
ID string `jsonapi:"primary,vars"`
Key string `jsonapi:"attr,key"`
Value string `jsonapi:"attr,value"`
Category CategoryType `jsonapi:"attr,category"`
HCL bool `jsonapi:"attr,hcl"`
Sensitive bool `jsonapi:"attr,sensitive"`
// Relations
Workspace *Workspace `jsonapi:"relation,workspace"`
}
// VariableListOptions represents the options for listing variables.
type VariableListOptions struct {
ListOptions
Organization *string `url:"filter[organization][name]"`
Workspace *string `url:"filter[workspace][name]"`
}
func (o VariableListOptions) valid() error {
if !validString(o.Organization) {
return errors.New("Organization is required")
}
if !validString(o.Workspace) {
return errors.New("Workspace is required")
}
return nil
}
// List all the variables associated with the given workspace.
func (s *variables) List(ctx context.Context, options VariableListOptions) ([]*Variable, error) {
if err := options.valid(); err != nil {
return nil, err
}
req, err := s.client.newRequest("GET", "vars", &options)
if err != nil {
return nil, err
}
var vs []*Variable
err = s.client.do(ctx, req, &vs)
if err != nil {
return nil, err
}
return vs, nil
}
// VariableCreateOptions represents the options for creating a new variable.
type VariableCreateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,vars"`
// The name of the variable.
Key *string `jsonapi:"attr,key"`
// The value of the variable.
Value *string `jsonapi:"attr,value"`
// Whether this is a Terraform or environment variable.
Category *CategoryType `jsonapi:"attr,category"`
// Whether to evaluate the value of the variable as a string of HCL code.
HCL *bool `jsonapi:"attr,hcl,omitempty"`
// Whether the value is sensitive.
Sensitive *bool `jsonapi:"attr,sensitive,omitempty"`
// The workspace that owns the variable.
Workspace *Workspace `jsonapi:"relation,workspace"`
}
func (o VariableCreateOptions) valid() error {
if !validString(o.Key) {
return errors.New("Key is required")
}
if !validString(o.Value) {
return errors.New("Value is required")
}
if o.Category == nil {
return errors.New("Category is required")
}
if o.Workspace == nil {
return errors.New("Workspace is required")
}
return nil
}
// Create is used to create a new variable.
func (s *variables) Create(ctx context.Context, options VariableCreateOptions) (*Variable, error) {
if err := options.valid(); err != nil {
return nil, err
}
// Make sure we don't send a user provided ID.
options.ID = ""
req, err := s.client.newRequest("POST", "vars", &options)
if err != nil {
return nil, err
}
v := &Variable{}
err = s.client.do(ctx, req, v)
if err != nil {
return nil, err
}
return v, nil
}
// VariableUpdateOptions represents the options for updating a variable.
type VariableUpdateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,vars"`
// The name of the variable.
Key *string `jsonapi:"attr,key,omitempty"`
// The value of the variable.
Value *string `jsonapi:"attr,value,omitempty"`
// Whether this is a Terraform or environment variable.
Category *CategoryType `jsonapi:"attr,category,omitempty"`
// Whether to evaluate the value of the variable as a string of HCL code.
HCL *bool `jsonapi:"attr,hcl,omitempty"`
// Whether the value is sensitive.
Sensitive *bool `jsonapi:"attr,sensitive,omitempty"`
}
// Update values of an existing variable.
func (s *variables) Update(ctx context.Context, variableID string, options VariableUpdateOptions) (*Variable, error) {
if !validStringID(&variableID) {
return nil, errors.New("Invalid value for variable ID")
}
// Make sure we don't send a user provided ID.
options.ID = variableID
u := fmt.Sprintf("vars/%s", url.QueryEscape(variableID))
req, err := s.client.newRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
v := &Variable{}
err = s.client.do(ctx, req, v)
if err != nil {
return nil, err
}
return v, nil
}
// Delete a variable by its ID.
func (s *variables) Delete(ctx context.Context, variableID string) error {
if !validStringID(&variableID) {
return errors.New("Invalid value for variable ID")
}
u := fmt.Sprintf("vars/%s", url.QueryEscape(variableID))
req, err := s.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}

437
vendor/github.com/hashicorp/go-tfe/workspace.go generated vendored Normal file
View File

@ -0,0 +1,437 @@
package tfe
import (
"context"
"errors"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ Workspaces = (*workspaces)(nil)
// Workspaces describes all the workspace related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://www.terraform.io/docs/enterprise/api/workspaces.html
type Workspaces interface {
// List all the workspaces within an organization.
List(ctx context.Context, organization string, options WorkspaceListOptions) ([]*Workspace, error)
// Create is used to create a new workspace.
Create(ctx context.Context, organization string, options WorkspaceCreateOptions) (*Workspace, error)
// Read a workspace by its name.
Read(ctx context.Context, organization string, workspace string) (*Workspace, error)
// Update settings of an existing workspace.
Update(ctx context.Context, organization string, workspace string, options WorkspaceUpdateOptions) (*Workspace, error)
// Delete a workspace by its name.
Delete(ctx context.Context, organization string, workspace string) error
// Lock a workspace by its ID.
Lock(ctx context.Context, workspaceID string, options WorkspaceLockOptions) (*Workspace, error)
// Unlock a workspace by its ID.
Unlock(ctx context.Context, workspaceID string) (*Workspace, error)
// AssignSSHKey to a workspace.
AssignSSHKey(ctx context.Context, workspaceID string, options WorkspaceAssignSSHKeyOptions) (*Workspace, error)
// UnassignSSHKey from a workspace.
UnassignSSHKey(ctx context.Context, workspaceID string) (*Workspace, error)
}
// workspaces implements Workspaces.
type workspaces struct {
client *Client
}
// Workspace represents a Terraform Enterprise workspace.
type Workspace struct {
ID string `jsonapi:"primary,workspaces"`
Actions *WorkspaceActions `jsonapi:"attr,actions"`
AutoApply bool `jsonapi:"attr,auto-apply"`
CanQueueDestroyPlan bool `jsonapi:"attr,can-queue-destroy-plan"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Environment string `jsonapi:"attr,environment"`
Locked bool `jsonapi:"attr,locked"`
MigrationEnvironment string `jsonapi:"attr,migration-environment"`
Name string `jsonapi:"attr,name"`
Permissions *WorkspacePermissions `jsonapi:"attr,permissions"`
TerraformVersion string `jsonapi:"attr,terraform-version"`
VCSRepo *VCSRepo `jsonapi:"attr,vcs-repo"`
WorkingDirectory string `jsonapi:"attr,working-directory"`
// Relations
Organization *Organization `jsonapi:"relation,organization"`
SSHKey *SSHKey `jsonapi:"relation,ssh-key"`
}
// VCSRepo contains the configuration of a VCS integration.
type VCSRepo struct {
Branch string `json:"branch"`
Identifier string `json:"identifier"`
IncludeSubmodules bool `json:"ingress-submodules"`
OAuthTokenID string `json:"oauth-token-id"`
}
// WorkspaceActions represents the workspace actions.
type WorkspaceActions struct {
IsDestroyable bool `json:"is-destroyable"`
}
// WorkspacePermissions represents the workspace permissions.
type WorkspacePermissions struct {
CanDestroy bool `json:"can-destroy"`
CanLock bool `json:"can-lock"`
CanQueueDestroy bool `json:"can-queue-destroy"`
CanQueueRun bool `json:"can-queue-run"`
CanReadSettings bool `json:"can-read-settings"`
CanUpdate bool `json:"can-update"`
CanUpdateVariable bool `json:"can-update-variable"`
}
// WorkspaceListOptions represents the options for listing workspaces.
type WorkspaceListOptions struct {
ListOptions
}
// List all the workspaces within an organization.
func (s *workspaces) List(ctx context.Context, organization string, options WorkspaceListOptions) ([]*Workspace, error) {
if !validStringID(&organization) {
return nil, errors.New("Invalid value for organization")
}
u := fmt.Sprintf("organizations/%s/workspaces", url.QueryEscape(organization))
req, err := s.client.newRequest("GET", u, &options)
if err != nil {
return nil, err
}
var ws []*Workspace
err = s.client.do(ctx, req, &ws)
if err != nil {
return nil, err
}
return ws, nil
}
// WorkspaceCreateOptions represents the options for creating a new workspace.
type WorkspaceCreateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,workspaces"`
// Whether to automatically apply changes when a Terraform plan is successful.
AutoApply *bool `jsonapi:"attr,auto-apply,omitempty"`
// The legacy TFE environment to use as the source of the migration, in the
// form organization/environment. Omit this unless you are migrating a legacy
// environment.
MigrationEnvironment *string `jsonapi:"attr,migration-environment,omitempty"`
// The name of the workspace, which can only include letters, numbers, -,
// and _. This will be used as an identifier and must be unique in the
// organization.
Name *string `jsonapi:"attr,name"`
// The version of Terraform to use for this workspace. Upon creating a
// workspace, the latest version is selected unless otherwise specified.
TerraformVersion *string `jsonapi:"attr,terraform-version,omitempty"`
// Settings for the workspace's VCS repository. If omitted, the workspace is
// created without a VCS repo. If included, you must specify at least the
// oauth-token-id and identifier keys below.
VCSRepo *VCSRepoOptions `jsonapi:"attr,vcs-repo,omitempty"`
// A relative path that Terraform will execute within. This defaults to the
// root of your repository and is typically set to a subdirectory matching the
// environment when multiple environments exist within the same repository.
WorkingDirectory *string `jsonapi:"attr,working-directory,omitempty"`
}
// VCSRepoOptions represents the configuration options of a VCS integration.
type VCSRepoOptions struct {
Branch *string `json:"branch,omitempty"`
Identifier *string `json:"identifier,omitempty"`
IncludeSubmodules *bool `json:"ingress-submodules,omitempty"`
OAuthTokenID *string `json:"oauth-token-id,omitempty"`
}
func (o WorkspaceCreateOptions) valid() error {
if !validString(o.Name) {
return errors.New("Name is required")
}
if !validStringID(o.Name) {
return errors.New("Invalid value for name")
}
return nil
}
// Create is used to create a new workspace.
func (s *workspaces) Create(ctx context.Context, organization string, options WorkspaceCreateOptions) (*Workspace, error) {
if !validStringID(&organization) {
return nil, errors.New("Invalid value for organization")
}
if err := options.valid(); err != nil {
return nil, err
}
// Make sure we don't send a user provided ID.
options.ID = ""
u := fmt.Sprintf("organizations/%s/workspaces", url.QueryEscape(organization))
req, err := s.client.newRequest("POST", u, &options)
if err != nil {
return nil, err
}
w := &Workspace{}
err = s.client.do(ctx, req, w)
if err != nil {
return nil, err
}
return w, nil
}
// Read a workspace by its name.
func (s *workspaces) Read(ctx context.Context, organization, workspace string) (*Workspace, error) {
if !validStringID(&organization) {
return nil, errors.New("Invalid value for organization")
}
if !validStringID(&workspace) {
return nil, errors.New("Invalid value for workspace")
}
u := fmt.Sprintf(
"organizations/%s/workspaces/%s",
url.QueryEscape(organization),
url.QueryEscape(workspace),
)
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
w := &Workspace{}
err = s.client.do(ctx, req, w)
if err != nil {
return nil, err
}
return w, nil
}
// WorkspaceUpdateOptions represents the options for updating a workspace.
type WorkspaceUpdateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,workspaces"`
// Whether to automatically apply changes when a Terraform plan is successful.
AutoApply *bool `jsonapi:"attr,auto-apply,omitempty"`
// A new name for the workspace, which can only include letters, numbers, -,
// and _. This will be used as an identifier and must be unique in the
// organization. Warning: Changing a workspace's name changes its URL in the
// API and UI.
Name *string `jsonapi:"attr,name,omitempty"`
// The version of Terraform to use for this workspace.
TerraformVersion *string `jsonapi:"attr,terraform-version,omitempty"`
// To delete a workspace's existing VCS repo, specify null instead of an
// object. To modify a workspace's existing VCS repo, include whichever of
// the keys below you wish to modify. To add a new VCS repo to a workspace
// that didn't previously have one, include at least the oauth-token-id and
// identifier keys. VCSRepo *VCSRepo `jsonapi:"relation,vcs-repo,om-tempty"`
VCSRepo *VCSRepoOptions `jsonapi:"attr,vcs-repo,omitempty"`
// A relative path that Terraform will execute within. This defaults to the
// root of your repository and is typically set to a subdirectory matching
// the environment when multiple environments exist within the same
// repository.
WorkingDirectory *string `jsonapi:"attr,working-directory,omitempty"`
}
// Update settings of an existing workspace.
func (s *workspaces) Update(ctx context.Context, organization, workspace string, options WorkspaceUpdateOptions) (*Workspace, error) {
if !validStringID(&organization) {
return nil, errors.New("Invalid value for organization")
}
if !validStringID(&workspace) {
return nil, errors.New("Invalid value for workspace")
}
// Make sure we don't send a user provided ID.
options.ID = ""
u := fmt.Sprintf(
"organizations/%s/workspaces/%s",
url.QueryEscape(organization),
url.QueryEscape(workspace),
)
req, err := s.client.newRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
w := &Workspace{}
err = s.client.do(ctx, req, w)
if err != nil {
return nil, err
}
return w, nil
}
// Delete a workspace by its name.
func (s *workspaces) Delete(ctx context.Context, organization, workspace string) error {
if !validStringID(&organization) {
return errors.New("Invalid value for organization")
}
if !validStringID(&workspace) {
return errors.New("Invalid value for workspace")
}
u := fmt.Sprintf(
"organizations/%s/workspaces/%s",
url.QueryEscape(organization),
url.QueryEscape(workspace),
)
req, err := s.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}
// WorkspaceLockOptions represents the options for locking a workspace.
type WorkspaceLockOptions struct {
// Specifies the reason for locking the workspace.
Reason *string `json:"reason,omitempty"`
}
// Lock a workspace by its ID.
func (s *workspaces) Lock(ctx context.Context, workspaceID string, options WorkspaceLockOptions) (*Workspace, error) {
if !validStringID(&workspaceID) {
return nil, errors.New("Invalid value for workspace ID")
}
u := fmt.Sprintf("workspaces/%s/actions/lock", url.QueryEscape(workspaceID))
req, err := s.client.newRequest("POST", u, &options)
if err != nil {
return nil, err
}
w := &Workspace{}
err = s.client.do(ctx, req, w)
if err != nil {
return nil, err
}
return w, nil
}
// Unlock a workspace by its ID.
func (s *workspaces) Unlock(ctx context.Context, workspaceID string) (*Workspace, error) {
if !validStringID(&workspaceID) {
return nil, errors.New("Invalid value for workspace ID")
}
u := fmt.Sprintf("workspaces/%s/actions/unlock", url.QueryEscape(workspaceID))
req, err := s.client.newRequest("POST", u, nil)
if err != nil {
return nil, err
}
w := &Workspace{}
err = s.client.do(ctx, req, w)
if err != nil {
return nil, err
}
return w, nil
}
// WorkspaceAssignSSHKeyOptions represents the options to assign an SSH key to
// a workspace.
type WorkspaceAssignSSHKeyOptions struct {
// For internal use only!
ID string `jsonapi:"primary,workspaces"`
// The SSH key ID to assign.
SSHKeyID *string `jsonapi:"attr,id"`
}
func (o WorkspaceAssignSSHKeyOptions) valid() error {
if !validString(o.SSHKeyID) {
return errors.New("SSH key ID is required")
}
if !validStringID(o.SSHKeyID) {
return errors.New("Invalid value for SSH key ID")
}
return nil
}
// AssignSSHKey to a workspace.
func (s *workspaces) AssignSSHKey(ctx context.Context, workspaceID string, options WorkspaceAssignSSHKeyOptions) (*Workspace, error) {
if !validStringID(&workspaceID) {
return nil, errors.New("Invalid value for workspace ID")
}
if err := options.valid(); err != nil {
return nil, err
}
// Make sure we don't send a user provided ID.
options.ID = ""
u := fmt.Sprintf("workspaces/%s/relationships/ssh-key", url.QueryEscape(workspaceID))
req, err := s.client.newRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
w := &Workspace{}
err = s.client.do(ctx, req, w)
if err != nil {
return nil, err
}
return w, nil
}
// workspaceUnassignSSHKeyOptions represents the options to unassign an SSH key
// to a workspace.
type workspaceUnassignSSHKeyOptions struct {
// For internal use only!
ID string `jsonapi:"primary,workspaces"`
// Must be nil to unset the currently assigned SSH key.
SSHKeyID *string `jsonapi:"attr,id"`
}
// UnassignSSHKey from a workspace.
func (s *workspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*Workspace, error) {
if !validStringID(&workspaceID) {
return nil, errors.New("Invalid value for workspace ID")
}
u := fmt.Sprintf("workspaces/%s/relationships/ssh-key", url.QueryEscape(workspaceID))
req, err := s.client.newRequest("PATCH", u, &workspaceUnassignSSHKeyOptions{})
if err != nil {
return nil, err
}
w := &Workspace{}
err = s.client.do(ctx, req, w)
if err != nil {
return nil, err
}
return w, nil
}

21
vendor/github.com/svanharmelen/jsonapi/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Google Inc.
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.

457
vendor/github.com/svanharmelen/jsonapi/README.md generated vendored Normal file
View File

@ -0,0 +1,457 @@
# jsonapi
[![Build Status](https://travis-ci.org/google/jsonapi.svg?branch=master)](https://travis-ci.org/google/jsonapi)
[![Go Report Card](https://goreportcard.com/badge/github.com/google/jsonapi)](https://goreportcard.com/report/github.com/google/jsonapi)
[![GoDoc](https://godoc.org/github.com/google/jsonapi?status.svg)](http://godoc.org/github.com/google/jsonapi)
A serializer/deserializer for JSON payloads that comply to the
[JSON API - jsonapi.org](http://jsonapi.org) spec in go.
## Installation
```
go get -u github.com/google/jsonapi
```
Or, see [Alternative Installation](#alternative-installation).
## Background
You are working in your Go web application and you have a struct that is
organized similarly to your database schema. You need to send and
receive json payloads that adhere to the JSON API spec. Once you realize that
your json needed to take on this special form, you go down the path of
creating more structs to be able to serialize and deserialize JSON API
payloads. Then there are more models required with this additional
structure. Ugh! With JSON API, you can keep your model structs as is and
use [StructTags](http://golang.org/pkg/reflect/#StructTag) to indicate
to JSON API how you want your response built or your request
deserialized. What about your relationships? JSON API supports
relationships out of the box and will even put them in your response
into an `included` side-loaded slice--that contains associated records.
## Introduction
JSON API uses [StructField](http://golang.org/pkg/reflect/#StructField)
tags to annotate the structs fields that you already have and use in
your app and then reads and writes [JSON API](http://jsonapi.org)
output based on the instructions you give the library in your JSON API
tags. Let's take an example. In your app, you most likely have structs
that look similar to these:
```go
type Blog struct {
ID int `json:"id"`
Title string `json:"title"`
Posts []*Post `json:"posts"`
CurrentPost *Post `json:"current_post"`
CurrentPostId int `json:"current_post_id"`
CreatedAt time.Time `json:"created_at"`
ViewCount int `json:"view_count"`
}
type Post struct {
ID int `json:"id"`
BlogID int `json:"blog_id"`
Title string `json:"title"`
Body string `json:"body"`
Comments []*Comment `json:"comments"`
}
type Comment struct {
Id int `json:"id"`
PostID int `json:"post_id"`
Body string `json:"body"`
Likes uint `json:"likes_count,omitempty"`
}
```
These structs may or may not resemble the layout of your database. But
these are the ones that you want to use right? You wouldn't want to use
structs like those that JSON API sends because it is difficult to get at
all of your data easily.
## Example App
[examples/app.go](https://github.com/google/jsonapi/blob/master/examples/app.go)
This program demonstrates the implementation of a create, a show,
and a list [http.Handler](http://golang.org/pkg/net/http#Handler). It
outputs some example requests and responses as well as serialized
examples of the source/target structs to json. That is to say, I show
you that the library has successfully taken your JSON API request and
turned it into your struct types.
To run,
* Make sure you have [Go installed](https://golang.org/doc/install)
* Create the following directories or similar: `~/go`
* Set `GOPATH` to `PWD` in your shell session, `export GOPATH=$PWD`
* `go get github.com/google/jsonapi`. (Append `-u` after `get` if you
are updating.)
* `cd $GOPATH/src/github.com/google/jsonapi/examples`
* `go build && ./examples`
## `jsonapi` Tag Reference
### Example
The `jsonapi` [StructTags](http://golang.org/pkg/reflect/#StructTag)
tells this library how to marshal and unmarshal your structs into
JSON API payloads and your JSON API payloads to structs, respectively.
Then Use JSON API's Marshal and Unmarshal methods to construct and read
your responses and replies. Here's an example of the structs above
using JSON API tags:
```go
type Blog struct {
ID int `jsonapi:"primary,blogs"`
Title string `jsonapi:"attr,title"`
Posts []*Post `jsonapi:"relation,posts"`
CurrentPost *Post `jsonapi:"relation,current_post"`
CurrentPostID int `jsonapi:"attr,current_post_id"`
CreatedAt time.Time `jsonapi:"attr,created_at"`
ViewCount int `jsonapi:"attr,view_count"`
}
type Post struct {
ID int `jsonapi:"primary,posts"`
BlogID int `jsonapi:"attr,blog_id"`
Title string `jsonapi:"attr,title"`
Body string `jsonapi:"attr,body"`
Comments []*Comment `jsonapi:"relation,comments"`
}
type Comment struct {
ID int `jsonapi:"primary,comments"`
PostID int `jsonapi:"attr,post_id"`
Body string `jsonapi:"attr,body"`
Likes uint `jsonapi:"attr,likes-count,omitempty"`
}
```
### Permitted Tag Values
#### `primary`
```
`jsonapi:"primary,<type field output>"`
```
This indicates this is the primary key field for this struct type.
Tag value arguments are comma separated. The first argument must be,
`primary`, and the second must be the name that should appear in the
`type`\* field for all data objects that represent this type of model.
\* According the [JSON API](http://jsonapi.org) spec, the plural record
types are shown in the examples, but not required.
#### `attr`
```
`jsonapi:"attr,<key name in attributes hash>,<optional: omitempty>"`
```
These fields' values will end up in the `attributes`hash for a record.
The first argument must be, `attr`, and the second should be the name
for the key to display in the `attributes` hash for that record. The optional
third argument is `omitempty` - if it is present the field will not be present
in the `"attributes"` if the field's value is equivalent to the field types
empty value (ie if the `count` field is of type `int`, `omitempty` will omit the
field when `count` has a value of `0`). Lastly, the spec indicates that
`attributes` key names should be dasherized for multiple word field names.
#### `relation`
```
`jsonapi:"relation,<key name in relationships hash>,<optional: omitempty>"`
```
Relations are struct fields that represent a one-to-one or one-to-many
relationship with other structs. JSON API will traverse the graph of
relationships and marshal or unmarshal records. The first argument must
be, `relation`, and the second should be the name of the relationship,
used as the key in the `relationships` hash for the record. The optional
third argument is `omitempty` - if present will prevent non existent to-one and
to-many from being serialized.
## Methods Reference
**All `Marshal` and `Unmarshal` methods expect pointers to struct
instance or slices of the same contained with the `interface{}`s**
Now you have your structs prepared to be seralized or materialized, What
about the rest?
### Create Record Example
You can Unmarshal a JSON API payload using
[jsonapi.UnmarshalPayload](http://godoc.org/github.com/google/jsonapi#UnmarshalPayload).
It reads from an [io.Reader](https://golang.org/pkg/io/#Reader)
containing a JSON API payload for one record (but can have related
records). Then, it materializes a struct that you created and passed in
(using new or &). Again, the method supports single records only, at
the top level, in request payloads at the moment. Bulk creates and
updates are not supported yet.
After saving your record, you can use,
[MarshalOnePayload](http://godoc.org/github.com/google/jsonapi#MarshalOnePayload),
to write the JSON API response to an
[io.Writer](https://golang.org/pkg/io/#Writer).
#### `UnmarshalPayload`
```go
UnmarshalPayload(in io.Reader, model interface{})
```
Visit [godoc](http://godoc.org/github.com/google/jsonapi#UnmarshalPayload)
#### `MarshalPayload`
```go
MarshalPayload(w io.Writer, models interface{}) error
```
Visit [godoc](http://godoc.org/github.com/google/jsonapi#MarshalPayload)
Writes a JSON API response, with related records sideloaded, into an
`included` array. This method encodes a response for either a single record or
many records.
##### Handler Example Code
```go
func CreateBlog(w http.ResponseWriter, r *http.Request) {
blog := new(Blog)
if err := jsonapi.UnmarshalPayload(r.Body, blog); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// ...save your blog...
w.Header().Set("Content-Type", jsonapi.MediaType)
w.WriteHeader(http.StatusCreated)
if err := jsonapi.MarshalPayload(w, blog); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
```
### Create Records Example
#### `UnmarshalManyPayload`
```go
UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error)
```
Visit [godoc](http://godoc.org/github.com/google/jsonapi#UnmarshalManyPayload)
Takes an `io.Reader` and a `reflect.Type` representing the uniform type
contained within the `"data"` JSON API member.
##### Handler Example Code
```go
func CreateBlogs(w http.ResponseWriter, r *http.Request) {
// ...create many blogs at once
blogs, err := UnmarshalManyPayload(r.Body, reflect.TypeOf(new(Blog)))
if err != nil {
t.Fatal(err)
}
for _, blog := range blogs {
b, ok := blog.(*Blog)
// ...save each of your blogs
}
w.Header().Set("Content-Type", jsonapi.MediaType)
w.WriteHeader(http.StatusCreated)
if err := jsonapi.MarshalPayload(w, blogs); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
```
### Links
If you need to include [link objects](http://jsonapi.org/format/#document-links) along with response data, implement the `Linkable` interface for document-links, and `RelationshipLinkable` for relationship links:
```go
func (post Post) JSONAPILinks() *Links {
return &Links{
"self": "href": fmt.Sprintf("https://example.com/posts/%d", post.ID),
"comments": Link{
Href: fmt.Sprintf("https://example.com/api/blogs/%d/comments", post.ID),
Meta: map[string]interface{}{
"counts": map[string]uint{
"likes": 4,
},
},
},
}
}
// Invoked for each relationship defined on the Post struct when marshaled
func (post Post) JSONAPIRelationshipLinks(relation string) *Links {
if relation == "comments" {
return &Links{
"related": fmt.Sprintf("https://example.com/posts/%d/comments", post.ID),
}
}
return nil
}
```
### Meta
If you need to include [meta objects](http://jsonapi.org/format/#document-meta) along with response data, implement the `Metable` interface for document-meta, and `RelationshipMetable` for relationship meta:
```go
func (post Post) JSONAPIMeta() *Meta {
return &Meta{
"details": "sample details here",
}
}
// Invoked for each relationship defined on the Post struct when marshaled
func (post Post) JSONAPIRelationshipMeta(relation string) *Meta {
if relation == "comments" {
return &Meta{
"this": map[string]interface{}{
"can": map[string]interface{}{
"go": []interface{}{
"as",
"deep",
map[string]interface{}{
"as": "required",
},
},
},
},
}
}
return nil
}
```
### Errors
This package also implements support for JSON API compatible `errors` payloads using the following types.
#### `MarshalErrors`
```go
MarshalErrors(w io.Writer, errs []*ErrorObject) error
```
Writes a JSON API response using the given `[]error`.
#### `ErrorsPayload`
```go
type ErrorsPayload struct {
Errors []*ErrorObject `json:"errors"`
}
```
ErrorsPayload is a serializer struct for representing a valid JSON API errors payload.
#### `ErrorObject`
```go
type ErrorObject struct { ... }
// Error implements the `Error` interface.
func (e *ErrorObject) Error() string {
return fmt.Sprintf("Error: %s %s\n", e.Title, e.Detail)
}
```
ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object.
The main idea behind this struct is that you can use it directly in your code as an error type and pass it directly to `MarshalErrors` to get a valid JSON API errors payload.
##### Errors Example Code
```go
// An error has come up in your code, so set an appropriate status, and serialize the error.
if err := validate(&myStructToValidate); err != nil {
context.SetStatusCode(http.StatusBadRequest) // Or however you need to set a status.
jsonapi.MarshalErrors(w, []*ErrorObject{{
Title: "Validation Error",
Detail: "Given request body was invalid.",
Status: "400",
Meta: map[string]interface{}{"field": "some_field", "error": "bad type", "expected": "string", "received": "float64"},
}})
return
}
```
## Testing
### `MarshalOnePayloadEmbedded`
```go
MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error
```
Visit [godoc](http://godoc.org/github.com/google/jsonapi#MarshalOnePayloadEmbedded)
This method is not strictly meant to for use in implementation code,
although feel free. It was mainly created for use in tests; in most cases,
your request payloads for create will be embedded rather than sideloaded
for related records. This method will serialize a single struct pointer
into an embedded json response. In other words, there will be no,
`included`, array in the json; all relationships will be serialized
inline with the data.
However, in tests, you may want to construct payloads to post to create
methods that are embedded to most closely model the payloads that will
be produced by the client. This method aims to enable that.
### Example
```go
out := bytes.NewBuffer(nil)
// testModel returns a pointer to a Blog
jsonapi.MarshalOnePayloadEmbedded(out, testModel())
h := new(BlogsHandler)
w := httptest.NewRecorder()
r, _ := http.NewRequest(http.MethodPost, "/blogs", out)
h.CreateBlog(w, r)
blog := new(Blog)
jsonapi.UnmarshalPayload(w.Body, blog)
// ... assert stuff about blog here ...
```
## Alternative Installation
I use git subtrees to manage dependencies rather than `go get` so that
the src is committed to my repo.
```
git subtree add --squash --prefix=src/github.com/google/jsonapi https://github.com/google/jsonapi.git master
```
To update,
```
git subtree pull --squash --prefix=src/github.com/google/jsonapi https://github.com/google/jsonapi.git master
```
This assumes that I have my repo structured with a `src` dir containing
a collection of packages and `GOPATH` is set to the root
folder--containing `src`.
## Contributing
Fork, Change, Pull Request *with tests*.

55
vendor/github.com/svanharmelen/jsonapi/constants.go generated vendored Normal file
View File

@ -0,0 +1,55 @@
package jsonapi
const (
// StructTag annotation strings
annotationJSONAPI = "jsonapi"
annotationPrimary = "primary"
annotationClientID = "client-id"
annotationAttribute = "attr"
annotationRelation = "relation"
annotationOmitEmpty = "omitempty"
annotationISO8601 = "iso8601"
annotationSeperator = ","
iso8601TimeFormat = "2006-01-02T15:04:05Z"
// MediaType is the identifier for the JSON API media type
//
// see http://jsonapi.org/format/#document-structure
MediaType = "application/vnd.api+json"
// Pagination Constants
//
// http://jsonapi.org/format/#fetching-pagination
// KeyFirstPage is the key to the links object whose value contains a link to
// the first page of data
KeyFirstPage = "first"
// KeyLastPage is the key to the links object whose value contains a link to
// the last page of data
KeyLastPage = "last"
// KeyPreviousPage is the key to the links object whose value contains a link
// to the previous page of data
KeyPreviousPage = "prev"
// KeyNextPage is the key to the links object whose value contains a link to
// the next page of data
KeyNextPage = "next"
// QueryParamPageNumber is a JSON API query parameter used in a page based
// pagination strategy in conjunction with QueryParamPageSize
QueryParamPageNumber = "page[number]"
// QueryParamPageSize is a JSON API query parameter used in a page based
// pagination strategy in conjunction with QueryParamPageNumber
QueryParamPageSize = "page[size]"
// QueryParamPageOffset is a JSON API query parameter used in an offset based
// pagination strategy in conjunction with QueryParamPageLimit
QueryParamPageOffset = "page[offset]"
// QueryParamPageLimit is a JSON API query parameter used in an offset based
// pagination strategy in conjunction with QueryParamPageOffset
QueryParamPageLimit = "page[limit]"
// QueryParamPageCursor is a JSON API query parameter used with a cursor-based
// strategy
QueryParamPageCursor = "page[cursor]"
)

70
vendor/github.com/svanharmelen/jsonapi/doc.go generated vendored Normal file
View File

@ -0,0 +1,70 @@
/*
Package jsonapi provides a serializer and deserializer for jsonapi.org spec payloads.
You can keep your model structs as is and use struct field tags to indicate to jsonapi
how you want your response built or your request deserialzied. What about my relationships?
jsonapi supports relationships out of the box and will even side load them in your response
into an "included" array--that contains associated objects.
jsonapi uses StructField tags to annotate the structs fields that you already have and use
in your app and then reads and writes jsonapi.org output based on the instructions you give
the library in your jsonapi tags.
Example structs using a Blog > Post > Comment structure,
type Blog struct {
ID int `jsonapi:"primary,blogs"`
Title string `jsonapi:"attr,title"`
Posts []*Post `jsonapi:"relation,posts"`
CurrentPost *Post `jsonapi:"relation,current_post"`
CurrentPostID int `jsonapi:"attr,current_post_id"`
CreatedAt time.Time `jsonapi:"attr,created_at"`
ViewCount int `jsonapi:"attr,view_count"`
}
type Post struct {
ID int `jsonapi:"primary,posts"`
BlogID int `jsonapi:"attr,blog_id"`
Title string `jsonapi:"attr,title"`
Body string `jsonapi:"attr,body"`
Comments []*Comment `jsonapi:"relation,comments"`
}
type Comment struct {
ID int `jsonapi:"primary,comments"`
PostID int `jsonapi:"attr,post_id"`
Body string `jsonapi:"attr,body"`
}
jsonapi Tag Reference
Value, primary: "primary,<type field output>"
This indicates that this is the primary key field for this struct type. Tag
value arguments are comma separated. The first argument must be, "primary", and
the second must be the name that should appear in the "type" field for all data
objects that represent this type of model.
Value, attr: "attr,<key name in attributes hash>[,<extra arguments>]"
These fields' values should end up in the "attribute" hash for a record. The first
argument must be, "attr', and the second should be the name for the key to display in
the the "attributes" hash for that record.
The following extra arguments are also supported:
"omitempty": excludes the fields value from the "attribute" hash.
"iso8601": uses the ISO8601 timestamp format when serialising or deserialising the time.Time value.
Value, relation: "relation,<key name in relationships hash>"
Relations are struct fields that represent a one-to-one or one-to-many to other structs.
jsonapi will traverse the graph of relationships and marshal or unmarshal records. The first
argument must be, "relation", and the second should be the name of the relationship, used as
the key in the "relationships" hash for the record.
Use the methods below to Marshal and Unmarshal jsonapi.org json payloads.
Visit the readme at https://github.com/google/jsonapi
*/
package jsonapi

55
vendor/github.com/svanharmelen/jsonapi/errors.go generated vendored Normal file
View File

@ -0,0 +1,55 @@
package jsonapi
import (
"encoding/json"
"fmt"
"io"
)
// MarshalErrors writes a JSON API response using the given `[]error`.
//
// For more information on JSON API error payloads, see the spec here:
// http://jsonapi.org/format/#document-top-level
// and here: http://jsonapi.org/format/#error-objects.
func MarshalErrors(w io.Writer, errorObjects []*ErrorObject) error {
if err := json.NewEncoder(w).Encode(&ErrorsPayload{Errors: errorObjects}); err != nil {
return err
}
return nil
}
// ErrorsPayload is a serializer struct for representing a valid JSON API errors payload.
type ErrorsPayload struct {
Errors []*ErrorObject `json:"errors"`
}
// ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object.
//
// The main idea behind this struct is that you can use it directly in your code as an error type
// and pass it directly to `MarshalErrors` to get a valid JSON API errors payload.
// For more information on Golang errors, see: https://golang.org/pkg/errors/
// For more information on the JSON API spec's error objects, see: http://jsonapi.org/format/#error-objects
type ErrorObject struct {
// ID is a unique identifier for this particular occurrence of a problem.
ID string `json:"id,omitempty"`
// Title is a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.
Title string `json:"title,omitempty"`
// Detail is a human-readable explanation specific to this occurrence of the problem. Like title, this fields value can be localized.
Detail string `json:"detail,omitempty"`
// Status is the HTTP status code applicable to this problem, expressed as a string value.
Status string `json:"status,omitempty"`
// Code is an application-specific error code, expressed as a string value.
Code string `json:"code,omitempty"`
// Meta is an object containing non-standard meta-information about the error.
Meta *map[string]interface{} `json:"meta,omitempty"`
}
// Error implements the `Error` interface.
func (e *ErrorObject) Error() string {
return fmt.Sprintf("Error: %s %s\n", e.Title, e.Detail)
}

121
vendor/github.com/svanharmelen/jsonapi/node.go generated vendored Normal file
View File

@ -0,0 +1,121 @@
package jsonapi
import "fmt"
// Payloader is used to encapsulate the One and Many payload types
type Payloader interface {
clearIncluded()
}
// OnePayload is used to represent a generic JSON API payload where a single
// resource (Node) was included as an {} in the "data" key
type OnePayload struct {
Data *Node `json:"data"`
Included []*Node `json:"included,omitempty"`
Links *Links `json:"links,omitempty"`
Meta *Meta `json:"meta,omitempty"`
}
func (p *OnePayload) clearIncluded() {
p.Included = []*Node{}
}
// ManyPayload is used to represent a generic JSON API payload where many
// resources (Nodes) were included in an [] in the "data" key
type ManyPayload struct {
Data []*Node `json:"data"`
Included []*Node `json:"included,omitempty"`
Links *Links `json:"links,omitempty"`
Meta *Meta `json:"meta,omitempty"`
}
func (p *ManyPayload) clearIncluded() {
p.Included = []*Node{}
}
// Node is used to represent a generic JSON API Resource
type Node struct {
Type string `json:"type"`
ID string `json:"id,omitempty"`
ClientID string `json:"client-id,omitempty"`
Attributes map[string]interface{} `json:"attributes,omitempty"`
Relationships map[string]interface{} `json:"relationships,omitempty"`
Links *Links `json:"links,omitempty"`
Meta *Meta `json:"meta,omitempty"`
}
// RelationshipOneNode is used to represent a generic has one JSON API relation
type RelationshipOneNode struct {
Data *Node `json:"data"`
Links *Links `json:"links,omitempty"`
Meta *Meta `json:"meta,omitempty"`
}
// RelationshipManyNode is used to represent a generic has many JSON API
// relation
type RelationshipManyNode struct {
Data []*Node `json:"data"`
Links *Links `json:"links,omitempty"`
Meta *Meta `json:"meta,omitempty"`
}
// Links is used to represent a `links` object.
// http://jsonapi.org/format/#document-links
type Links map[string]interface{}
func (l *Links) validate() (err error) {
// Each member of a links object is a “link”. A link MUST be represented as
// either:
// - a string containing the links URL.
// - an object (“link object”) which can contain the following members:
// - href: a string containing the links URL.
// - meta: a meta object containing non-standard meta-information about the
// link.
for k, v := range *l {
_, isString := v.(string)
_, isLink := v.(Link)
if !(isString || isLink) {
return fmt.Errorf(
"The %s member of the links object was not a string or link object",
k,
)
}
}
return
}
// Link is used to represent a member of the `links` object.
type Link struct {
Href string `json:"href"`
Meta Meta `json:"meta,omitempty"`
}
// Linkable is used to include document links in response data
// e.g. {"self": "http://example.com/posts/1"}
type Linkable interface {
JSONAPILinks() *Links
}
// RelationshipLinkable is used to include relationship links in response data
// e.g. {"related": "http://example.com/posts/1/comments"}
type RelationshipLinkable interface {
// JSONAPIRelationshipLinks will be invoked for each relationship with the corresponding relation name (e.g. `comments`)
JSONAPIRelationshipLinks(relation string) *Links
}
// Meta is used to represent a `meta` object.
// http://jsonapi.org/format/#document-meta
type Meta map[string]interface{}
// Metable is used to include document meta in response data
// e.g. {"foo": "bar"}
type Metable interface {
JSONAPIMeta() *Meta
}
// RelationshipMetable is used to include relationship meta in response data
type RelationshipMetable interface {
// JSONRelationshipMeta will be invoked for each relationship with the corresponding relation name (e.g. `comments`)
JSONAPIRelationshipMeta(relation string) *Meta
}

680
vendor/github.com/svanharmelen/jsonapi/request.go generated vendored Normal file
View File

@ -0,0 +1,680 @@
package jsonapi
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
"strconv"
"strings"
"time"
)
const (
unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s"
)
var (
// ErrInvalidTime is returned when a struct has a time.Time type field, but
// the JSON value was not a unix timestamp integer.
ErrInvalidTime = errors.New("Only numbers can be parsed as dates, unix timestamps")
// ErrInvalidISO8601 is returned when a struct has a time.Time type field and includes
// "iso8601" in the tag spec, but the JSON value was not an ISO8601 timestamp string.
ErrInvalidISO8601 = errors.New("Only strings can be parsed as dates, ISO8601 timestamps")
// ErrUnknownFieldNumberType is returned when the JSON value was a float
// (numeric) but the Struct field was a non numeric type (i.e. not int, uint,
// float, etc)
ErrUnknownFieldNumberType = errors.New("The struct field was not of a known number type")
// ErrInvalidType is returned when the given type is incompatible with the expected type.
ErrInvalidType = errors.New("Invalid type provided") // I wish we used punctuation.
)
// ErrUnsupportedPtrType is returned when the Struct field was a pointer but
// the JSON value was of a different type
type ErrUnsupportedPtrType struct {
rf reflect.Value
t reflect.Type
structField reflect.StructField
}
func (eupt ErrUnsupportedPtrType) Error() string {
typeName := eupt.t.Elem().Name()
kind := eupt.t.Elem().Kind()
if kind.String() != "" && kind.String() != typeName {
typeName = fmt.Sprintf("%s (%s)", typeName, kind.String())
}
return fmt.Sprintf(
"jsonapi: Can't unmarshal %+v (%s) to struct field `%s`, which is a pointer to `%s`",
eupt.rf, eupt.rf.Type().Kind(), eupt.structField.Name, typeName,
)
}
func newErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField reflect.StructField) error {
return ErrUnsupportedPtrType{rf, t, structField}
}
// UnmarshalPayload converts an io into a struct instance using jsonapi tags on
// struct fields. This method supports single request payloads only, at the
// moment. Bulk creates and updates are not supported yet.
//
// Will Unmarshal embedded and sideloaded payloads. The latter is only possible if the
// object graph is complete. That is, in the "relationships" data there are type and id,
// keys that correspond to records in the "included" array.
//
// For example you could pass it, in, req.Body and, model, a BlogPost
// struct instance to populate in an http handler,
//
// func CreateBlog(w http.ResponseWriter, r *http.Request) {
// blog := new(Blog)
//
// if err := jsonapi.UnmarshalPayload(r.Body, blog); err != nil {
// http.Error(w, err.Error(), 500)
// return
// }
//
// // ...do stuff with your blog...
//
// w.Header().Set("Content-Type", jsonapi.MediaType)
// w.WriteHeader(201)
//
// if err := jsonapi.MarshalPayload(w, blog); err != nil {
// http.Error(w, err.Error(), 500)
// }
// }
//
//
// Visit https://github.com/google/jsonapi#create for more info.
//
// model interface{} should be a pointer to a struct.
func UnmarshalPayload(in io.Reader, model interface{}) error {
payload := new(OnePayload)
if err := json.NewDecoder(in).Decode(payload); err != nil {
return err
}
if payload.Included != nil {
includedMap := make(map[string]*Node)
for _, included := range payload.Included {
key := fmt.Sprintf("%s,%s", included.Type, included.ID)
includedMap[key] = included
}
return unmarshalNode(payload.Data, reflect.ValueOf(model), &includedMap)
}
return unmarshalNode(payload.Data, reflect.ValueOf(model), nil)
}
// UnmarshalManyPayload converts an io into a set of struct instances using
// jsonapi tags on the type's struct fields.
func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
payload := new(ManyPayload)
if err := json.NewDecoder(in).Decode(payload); err != nil {
return nil, err
}
models := []interface{}{} // will be populated from the "data"
includedMap := map[string]*Node{} // will be populate from the "included"
if payload.Included != nil {
for _, included := range payload.Included {
key := fmt.Sprintf("%s,%s", included.Type, included.ID)
includedMap[key] = included
}
}
for _, data := range payload.Data {
model := reflect.New(t.Elem())
err := unmarshalNode(data, model, &includedMap)
if err != nil {
return nil, err
}
models = append(models, model.Interface())
}
return models, nil
}
func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("data is not a jsonapi representation of '%v'", model.Type())
}
}()
modelValue := model.Elem()
modelType := model.Type().Elem()
var er error
for i := 0; i < modelValue.NumField(); i++ {
fieldType := modelType.Field(i)
tag := fieldType.Tag.Get("jsonapi")
if tag == "" {
continue
}
fieldValue := modelValue.Field(i)
args := strings.Split(tag, ",")
if len(args) < 1 {
er = ErrBadJSONAPIStructTag
break
}
annotation := args[0]
if (annotation == annotationClientID && len(args) != 1) ||
(annotation != annotationClientID && len(args) < 2) {
er = ErrBadJSONAPIStructTag
break
}
if annotation == annotationPrimary {
if data.ID == "" {
continue
}
// Check the JSON API Type
if data.Type != args[1] {
er = fmt.Errorf(
"Trying to Unmarshal an object of type %#v, but %#v does not match",
data.Type,
args[1],
)
break
}
// ID will have to be transmitted as astring per the JSON API spec
v := reflect.ValueOf(data.ID)
// Deal with PTRS
var kind reflect.Kind
if fieldValue.Kind() == reflect.Ptr {
kind = fieldType.Type.Elem().Kind()
} else {
kind = fieldType.Type.Kind()
}
// Handle String case
if kind == reflect.String {
assign(fieldValue, v)
continue
}
// Value was not a string... only other supported type was a numeric,
// which would have been sent as a float value.
floatValue, err := strconv.ParseFloat(data.ID, 64)
if err != nil {
// Could not convert the value in the "id" attr to a float
er = ErrBadJSONAPIID
break
}
// Convert the numeric float to one of the supported ID numeric types
// (int[8,16,32,64] or uint[8,16,32,64])
var idValue reflect.Value
switch kind {
case reflect.Int:
n := int(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Int8:
n := int8(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Int16:
n := int16(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Int32:
n := int32(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Int64:
n := int64(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Uint:
n := uint(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Uint8:
n := uint8(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Uint16:
n := uint16(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Uint32:
n := uint32(floatValue)
idValue = reflect.ValueOf(&n)
case reflect.Uint64:
n := uint64(floatValue)
idValue = reflect.ValueOf(&n)
default:
// We had a JSON float (numeric), but our field was not one of the
// allowed numeric types
er = ErrBadJSONAPIID
break
}
assign(fieldValue, idValue)
} else if annotation == annotationClientID {
if data.ClientID == "" {
continue
}
fieldValue.Set(reflect.ValueOf(data.ClientID))
} else if annotation == annotationAttribute {
attributes := data.Attributes
if attributes == nil || len(data.Attributes) == 0 {
continue
}
attribute := attributes[args[1]]
// continue if the attribute was not included in the request
if attribute == nil {
continue
}
structField := fieldType
value, err := unmarshalAttribute(attribute, args, structField, fieldValue)
if err != nil {
er = err
break
}
assign(fieldValue, value)
continue
} else if annotation == annotationRelation {
isSlice := fieldValue.Type().Kind() == reflect.Slice
if data.Relationships == nil || data.Relationships[args[1]] == nil {
continue
}
if isSlice {
// to-many relationship
relationship := new(RelationshipManyNode)
buf := bytes.NewBuffer(nil)
json.NewEncoder(buf).Encode(data.Relationships[args[1]])
json.NewDecoder(buf).Decode(relationship)
data := relationship.Data
models := reflect.New(fieldValue.Type()).Elem()
for _, n := range data {
m := reflect.New(fieldValue.Type().Elem().Elem())
if err := unmarshalNode(
fullNode(n, included),
m,
included,
); err != nil {
er = err
break
}
models = reflect.Append(models, m)
}
fieldValue.Set(models)
} else {
// to-one relationships
relationship := new(RelationshipOneNode)
buf := bytes.NewBuffer(nil)
json.NewEncoder(buf).Encode(
data.Relationships[args[1]],
)
json.NewDecoder(buf).Decode(relationship)
/*
http://jsonapi.org/format/#document-resource-object-relationships
http://jsonapi.org/format/#document-resource-object-linkage
relationship can have a data node set to null (e.g. to disassociate the relationship)
so unmarshal and set fieldValue only if data obj is not null
*/
if relationship.Data == nil {
continue
}
m := reflect.New(fieldValue.Type().Elem())
if err := unmarshalNode(
fullNode(relationship.Data, included),
m,
included,
); err != nil {
er = err
break
}
fieldValue.Set(m)
}
} else {
er = fmt.Errorf(unsuportedStructTagMsg, annotation)
}
}
return er
}
func fullNode(n *Node, included *map[string]*Node) *Node {
includedKey := fmt.Sprintf("%s,%s", n.Type, n.ID)
if included != nil && (*included)[includedKey] != nil {
return (*included)[includedKey]
}
return n
}
// assign will take the value specified and assign it to the field; if
// field is expecting a ptr assign will assign a ptr.
func assign(field, value reflect.Value) {
value = reflect.Indirect(value)
if field.Kind() == reflect.Ptr {
// initialize pointer so it's value
// can be set by assignValue
field.Set(reflect.New(field.Type().Elem()))
assignValue(field.Elem(), value)
} else {
assignValue(field, value)
}
}
// assign assigns the specified value to the field,
// expecting both values not to be pointer types.
func assignValue(field, value reflect.Value) {
switch field.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64:
field.SetInt(value.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64, reflect.Uintptr:
field.SetUint(value.Uint())
case reflect.Float32, reflect.Float64:
field.SetFloat(value.Float())
case reflect.String:
field.SetString(value.String())
case reflect.Bool:
field.SetBool(value.Bool())
default:
field.Set(value)
}
}
func unmarshalAttribute(
attribute interface{},
args []string,
structField reflect.StructField,
fieldValue reflect.Value) (value reflect.Value, err error) {
value = reflect.ValueOf(attribute)
fieldType := structField.Type
// Handle field of type []string
if fieldValue.Type() == reflect.TypeOf([]string{}) {
value, err = handleStringSlice(attribute, args, fieldType, fieldValue)
return
}
// Handle field of type time.Time
if fieldValue.Type() == reflect.TypeOf(time.Time{}) ||
fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
value, err = handleTime(attribute, args, fieldType, fieldValue)
return
}
// Handle field of type struct
if fieldValue.Type().Kind() == reflect.Struct {
value, err = handleStruct(attribute, args, fieldType, fieldValue)
return
}
// Handle field containing slice of structs
if fieldValue.Type().Kind() == reflect.Slice {
elem := reflect.TypeOf(fieldValue.Interface()).Elem()
if elem.Kind() == reflect.Ptr {
elem = elem.Elem()
}
if elem.Kind() == reflect.Struct {
value, err = handleStructSlice(attribute, args, fieldType, fieldValue)
return
}
}
// JSON value was a float (numeric)
if value.Kind() == reflect.Float64 {
value, err = handleNumeric(attribute, args, fieldType, fieldValue)
return
}
// Field was a Pointer type
if fieldValue.Kind() == reflect.Ptr {
value, err = handlePointer(attribute, args, fieldType, fieldValue, structField)
return
}
// As a final catch-all, ensure types line up to avoid a runtime panic.
if fieldValue.Kind() != value.Kind() {
err = ErrInvalidType
return
}
return
}
func handleStringSlice(
attribute interface{},
args []string,
fieldType reflect.Type,
fieldValue reflect.Value) (reflect.Value, error) {
v := reflect.ValueOf(attribute)
values := make([]string, v.Len())
for i := 0; i < v.Len(); i++ {
values[i] = v.Index(i).Interface().(string)
}
return reflect.ValueOf(values), nil
}
func handleTime(
attribute interface{},
args []string,
fieldType reflect.Type,
fieldValue reflect.Value) (reflect.Value, error) {
var isIso8601 bool
v := reflect.ValueOf(attribute)
if len(args) > 2 {
for _, arg := range args[2:] {
if arg == annotationISO8601 {
isIso8601 = true
}
}
}
if isIso8601 {
var tm string
if v.Kind() == reflect.String {
tm = v.Interface().(string)
} else {
return reflect.ValueOf(time.Now()), ErrInvalidISO8601
}
t, err := time.Parse(iso8601TimeFormat, tm)
if err != nil {
return reflect.ValueOf(time.Now()), ErrInvalidISO8601
}
if fieldValue.Kind() == reflect.Ptr {
return reflect.ValueOf(&t), nil
}
return reflect.ValueOf(t), nil
}
var at int64
if v.Kind() == reflect.Float64 {
at = int64(v.Interface().(float64))
} else if v.Kind() == reflect.Int {
at = v.Int()
} else {
return reflect.ValueOf(time.Now()), ErrInvalidTime
}
t := time.Unix(at, 0)
return reflect.ValueOf(t), nil
}
func handleNumeric(
attribute interface{},
args []string,
fieldType reflect.Type,
fieldValue reflect.Value) (reflect.Value, error) {
v := reflect.ValueOf(attribute)
floatValue := v.Interface().(float64)
var kind reflect.Kind
if fieldValue.Kind() == reflect.Ptr {
kind = fieldType.Elem().Kind()
} else {
kind = fieldType.Kind()
}
var numericValue reflect.Value
switch kind {
case reflect.Int:
n := int(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Int8:
n := int8(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Int16:
n := int16(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Int32:
n := int32(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Int64:
n := int64(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Uint:
n := uint(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Uint8:
n := uint8(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Uint16:
n := uint16(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Uint32:
n := uint32(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Uint64:
n := uint64(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Float32:
n := float32(floatValue)
numericValue = reflect.ValueOf(&n)
case reflect.Float64:
n := floatValue
numericValue = reflect.ValueOf(&n)
default:
return reflect.Value{}, ErrUnknownFieldNumberType
}
return numericValue, nil
}
func handlePointer(
attribute interface{},
args []string,
fieldType reflect.Type,
fieldValue reflect.Value,
structField reflect.StructField) (reflect.Value, error) {
t := fieldValue.Type()
var concreteVal reflect.Value
switch cVal := attribute.(type) {
case string:
concreteVal = reflect.ValueOf(&cVal)
case bool:
concreteVal = reflect.ValueOf(&cVal)
case complex64, complex128, uintptr:
concreteVal = reflect.ValueOf(&cVal)
case map[string]interface{}:
var err error
concreteVal, err = handleStruct(attribute, args, fieldType, fieldValue)
if err != nil {
return reflect.Value{}, newErrUnsupportedPtrType(
reflect.ValueOf(attribute), fieldType, structField)
}
return concreteVal.Elem(), err
default:
return reflect.Value{}, newErrUnsupportedPtrType(
reflect.ValueOf(attribute), fieldType, structField)
}
if t != concreteVal.Type() {
return reflect.Value{}, newErrUnsupportedPtrType(
reflect.ValueOf(attribute), fieldType, structField)
}
return concreteVal, nil
}
func handleStruct(
attribute interface{},
args []string,
fieldType reflect.Type,
fieldValue reflect.Value) (reflect.Value, error) {
model := reflect.New(fieldValue.Type())
data, err := json.Marshal(attribute)
if err != nil {
return model, err
}
err = json.Unmarshal(data, model.Interface())
if err != nil {
return model, err
}
return model, err
}
func handleStructSlice(
attribute interface{},
args []string,
fieldType reflect.Type,
fieldValue reflect.Value) (reflect.Value, error) {
models := reflect.New(fieldValue.Type()).Elem()
dataMap := reflect.ValueOf(attribute).Interface().([]interface{})
for _, data := range dataMap {
model := reflect.New(fieldValue.Type().Elem()).Elem()
modelType := model.Type()
value, err := handleStruct(data, []string{}, modelType, model)
if err != nil {
continue
}
models = reflect.Append(models, reflect.Indirect(value))
}
return models, nil
}

539
vendor/github.com/svanharmelen/jsonapi/response.go generated vendored Normal file
View File

@ -0,0 +1,539 @@
package jsonapi
import (
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
"strconv"
"strings"
"time"
)
var (
// ErrBadJSONAPIStructTag is returned when the Struct field's JSON API
// annotation is invalid.
ErrBadJSONAPIStructTag = errors.New("Bad jsonapi struct tag format")
// ErrBadJSONAPIID is returned when the Struct JSON API annotated "id" field
// was not a valid numeric type.
ErrBadJSONAPIID = errors.New(
"id should be either string, int(8,16,32,64) or uint(8,16,32,64)")
// ErrExpectedSlice is returned when a variable or argument was expected to
// be a slice of *Structs; MarshalMany will return this error when its
// interface{} argument is invalid.
ErrExpectedSlice = errors.New("models should be a slice of struct pointers")
// ErrUnexpectedType is returned when marshalling an interface; the interface
// had to be a pointer or a slice; otherwise this error is returned.
ErrUnexpectedType = errors.New("models should be a struct pointer or slice of struct pointers")
)
// MarshalPayload writes a jsonapi response for one or many records. The
// related records are sideloaded into the "included" array. If this method is
// given a struct pointer as an argument it will serialize in the form
// "data": {...}. If this method is given a slice of pointers, this method will
// serialize in the form "data": [...]
//
// One Example: you could pass it, w, your http.ResponseWriter, and, models, a
// ptr to a Blog to be written to the response body:
//
// func ShowBlog(w http.ResponseWriter, r *http.Request) {
// blog := &Blog{}
//
// w.Header().Set("Content-Type", jsonapi.MediaType)
// w.WriteHeader(http.StatusOK)
//
// if err := jsonapi.MarshalPayload(w, blog); err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
// }
// }
//
// Many Example: you could pass it, w, your http.ResponseWriter, and, models, a
// slice of Blog struct instance pointers to be written to the response body:
//
// func ListBlogs(w http.ResponseWriter, r *http.Request) {
// blogs := []*Blog{}
//
// w.Header().Set("Content-Type", jsonapi.MediaType)
// w.WriteHeader(http.StatusOK)
//
// if err := jsonapi.MarshalPayload(w, blogs); err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
// }
// }
//
func MarshalPayload(w io.Writer, models interface{}) error {
payload, err := Marshal(models)
if err != nil {
return err
}
if err := json.NewEncoder(w).Encode(payload); err != nil {
return err
}
return nil
}
// Marshal does the same as MarshalPayload except it just returns the payload
// and doesn't write out results. Useful if you use your own JSON rendering
// library.
func Marshal(models interface{}) (Payloader, error) {
switch vals := reflect.ValueOf(models); vals.Kind() {
case reflect.Slice:
m, err := convertToSliceInterface(&models)
if err != nil {
return nil, err
}
payload, err := marshalMany(m)
if err != nil {
return nil, err
}
if linkableModels, isLinkable := models.(Linkable); isLinkable {
jl := linkableModels.JSONAPILinks()
if er := jl.validate(); er != nil {
return nil, er
}
payload.Links = linkableModels.JSONAPILinks()
}
if metableModels, ok := models.(Metable); ok {
payload.Meta = metableModels.JSONAPIMeta()
}
return payload, nil
case reflect.Ptr:
// Check that the pointer was to a struct
if reflect.Indirect(vals).Kind() != reflect.Struct {
return nil, ErrUnexpectedType
}
return marshalOne(models)
default:
return nil, ErrUnexpectedType
}
}
// MarshalPayloadWithoutIncluded writes a jsonapi response with one or many
// records, without the related records sideloaded into "included" array.
// If you want to serialize the relations into the "included" array see
// MarshalPayload.
//
// models interface{} should be either a struct pointer or a slice of struct
// pointers.
func MarshalPayloadWithoutIncluded(w io.Writer, model interface{}) error {
payload, err := Marshal(model)
if err != nil {
return err
}
payload.clearIncluded()
if err := json.NewEncoder(w).Encode(payload); err != nil {
return err
}
return nil
}
// marshalOne does the same as MarshalOnePayload except it just returns the
// payload and doesn't write out results. Useful is you use your JSON rendering
// library.
func marshalOne(model interface{}) (*OnePayload, error) {
included := make(map[string]*Node)
rootNode, err := visitModelNode(model, &included, true)
if err != nil {
return nil, err
}
payload := &OnePayload{Data: rootNode}
payload.Included = nodeMapValues(&included)
return payload, nil
}
// marshalMany does the same as MarshalManyPayload except it just returns the
// payload and doesn't write out results. Useful is you use your JSON rendering
// library.
func marshalMany(models []interface{}) (*ManyPayload, error) {
payload := &ManyPayload{
Data: []*Node{},
}
included := map[string]*Node{}
for _, model := range models {
node, err := visitModelNode(model, &included, true)
if err != nil {
return nil, err
}
payload.Data = append(payload.Data, node)
}
payload.Included = nodeMapValues(&included)
return payload, nil
}
// MarshalOnePayloadEmbedded - This method not meant to for use in
// implementation code, although feel free. The purpose of this
// method is for use in tests. In most cases, your request
// payloads for create will be embedded rather than sideloaded for
// related records. This method will serialize a single struct
// pointer into an embedded json response. In other words, there
// will be no, "included", array in the json all relationships will
// be serailized inline in the data.
//
// However, in tests, you may want to construct payloads to post
// to create methods that are embedded to most closely resemble
// the payloads that will be produced by the client. This is what
// this method is intended for.
//
// model interface{} should be a pointer to a struct.
func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error {
rootNode, err := visitModelNode(model, nil, false)
if err != nil {
return err
}
payload := &OnePayload{Data: rootNode}
if err := json.NewEncoder(w).Encode(payload); err != nil {
return err
}
return nil
}
func visitModelNode(model interface{}, included *map[string]*Node,
sideload bool) (*Node, error) {
node := new(Node)
var er error
value := reflect.ValueOf(model)
if value.IsNil() {
return nil, nil
}
modelValue := value.Elem()
modelType := value.Type().Elem()
for i := 0; i < modelValue.NumField(); i++ {
structField := modelValue.Type().Field(i)
tag := structField.Tag.Get(annotationJSONAPI)
if tag == "" {
continue
}
fieldValue := modelValue.Field(i)
fieldType := modelType.Field(i)
args := strings.Split(tag, annotationSeperator)
if len(args) < 1 {
er = ErrBadJSONAPIStructTag
break
}
annotation := args[0]
if (annotation == annotationClientID && len(args) != 1) ||
(annotation != annotationClientID && len(args) < 2) {
er = ErrBadJSONAPIStructTag
break
}
if annotation == annotationPrimary {
v := fieldValue
// Deal with PTRS
var kind reflect.Kind
if fieldValue.Kind() == reflect.Ptr {
kind = fieldType.Type.Elem().Kind()
v = reflect.Indirect(fieldValue)
} else {
kind = fieldType.Type.Kind()
}
// Handle allowed types
switch kind {
case reflect.String:
node.ID = v.Interface().(string)
case reflect.Int:
node.ID = strconv.FormatInt(int64(v.Interface().(int)), 10)
case reflect.Int8:
node.ID = strconv.FormatInt(int64(v.Interface().(int8)), 10)
case reflect.Int16:
node.ID = strconv.FormatInt(int64(v.Interface().(int16)), 10)
case reflect.Int32:
node.ID = strconv.FormatInt(int64(v.Interface().(int32)), 10)
case reflect.Int64:
node.ID = strconv.FormatInt(v.Interface().(int64), 10)
case reflect.Uint:
node.ID = strconv.FormatUint(uint64(v.Interface().(uint)), 10)
case reflect.Uint8:
node.ID = strconv.FormatUint(uint64(v.Interface().(uint8)), 10)
case reflect.Uint16:
node.ID = strconv.FormatUint(uint64(v.Interface().(uint16)), 10)
case reflect.Uint32:
node.ID = strconv.FormatUint(uint64(v.Interface().(uint32)), 10)
case reflect.Uint64:
node.ID = strconv.FormatUint(v.Interface().(uint64), 10)
default:
// We had a JSON float (numeric), but our field was not one of the
// allowed numeric types
er = ErrBadJSONAPIID
break
}
node.Type = args[1]
} else if annotation == annotationClientID {
clientID := fieldValue.String()
if clientID != "" {
node.ClientID = clientID
}
} else if annotation == annotationAttribute {
var omitEmpty, iso8601 bool
if len(args) > 2 {
for _, arg := range args[2:] {
switch arg {
case annotationOmitEmpty:
omitEmpty = true
case annotationISO8601:
iso8601 = true
}
}
}
if node.Attributes == nil {
node.Attributes = make(map[string]interface{})
}
if fieldValue.Type() == reflect.TypeOf(time.Time{}) {
t := fieldValue.Interface().(time.Time)
if t.IsZero() {
continue
}
if iso8601 {
node.Attributes[args[1]] = t.UTC().Format(iso8601TimeFormat)
} else {
node.Attributes[args[1]] = t.Unix()
}
} else if fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
// A time pointer may be nil
if fieldValue.IsNil() {
if omitEmpty {
continue
}
node.Attributes[args[1]] = nil
} else {
tm := fieldValue.Interface().(*time.Time)
if tm.IsZero() && omitEmpty {
continue
}
if iso8601 {
node.Attributes[args[1]] = tm.UTC().Format(iso8601TimeFormat)
} else {
node.Attributes[args[1]] = tm.Unix()
}
}
} else {
// Dealing with a fieldValue that is not a time
emptyValue := reflect.Zero(fieldValue.Type())
// See if we need to omit this field
if omitEmpty && reflect.DeepEqual(fieldValue.Interface(), emptyValue.Interface()) {
continue
}
strAttr, ok := fieldValue.Interface().(string)
if ok {
node.Attributes[args[1]] = strAttr
} else {
node.Attributes[args[1]] = fieldValue.Interface()
}
}
} else if annotation == annotationRelation {
var omitEmpty bool
//add support for 'omitempty' struct tag for marshaling as absent
if len(args) > 2 {
omitEmpty = args[2] == annotationOmitEmpty
}
isSlice := fieldValue.Type().Kind() == reflect.Slice
if omitEmpty &&
(isSlice && fieldValue.Len() < 1 ||
(!isSlice && fieldValue.IsNil())) {
continue
}
if node.Relationships == nil {
node.Relationships = make(map[string]interface{})
}
var relLinks *Links
if linkableModel, ok := model.(RelationshipLinkable); ok {
relLinks = linkableModel.JSONAPIRelationshipLinks(args[1])
}
var relMeta *Meta
if metableModel, ok := model.(RelationshipMetable); ok {
relMeta = metableModel.JSONAPIRelationshipMeta(args[1])
}
if isSlice {
// to-many relationship
relationship, err := visitModelNodeRelationships(
fieldValue,
included,
sideload,
)
if err != nil {
er = err
break
}
relationship.Links = relLinks
relationship.Meta = relMeta
if sideload {
shallowNodes := []*Node{}
for _, n := range relationship.Data {
appendIncluded(included, n)
shallowNodes = append(shallowNodes, toShallowNode(n))
}
node.Relationships[args[1]] = &RelationshipManyNode{
Data: shallowNodes,
Links: relationship.Links,
Meta: relationship.Meta,
}
} else {
node.Relationships[args[1]] = relationship
}
} else {
// to-one relationships
// Handle null relationship case
if fieldValue.IsNil() {
node.Relationships[args[1]] = &RelationshipOneNode{Data: nil}
continue
}
relationship, err := visitModelNode(
fieldValue.Interface(),
included,
sideload,
)
if err != nil {
er = err
break
}
if sideload {
appendIncluded(included, relationship)
node.Relationships[args[1]] = &RelationshipOneNode{
Data: toShallowNode(relationship),
Links: relLinks,
Meta: relMeta,
}
} else {
node.Relationships[args[1]] = &RelationshipOneNode{
Data: relationship,
Links: relLinks,
Meta: relMeta,
}
}
}
} else {
er = ErrBadJSONAPIStructTag
break
}
}
if er != nil {
return nil, er
}
if linkableModel, isLinkable := model.(Linkable); isLinkable {
jl := linkableModel.JSONAPILinks()
if er := jl.validate(); er != nil {
return nil, er
}
node.Links = linkableModel.JSONAPILinks()
}
if metableModel, ok := model.(Metable); ok {
node.Meta = metableModel.JSONAPIMeta()
}
return node, nil
}
func toShallowNode(node *Node) *Node {
return &Node{
ID: node.ID,
Type: node.Type,
}
}
func visitModelNodeRelationships(models reflect.Value, included *map[string]*Node,
sideload bool) (*RelationshipManyNode, error) {
nodes := []*Node{}
for i := 0; i < models.Len(); i++ {
n := models.Index(i).Interface()
node, err := visitModelNode(n, included, sideload)
if err != nil {
return nil, err
}
nodes = append(nodes, node)
}
return &RelationshipManyNode{Data: nodes}, nil
}
func appendIncluded(m *map[string]*Node, nodes ...*Node) {
included := *m
for _, n := range nodes {
k := fmt.Sprintf("%s,%s", n.Type, n.ID)
if _, hasNode := included[k]; hasNode {
continue
}
included[k] = n
}
}
func nodeMapValues(m *map[string]*Node) []*Node {
mp := *m
nodes := make([]*Node, len(mp))
i := 0
for _, n := range mp {
nodes[i] = n
i++
}
return nodes
}
func convertToSliceInterface(i *interface{}) ([]interface{}, error) {
vals := reflect.ValueOf(*i)
if vals.Kind() != reflect.Slice {
return nil, ErrExpectedSlice
}
var response []interface{}
for x := 0; x < vals.Len(); x++ {
response = append(response, vals.Index(x).Interface())
}
return response, nil
}

103
vendor/github.com/svanharmelen/jsonapi/runtime.go generated vendored Normal file
View File

@ -0,0 +1,103 @@
package jsonapi
import (
"crypto/rand"
"fmt"
"io"
"reflect"
"time"
)
type Event int
const (
UnmarshalStart Event = iota
UnmarshalStop
MarshalStart
MarshalStop
)
type Runtime struct {
ctx map[string]interface{}
}
type Events func(*Runtime, Event, string, time.Duration)
var Instrumentation Events
func NewRuntime() *Runtime { return &Runtime{make(map[string]interface{})} }
func (r *Runtime) WithValue(key string, value interface{}) *Runtime {
r.ctx[key] = value
return r
}
func (r *Runtime) Value(key string) interface{} {
return r.ctx[key]
}
func (r *Runtime) Instrument(key string) *Runtime {
return r.WithValue("instrument", key)
}
func (r *Runtime) shouldInstrument() bool {
return Instrumentation != nil
}
func (r *Runtime) UnmarshalPayload(reader io.Reader, model interface{}) error {
return r.instrumentCall(UnmarshalStart, UnmarshalStop, func() error {
return UnmarshalPayload(reader, model)
})
}
func (r *Runtime) UnmarshalManyPayload(reader io.Reader, kind reflect.Type) (elems []interface{}, err error) {
r.instrumentCall(UnmarshalStart, UnmarshalStop, func() error {
elems, err = UnmarshalManyPayload(reader, kind)
return err
})
return
}
func (r *Runtime) MarshalPayload(w io.Writer, model interface{}) error {
return r.instrumentCall(MarshalStart, MarshalStop, func() error {
return MarshalPayload(w, model)
})
}
func (r *Runtime) instrumentCall(start Event, stop Event, c func() error) error {
if !r.shouldInstrument() {
return c()
}
instrumentationGUID, err := newUUID()
if err != nil {
return err
}
begin := time.Now()
Instrumentation(r, start, instrumentationGUID, time.Duration(0))
if err := c(); err != nil {
return err
}
diff := time.Duration(time.Now().UnixNano() - begin.UnixNano())
Instrumentation(r, stop, instrumentationGUID, diff)
return nil
}
// citation: http://play.golang.org/p/4FkNSiUDMg
func newUUID() (string, error) {
uuid := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, uuid); err != nil {
return "", err
}
// variant bits; see section 4.1.1
uuid[8] = uuid[8]&^0xc0 | 0x80
// version 4 (pseudo-random); see section 4.1.3
uuid[6] = uuid[6]&^0xf0 | 0x40
return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil
}

24
vendor/vendor.json vendored
View File

@ -1362,6 +1362,12 @@
"revision": "1909bc2f63dc92bb931deace8b8312c4db72d12f",
"revisionTime": "2017-08-08T02:16:21Z"
},
{
"checksumSHA1": "p3IB18uJRs4dL2K5yx24MrLYE9A=",
"path": "github.com/google/go-querystring/query",
"revision": "53e6ce116135b80d037921a7fdd5138cf32d7a8a",
"revisionTime": "2017-01-11T10:11:55Z"
},
{
"checksumSHA1": "V/53BpqgOkSDZCX6snQCAkdO2fM=",
"path": "github.com/googleapis/gax-go",
@ -1791,6 +1797,18 @@
"revision": "b1a1dbde6fdc11e3ae79efd9039009e22d4ae240",
"revisionTime": "2018-03-26T21:11:50Z"
},
{
"checksumSHA1": "CdPSnO0sFf6G9CtXBzLEI9Pcmsw=",
"path": "github.com/hashicorp/go-slug",
"revision": "cc9d70694ef317c2e6443e20b7927363d69c8b1e",
"revisionTime": "2018-07-12T07:51:27Z"
},
{
"checksumSHA1": "926/ijhO8KTdgb02B/++x3w+Ykc=",
"path": "github.com/hashicorp/go-tfe",
"revision": "0e7cd8ef626181232db2d6886263a3db937708cd",
"revisionTime": "2018-08-01T08:24:33Z"
},
{
"checksumSHA1": "85XUnluYJL7F55ptcwdmN8eSOsk=",
"path": "github.com/hashicorp/go-uuid",
@ -2337,6 +2355,12 @@
"revision": "bb8f1927f2a9d3ab41c9340aa034f6b803f4359c",
"revisionTime": "2018-01-15T19:27:20Z"
},
{
"checksumSHA1": "byoZ+QptbiPnkoOXvGZulTFS9/M=",
"path": "github.com/svanharmelen/jsonapi",
"revision": "0c0828c3f16d3732cc7edecf49934d95550894e7",
"revisionTime": "2018-06-18T11:39:44Z"
},
{
"checksumSHA1": "GwjOkJpLvKznbv5zS2hFbg9fI4g=",
"path": "github.com/terraform-providers/terraform-provider-aws",

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