From 064cb0d0b0daa7d71d68ec8595dd1ff008457ccd Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Fri, 3 Aug 2018 11:05:43 +0200 Subject: [PATCH 1/5] govendor: add dependencies for the remote backend --- .../github.com/google/go-querystring/LICENSE | 27 + .../google/go-querystring/query/encode.go | 320 +++++++++ vendor/github.com/hashicorp/go-slug/LICENSE | 373 ++++++++++ vendor/github.com/hashicorp/go-slug/README.md | 70 ++ vendor/github.com/hashicorp/go-slug/slug.go | 215 ++++++ vendor/github.com/hashicorp/go-tfe/LICENSE | 354 +++++++++ vendor/github.com/hashicorp/go-tfe/README.md | 131 ++++ .../hashicorp/go-tfe/configuration_version.go | 193 +++++ .../hashicorp/go-tfe/oauth_client.go | 127 ++++ .../hashicorp/go-tfe/oauth_token.go | 61 ++ .../hashicorp/go-tfe/organization.go | 238 ++++++ .../hashicorp/go-tfe/organization_token.go | 75 ++ vendor/github.com/hashicorp/go-tfe/plan.go | 206 ++++++ vendor/github.com/hashicorp/go-tfe/policy.go | 251 +++++++ .../hashicorp/go-tfe/policy_check.go | 143 ++++ vendor/github.com/hashicorp/go-tfe/run.go | 273 +++++++ vendor/github.com/hashicorp/go-tfe/ssh_key.go | 192 +++++ .../hashicorp/go-tfe/state_version.go | 207 ++++++ vendor/github.com/hashicorp/go-tfe/team.go | 159 ++++ .../hashicorp/go-tfe/team_access.go | 178 +++++ .../hashicorp/go-tfe/team_member.go | 109 +++ .../github.com/hashicorp/go-tfe/team_token.go | 75 ++ vendor/github.com/hashicorp/go-tfe/tfe.go | 349 +++++++++ .../hashicorp/go-tfe/type_helpers.go | 41 ++ vendor/github.com/hashicorp/go-tfe/user.go | 93 +++ .../hashicorp/go-tfe/validations.go | 19 + .../github.com/hashicorp/go-tfe/variable.go | 216 ++++++ .../github.com/hashicorp/go-tfe/workspace.go | 437 +++++++++++ .../github.com/svanharmelen/jsonapi/LICENSE | 21 + .../github.com/svanharmelen/jsonapi/README.md | 457 ++++++++++++ .../svanharmelen/jsonapi/constants.go | 55 ++ vendor/github.com/svanharmelen/jsonapi/doc.go | 70 ++ .../github.com/svanharmelen/jsonapi/errors.go | 55 ++ .../github.com/svanharmelen/jsonapi/node.go | 121 ++++ .../svanharmelen/jsonapi/request.go | 680 ++++++++++++++++++ .../svanharmelen/jsonapi/response.go | 539 ++++++++++++++ .../svanharmelen/jsonapi/runtime.go | 103 +++ vendor/vendor.json | 24 + 38 files changed, 7257 insertions(+) create mode 100644 vendor/github.com/google/go-querystring/LICENSE create mode 100644 vendor/github.com/google/go-querystring/query/encode.go create mode 100644 vendor/github.com/hashicorp/go-slug/LICENSE create mode 100644 vendor/github.com/hashicorp/go-slug/README.md create mode 100644 vendor/github.com/hashicorp/go-slug/slug.go create mode 100644 vendor/github.com/hashicorp/go-tfe/LICENSE create mode 100644 vendor/github.com/hashicorp/go-tfe/README.md create mode 100644 vendor/github.com/hashicorp/go-tfe/configuration_version.go create mode 100644 vendor/github.com/hashicorp/go-tfe/oauth_client.go create mode 100644 vendor/github.com/hashicorp/go-tfe/oauth_token.go create mode 100644 vendor/github.com/hashicorp/go-tfe/organization.go create mode 100644 vendor/github.com/hashicorp/go-tfe/organization_token.go create mode 100644 vendor/github.com/hashicorp/go-tfe/plan.go create mode 100644 vendor/github.com/hashicorp/go-tfe/policy.go create mode 100644 vendor/github.com/hashicorp/go-tfe/policy_check.go create mode 100644 vendor/github.com/hashicorp/go-tfe/run.go create mode 100644 vendor/github.com/hashicorp/go-tfe/ssh_key.go create mode 100644 vendor/github.com/hashicorp/go-tfe/state_version.go create mode 100644 vendor/github.com/hashicorp/go-tfe/team.go create mode 100644 vendor/github.com/hashicorp/go-tfe/team_access.go create mode 100644 vendor/github.com/hashicorp/go-tfe/team_member.go create mode 100644 vendor/github.com/hashicorp/go-tfe/team_token.go create mode 100644 vendor/github.com/hashicorp/go-tfe/tfe.go create mode 100644 vendor/github.com/hashicorp/go-tfe/type_helpers.go create mode 100644 vendor/github.com/hashicorp/go-tfe/user.go create mode 100644 vendor/github.com/hashicorp/go-tfe/validations.go create mode 100644 vendor/github.com/hashicorp/go-tfe/variable.go create mode 100644 vendor/github.com/hashicorp/go-tfe/workspace.go create mode 100644 vendor/github.com/svanharmelen/jsonapi/LICENSE create mode 100644 vendor/github.com/svanharmelen/jsonapi/README.md create mode 100644 vendor/github.com/svanharmelen/jsonapi/constants.go create mode 100644 vendor/github.com/svanharmelen/jsonapi/doc.go create mode 100644 vendor/github.com/svanharmelen/jsonapi/errors.go create mode 100644 vendor/github.com/svanharmelen/jsonapi/node.go create mode 100644 vendor/github.com/svanharmelen/jsonapi/request.go create mode 100644 vendor/github.com/svanharmelen/jsonapi/response.go create mode 100644 vendor/github.com/svanharmelen/jsonapi/runtime.go diff --git a/vendor/github.com/google/go-querystring/LICENSE b/vendor/github.com/google/go-querystring/LICENSE new file mode 100644 index 0000000000..ae121a1e46 --- /dev/null +++ b/vendor/github.com/google/go-querystring/LICENSE @@ -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. diff --git a/vendor/github.com/google/go-querystring/query/encode.go b/vendor/github.com/google/go-querystring/query/encode.go new file mode 100644 index 0000000000..37080b19b5 --- /dev/null +++ b/vendor/github.com/google/go-querystring/query/encode.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-slug/LICENSE b/vendor/github.com/hashicorp/go-slug/LICENSE new file mode 100644 index 0000000000..a612ad9813 --- /dev/null +++ b/vendor/github.com/hashicorp/go-slug/LICENSE @@ -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. diff --git a/vendor/github.com/hashicorp/go-slug/README.md b/vendor/github.com/hashicorp/go-slug/README.md new file mode 100644 index 0000000000..978314f1b1 --- /dev/null +++ b/vendor/github.com/hashicorp/go-slug/README.md @@ -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. diff --git a/vendor/github.com/hashicorp/go-slug/slug.go b/vendor/github.com/hashicorp/go-slug/slug.go new file mode 100644 index 0000000000..b7a62c9630 --- /dev/null +++ b/vendor/github.com/hashicorp/go-slug/slug.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/LICENSE b/vendor/github.com/hashicorp/go-tfe/LICENSE new file mode 100644 index 0000000000..c33dcc7c92 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/LICENSE @@ -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 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. + diff --git a/vendor/github.com/hashicorp/go-tfe/README.md b/vendor/github.com/hashicorp/go-tfe/README.md new file mode 100644 index 0000000000..05bbc78d8a --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/README.md @@ -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. diff --git a/vendor/github.com/hashicorp/go-tfe/configuration_version.go b/vendor/github.com/hashicorp/go-tfe/configuration_version.go new file mode 100644 index 0000000000..97d9347194 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/configuration_version.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/oauth_client.go b/vendor/github.com/hashicorp/go-tfe/oauth_client.go new file mode 100644 index 0000000000..146bf7f9be --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/oauth_client.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/oauth_token.go b/vendor/github.com/hashicorp/go-tfe/oauth_token.go new file mode 100644 index 0000000000..c247338cb7 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/oauth_token.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/organization.go b/vendor/github.com/hashicorp/go-tfe/organization.go new file mode 100644 index 0000000000..69b6f2441c --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/organization.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/organization_token.go b/vendor/github.com/hashicorp/go-tfe/organization_token.go new file mode 100644 index 0000000000..8d32b01cac --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/organization_token.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/plan.go b/vendor/github.com/hashicorp/go-tfe/plan.go new file mode 100644 index 0000000000..f50be213f4 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/plan.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/policy.go b/vendor/github.com/hashicorp/go-tfe/policy.go new file mode 100644 index 0000000000..70a96b8486 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/policy.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/policy_check.go b/vendor/github.com/hashicorp/go-tfe/policy_check.go new file mode 100644 index 0000000000..ef01a18729 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/policy_check.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/run.go b/vendor/github.com/hashicorp/go-tfe/run.go new file mode 100644 index 0000000000..c5a4ab9f89 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/run.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/ssh_key.go b/vendor/github.com/hashicorp/go-tfe/ssh_key.go new file mode 100644 index 0000000000..7f8c930329 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/ssh_key.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/state_version.go b/vendor/github.com/hashicorp/go-tfe/state_version.go new file mode 100644 index 0000000000..de6e4c5370 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/state_version.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/team.go b/vendor/github.com/hashicorp/go-tfe/team.go new file mode 100644 index 0000000000..8f82e7cb01 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/team.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/team_access.go b/vendor/github.com/hashicorp/go-tfe/team_access.go new file mode 100644 index 0000000000..9c3b4b835f --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/team_access.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/team_member.go b/vendor/github.com/hashicorp/go-tfe/team_member.go new file mode 100644 index 0000000000..5182e5b8fa --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/team_member.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/team_token.go b/vendor/github.com/hashicorp/go-tfe/team_token.go new file mode 100644 index 0000000000..105dd72953 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/team_token.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/tfe.go b/vendor/github.com/hashicorp/go-tfe/tfe.go new file mode 100644 index 0000000000..b783aefd50 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/tfe.go @@ -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")) +} diff --git a/vendor/github.com/hashicorp/go-tfe/type_helpers.go b/vendor/github.com/hashicorp/go-tfe/type_helpers.go new file mode 100644 index 0000000000..3b2a95d42c --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/type_helpers.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/user.go b/vendor/github.com/hashicorp/go-tfe/user.go new file mode 100644 index 0000000000..f0ca28ee39 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/user.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/go-tfe/validations.go b/vendor/github.com/hashicorp/go-tfe/validations.go new file mode 100644 index 0000000000..38d95a681e --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/validations.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/variable.go b/vendor/github.com/hashicorp/go-tfe/variable.go new file mode 100644 index 0000000000..ff7101f1a2 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/variable.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/go-tfe/workspace.go b/vendor/github.com/hashicorp/go-tfe/workspace.go new file mode 100644 index 0000000000..923e81d66c --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/workspace.go @@ -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 +} diff --git a/vendor/github.com/svanharmelen/jsonapi/LICENSE b/vendor/github.com/svanharmelen/jsonapi/LICENSE new file mode 100644 index 0000000000..c97912cef0 --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/LICENSE @@ -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. diff --git a/vendor/github.com/svanharmelen/jsonapi/README.md b/vendor/github.com/svanharmelen/jsonapi/README.md new file mode 100644 index 0000000000..44b0541815 --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/README.md @@ -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,"` +``` + +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,,"` +``` + +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,,"` +``` + +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*. diff --git a/vendor/github.com/svanharmelen/jsonapi/constants.go b/vendor/github.com/svanharmelen/jsonapi/constants.go new file mode 100644 index 0000000000..23288d311e --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/constants.go @@ -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]" +) diff --git a/vendor/github.com/svanharmelen/jsonapi/doc.go b/vendor/github.com/svanharmelen/jsonapi/doc.go new file mode 100644 index 0000000000..29d7a14ba7 --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/doc.go @@ -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," + +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,[,]" + +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," + +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 diff --git a/vendor/github.com/svanharmelen/jsonapi/errors.go b/vendor/github.com/svanharmelen/jsonapi/errors.go new file mode 100644 index 0000000000..ed7fa9f75d --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/errors.go @@ -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 field’s 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) +} diff --git a/vendor/github.com/svanharmelen/jsonapi/node.go b/vendor/github.com/svanharmelen/jsonapi/node.go new file mode 100644 index 0000000000..a58488c822 --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/node.go @@ -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 link’s URL. + // - an object (“link object”) which can contain the following members: + // - href: a string containing the link’s 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 +} diff --git a/vendor/github.com/svanharmelen/jsonapi/request.go b/vendor/github.com/svanharmelen/jsonapi/request.go new file mode 100644 index 0000000000..e3543428a5 --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/request.go @@ -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 +} diff --git a/vendor/github.com/svanharmelen/jsonapi/response.go b/vendor/github.com/svanharmelen/jsonapi/response.go new file mode 100644 index 0000000000..e8e85fa42f --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/response.go @@ -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 +} diff --git a/vendor/github.com/svanharmelen/jsonapi/runtime.go b/vendor/github.com/svanharmelen/jsonapi/runtime.go new file mode 100644 index 0000000000..7dc658155b --- /dev/null +++ b/vendor/github.com/svanharmelen/jsonapi/runtime.go @@ -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 +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 081d685ba5..40d3646b74 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -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", From 97d1c466025bf2692106d3d96f78ff90f85faa6e Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Wed, 4 Jul 2018 12:07:44 +0200 Subject: [PATCH 2/5] Update the backend import names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s a purely cosmetic change, but I find it easier to read them like this. --- backend/cli.go | 2 +- backend/init/init.go | 30 +++++++------- .../providers/terraform/data_source_state.go | 4 +- .../terraform/data_source_state_test.go | 6 +-- command/meta_backend.go | 28 ++++++------- command/meta_backend_test.go | 40 +++++++++---------- command/state_meta.go | 4 +- 7 files changed, 57 insertions(+), 57 deletions(-) diff --git a/backend/cli.go b/backend/cli.go index 40a66e6985..b76b6e1116 100644 --- a/backend/cli.go +++ b/backend/cli.go @@ -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. // diff --git a/backend/init/init.go b/backend/init/init.go index d499968c74..aeadb779fc 100644 --- a/backend/init/init.go +++ b/backend/init/init.go @@ -8,14 +8,14 @@ import ( "github.com/hashicorp/terraform/backend" "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" 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" @@ -39,23 +39,23 @@ 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() }, - "s3": func() backend.Backend { return backendS3.New() }, + "local": func() backend.Backend { return &backendLocal.Local{} }, + "atlas": func() backend.Backend { return &backendAtlas.Backend{} }, "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() }, + "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() }, } - // 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 diff --git a/builtin/providers/terraform/data_source_state.go b/builtin/providers/terraform/data_source_state.go index 1eefe9e8da..e7bf54f289 100644 --- a/builtin/providers/terraform/data_source_state.go +++ b/builtin/providers/terraform/data_source_state.go @@ -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) } diff --git a/builtin/providers/terraform/data_source_state_test.go b/builtin/providers/terraform/data_source_state_test.go index 9b2040e823..edca009229 100644 --- a/builtin/providers/terraform/data_source_state_test.go +++ b/builtin/providers/terraform/data_source_state_test.go @@ -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) }, diff --git a/command/meta_backend.go b/command/meta_backend.go index f5cb034e7e..73d346950d 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -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(), @@ -106,7 +106,7 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, error) { RunningInAutomation: m.RunningInAutomation, } - // Don't validate if we have a plan. Validation is normally harmless here, + // Don't validate if we have a plan. Validation is normally harmless here, // but validation requires interpolation, and `file()` function calls may // not have the original files in the current execution context. cliOpts.Validation = opts.Plan == nil @@ -136,7 +136,7 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, error) { } // Build the local backend - local := &backendlocal.Local{Backend: b} + local := &backendLocal.Local{Backend: 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) } diff --git a/command/meta_backend_test.go b/command/meta_backend_test.go index 8f059693ce..e3e735fdd7 100644 --- a/command/meta_backend_test.go +++ b/command/meta_backend_test.go @@ -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,7 @@ 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") } @@ -3351,7 +3351,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 +3373,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 +3442,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 +3463,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") diff --git a/command/state_meta.go b/command/state_meta.go index aa79e9d47e..a1648358bd 100644 --- a/command/state_meta.go +++ b/command/state_meta.go @@ -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 From 495d1ea350f20bf442a77bc649c0b1dc3f483c7b Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Wed, 4 Jul 2018 12:11:35 +0200 Subject: [PATCH 3/5] Use New() instead of `once.Do(b.init)` --- backend/atlas/backend.go | 98 +++++---- backend/atlas/backend_test.go | 4 +- backend/atlas/state_client_test.go | 2 +- backend/init/init.go | 4 +- backend/local/backend.go | 249 +++++++++++----------- backend/local/backend_local.go | 8 +- backend/local/backend_test.go | 24 +-- backend/local/testing.go | 19 +- backend/remote-state/gcs/backend.go | 14 +- backend/remote-state/gcs/backend_state.go | 12 +- backend/remote-state/gcs/backend_test.go | 6 +- command/init.go | 6 +- command/meta_backend.go | 2 +- 13 files changed, 223 insertions(+), 225 deletions(-) diff --git a/backend/atlas/backend.go b/backend/atlas/backend.go index 660327ae02..655de23c9d 100644 --- a/backend/atlas/backend.go +++ b/backend/atlas/backend.go @@ -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" + diff --git a/backend/atlas/backend_test.go b/backend/atlas/backend_test.go index 313a528d27..286d3de8d8 100644 --- a/backend/atlas/backend_test.go +++ b/backend/atlas/backend_test.go @@ -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", }))) diff --git a/backend/atlas/state_client_test.go b/backend/atlas/state_client_test.go index 2fe85559d5..5135bfd7d7 100644 --- a/backend/atlas/state_client_test.go +++ b/backend/atlas/state_client_test.go @@ -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) diff --git a/backend/init/init.go b/backend/init/init.go index aeadb779fc..056827905f 100644 --- a/backend/init/init.go +++ b/backend/init/init.go @@ -39,8 +39,8 @@ 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{ - "local": func() backend.Backend { return &backendLocal.Local{} }, - "atlas": func() backend.Backend { return &backendAtlas.Backend{} }, + "local": func() backend.Backend { return backendLocal.New() }, + "atlas": func() backend.Backend { return backendAtlas.New() }, "azure": deprecateBackend(backendAzure.New(), `Warning: "azure" name is deprecated, please use "azurerm"`), "azurerm": func() backend.Backend { return backendAzure.New() }, diff --git a/backend/local/backend.go b/backend/local/backend.go index eb4eff4b00..abb4d37c9f 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -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) { diff --git a/backend/local/backend_local.go b/backend/local/backend_local.go index d26fefa57d..4c810f50e6 100644 --- a/backend/local/backend_local.go +++ b/backend/local/backend_local.go @@ -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. diff --git a/backend/local/backend_test.go b/backend/local/backend_test.go index 3852aae2ff..7162f7a6dc 100644 --- a/backend/local/backend_test.go +++ b/backend/local/backend_test.go @@ -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{ - stateErr: true, - statesErr: true, - deleteErr: true, - }, - } + b := NewWithBackend(&testDelegateBackend{ + stateErr: true, + statesErr: true, + deleteErr: true, + }) if _, err := b.State("test"); err != errTestDelegateState { t.Fatal("expected errTestDelegateState, got:", err) diff --git a/backend/local/testing.go b/backend/local/testing.go index 4d480f6d7b..bd07fc8869 100644 --- a/backend/local/testing.go +++ b/backend/local/testing.go @@ -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) { diff --git a/backend/remote-state/gcs/backend.go b/backend/remote-state/gcs/backend.go index fc51092642..26b430b41c 100644 --- a/backend/remote-state/gcs/backend.go +++ b/backend/remote-state/gcs/backend.go @@ -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 } diff --git a/backend/remote-state/gcs/backend_state.go b/backend/remote-state/gcs/backend_state.go index 61e3e3f25d..bc75465b56 100644 --- a/backend/remote-state/gcs/backend_state.go +++ b/backend/remote-state/gcs/backend_state.go @@ -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 } diff --git a/backend/remote-state/gcs/backend_test.go b/backend/remote-state/gcs/backend_test.go index b561b1f230..61d27da8cd 100644 --- a/backend/remote-state/gcs/backend_test.go +++ b/backend/remote-state/gcs/backend_test.go @@ -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) } diff --git a/command/init.go b/command/init.go index 86df82ee7b..efa4b5724d 100644 --- a/command/init.go +++ b/command/init.go @@ -138,11 +138,13 @@ 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 { + 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 } diff --git a/command/meta_backend.go b/command/meta_backend.go index 73d346950d..185e8b6da0 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -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) From 179b32d426025f9cb94f57488634ee4e3d9699a3 Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Thu, 5 Jul 2018 21:28:29 +0200 Subject: [PATCH 4/5] Add a `CredentialsForHost` method to disco.Disco By adding this method you now only have to pass a `*disco.Disco` object around in order to do discovery and use any configured credentials for the discovered hosts. Of course you can also still pass around both a `*disco.Disco` and a `auth.CredentialsSource` object if there is a need or a reason for that! --- command/command_test.go | 2 +- command/init.go | 2 +- command/meta.go | 7 +----- commands.go | 8 ++----- config/module/module_test.go | 2 +- config/module/storage.go | 13 ++++------- config/module/storage_test.go | 4 ++-- configs/configload/loader.go | 8 +------ configs/configload/module_mgr.go | 4 ---- main.go | 5 ++++- registry/client.go | 18 +++------------ registry/client_test.go | 37 ++++++++++++++++--------------- registry/test/mock_registry.go | 6 ++--- svchost/auth/credentials.go | 3 +++ svchost/auth/token_credentials.go | 5 +++++ svchost/disco/disco.go | 34 ++++++++++++++++++---------- svchost/disco/disco_test.go | 18 +++++++-------- 17 files changed, 81 insertions(+), 95 deletions(-) diff --git a/command/command_test.go b/command/command_test.go index 12d48761b7..c0a8529c6e 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -117,7 +117,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) diff --git a/command/init.go b/command/init.go index efa4b5724d..b96cdc6eca 100644 --- a/command/init.go +++ b/command/init.go @@ -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 diff --git a/command/meta.go b/command/meta.go index 91f1008fed..f154f2d6c8 100644 --- a/command/meta.go +++ b/command/meta.go @@ -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 diff --git a/commands.go b/commands.go index 3335d2cdb5..113c771eb7 100644 --- a/commands.go +++ b/commands.go @@ -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 { @@ -57,8 +54,7 @@ func initCommands(config *Config) { PluginOverrides: &PluginOverrides, Ui: Ui, - Services: services, - Credentials: credsSrc, + Services: services, RunningInAutomation: inAutomation, PluginCacheDir: config.PluginCacheDir, diff --git a/config/module/module_test.go b/config/module/module_test.go index 62e7ed2a75..80e931e0b5 100644 --- a/config/module/module_test.go +++ b/config/module/module_test.go @@ -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) } diff --git a/config/module/storage.go b/config/module/storage.go index fa5e1c621c..4b828dcb08 100644 --- a/config/module/storage.go +++ b/config/module/storage.go @@ -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, diff --git a/config/module/storage_test.go b/config/module/storage_test.go index 10811190e3..cb41f6d65b 100644 --- a/config/module/storage_test.go +++ b/config/module/storage_test.go @@ -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) diff --git a/configs/configload/loader.go b/configs/configload/loader.go index 06ff27400c..45e60f77ce 100644 --- a/configs/configload/loader.go +++ b/configs/configload/loader.go @@ -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, }, } diff --git a/configs/configload/module_mgr.go b/configs/configload/module_mgr.go index ef17fda7a7..6b2a5199fb 100644 --- a/configs/configload/module_mgr.go +++ b/configs/configload/module_mgr.go @@ -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 diff --git a/main.go b/main.go index 1818a91c44..523863e7b8 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/go-plugin" "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" @@ -144,7 +145,9 @@ func wrappedMain() int { // In tests, Commands may already be set to provide mock commands if Commands == nil { - initCommands(config) + credsSrc := credentialsSource(config) + services := disco.NewWithCredentialsSource(credsSrc) + initCommands(config, services) } // Run checkpoint diff --git a/registry/client.go b/registry/client.go index fba59ec873..8e31a6a3e2 100644 --- a/registry/client.go +++ b/registry/client.go @@ -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 diff --git a/registry/client_test.go b/registry/client_test.go index 279c5a4831..5ee712f7f2 100644 --- a/registry/client_test.go +++ b/registry/client_test.go @@ -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" diff --git a/registry/test/mock_registry.go b/registry/test/mock_registry.go index c1fabbc25b..bd3d80b7f0 100644 --- a/registry/test/mock_registry.go +++ b/registry/test/mock_registry.go @@ -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) @@ -48,8 +48,8 @@ const ( ) var ( - regHost = svchost.Hostname(regsrc.PublicRegistryHost.Normalized()) - Credentials = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ + regHost = svchost.Hostname(regsrc.PublicRegistryHost.Normalized()) + credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ regHost: {"token": testCred}, }) ) diff --git a/svchost/auth/credentials.go b/svchost/auth/credentials.go index 0bc6db4f10..0372c16096 100644 --- a/svchost/auth/credentials.go +++ b/svchost/auth/credentials.go @@ -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 diff --git a/svchost/auth/token_credentials.go b/svchost/auth/token_credentials.go index 8f771b0d9b..9358bcb644 100644 --- a/svchost/auth/token_credentials.go +++ b/svchost/auth/token_credentials.go @@ -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) +} diff --git a/svchost/disco/disco.go b/svchost/disco/disco.go index 76a1b3b0d5..7fc49da9cb 100644 --- a/svchost/disco/disco.go +++ b/svchost/disco/disco.go @@ -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 { - log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err) - } + 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) diff --git a/svchost/disco/disco_test.go b/svchost/disco/disco_test.go index 94d2a220f5..c8bc16c455 100644 --- a/svchost/disco/disco_test.go +++ b/svchost/disco/disco_test.go @@ -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") From 7fb2d1b8de6cc0bf6b815910b7410f7d7307a02d Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Wed, 4 Jul 2018 17:24:49 +0200 Subject: [PATCH 5/5] Implement the Enterprise enhanced remote backend --- backend/backend.go | 23 +- backend/init/init.go | 39 +- backend/init/init_test.go | 110 +++++ backend/legacy/legacy.go | 4 +- backend/legacy/legacy_test.go | 4 +- backend/local/testing.go | 44 ++ backend/remote/backend.go | 453 ++++++++++++++++++ backend/remote/backend_mock.go | 384 +++++++++++++++ backend/remote/backend_plan.go | 206 ++++++++ backend/remote/backend_plan_test.go | 181 +++++++ backend/remote/backend_state.go | 103 ++++ backend/remote/backend_state_test.go | 16 + backend/remote/backend_test.go | 254 ++++++++++ backend/remote/cli.go | 13 + .../test-fixtures/plan-scaleout/main.tf | 10 + backend/remote/test-fixtures/plan/main.tf | 1 + backend/remote/test-fixtures/plan/output.log | 29 ++ backend/remote/testing.go | 128 +++++ backend/testing.go | 28 +- builtin/providers/terraform/provider_test.go | 4 + command/command.go | 4 - command/command_test.go | 4 + command/init.go | 9 +- command/meta_backend_migrate.go | 14 +- command/meta_backend_test.go | 106 ++++ .../.terraform/terraform.tfstate | 22 + .../local-state.tfstate | 6 + .../main.tf | 5 + .../env2/terraform.tfstate | 6 + .../.terraform/terraform.tfstate | 22 + .../main.tf | 5 + .../env2/terraform.tfstate | 6 + main.go | 14 +- state/remote/testing.go | 4 +- website/docs/backends/types/remote.html.md | 118 +++++ .../types/terraform-enterprise.html.md | 3 + website/layouts/backend-types.erb | 3 + 37 files changed, 2342 insertions(+), 43 deletions(-) create mode 100644 backend/init/init_test.go create mode 100644 backend/remote/backend.go create mode 100644 backend/remote/backend_mock.go create mode 100644 backend/remote/backend_plan.go create mode 100644 backend/remote/backend_plan_test.go create mode 100644 backend/remote/backend_state.go create mode 100644 backend/remote/backend_state_test.go create mode 100644 backend/remote/backend_test.go create mode 100644 backend/remote/cli.go create mode 100644 backend/remote/test-fixtures/plan-scaleout/main.tf create mode 100644 backend/remote/test-fixtures/plan/main.tf create mode 100644 backend/remote/test-fixtures/plan/output.log create mode 100644 backend/remote/testing.go create mode 100644 command/test-fixtures/backend-change-multi-to-no-default-with-default/.terraform/terraform.tfstate create mode 100644 command/test-fixtures/backend-change-multi-to-no-default-with-default/local-state.tfstate create mode 100644 command/test-fixtures/backend-change-multi-to-no-default-with-default/main.tf create mode 100644 command/test-fixtures/backend-change-multi-to-no-default-with-default/terraform.tfstate.d/env2/terraform.tfstate create mode 100644 command/test-fixtures/backend-change-multi-to-no-default-without-default/.terraform/terraform.tfstate create mode 100644 command/test-fixtures/backend-change-multi-to-no-default-without-default/main.tf create mode 100644 command/test-fixtures/backend-change-multi-to-no-default-without-default/terraform.tfstate.d/env2/terraform.tfstate create mode 100644 website/docs/backends/types/remote.html.md diff --git a/backend/backend.go b/backend/backend.go index dfeb80ef6f..f10c27c9f5 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -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 { diff --git a/backend/init/init.go b/backend/init/init.go index 056827905f..8fcd249c42 100644 --- a/backend/init/init.go +++ b/backend/init/init.go @@ -3,14 +3,17 @@ 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" + 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" @@ -32,17 +35,27 @@ 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{ +// 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() }, - "atlas": func() backend.Backend { return backendAtlas.New() }, - "azure": deprecateBackend(backendAzure.New(), - `Warning: "azure" name is deprecated, please use "azurerm"`), + "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() }, @@ -51,6 +64,10 @@ func init() { "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"`), } // Add the legacy remote backends that haven't yet been converted to @@ -60,7 +77,7 @@ func init() { // 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 diff --git a/backend/init/init_test.go b/backend/init/init_test.go new file mode 100644 index 0000000000..150b4c101e --- /dev/null +++ b/backend/init/init_test.go @@ -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) + } + } +} diff --git a/backend/legacy/legacy.go b/backend/legacy/legacy.go index be3163bd53..6ed3e41d33 100644 --- a/backend/legacy/legacy.go +++ b/backend/legacy/legacy.go @@ -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). diff --git a/backend/legacy/legacy_test.go b/backend/legacy/legacy_test.go index 77d81bf1ae..8a13c25776 100644 --- a/backend/legacy/legacy_test.go +++ b/backend/legacy/legacy_test.go @@ -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) diff --git a/backend/local/testing.go b/backend/local/testing.go index bd07fc8869..6e8711860a 100644 --- a/backend/local/testing.go +++ b/backend/local/testing.go @@ -99,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 { diff --git a/backend/remote/backend.go b/backend/remote/backend.go new file mode 100644 index 0000000000..30a990915a --- /dev/null +++ b/backend/remote/backend.go @@ -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\"", +} diff --git a/backend/remote/backend_mock.go b/backend/remote/backend_mock.go new file mode 100644 index 0000000000..9aea17168a --- /dev/null +++ b/backend/remote/backend_mock.go @@ -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) +} diff --git a/backend/remote/backend_plan.go b/backend/remote/backend_plan.go new file mode 100644 index 0000000000..3928301255 --- /dev/null +++ b/backend/remote/backend_plan.go @@ -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... +` diff --git a/backend/remote/backend_plan_test.go b/backend/remote/backend_plan_test.go new file mode 100644 index 0000000000..cf34057294 --- /dev/null +++ b/backend/remote/backend_plan_test.go @@ -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) + } +} diff --git a/backend/remote/backend_state.go b/backend/remote/backend_state.go new file mode 100644 index 0000000000..135d48a6db --- /dev/null +++ b/backend/remote/backend_state.go @@ -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 +} diff --git a/backend/remote/backend_state_test.go b/backend/remote/backend_state_test.go new file mode 100644 index 0000000000..d3c4478a0b --- /dev/null +++ b/backend/remote/backend_state_test.go @@ -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) +} diff --git a/backend/remote/backend_test.go b/backend/remote/backend_test.go new file mode 100644 index 0000000000..895a7fd55f --- /dev/null +++ b/backend/remote/backend_test.go @@ -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) + } +} diff --git a/backend/remote/cli.go b/backend/remote/cli.go new file mode 100644 index 0000000000..9339c10912 --- /dev/null +++ b/backend/remote/cli.go @@ -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 +} diff --git a/backend/remote/test-fixtures/plan-scaleout/main.tf b/backend/remote/test-fixtures/plan-scaleout/main.tf new file mode 100644 index 0000000000..4067af592d --- /dev/null +++ b/backend/remote/test-fixtures/plan-scaleout/main.tf @@ -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" + } +} diff --git a/backend/remote/test-fixtures/plan/main.tf b/backend/remote/test-fixtures/plan/main.tf new file mode 100644 index 0000000000..3911a2a9b2 --- /dev/null +++ b/backend/remote/test-fixtures/plan/main.tf @@ -0,0 +1 @@ +resource "null_resource" "foo" {} diff --git a/backend/remote/test-fixtures/plan/output.log b/backend/remote/test-fixtures/plan/output.log new file mode 100644 index 0000000000..d9fe98082d --- /dev/null +++ b/backend/remote/test-fixtures/plan/output.log @@ -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: + + +Plan: 1 to add, 0 to change, 0 to destroy. diff --git a/backend/remote/testing.go b/backend/remote/testing.go new file mode 100644 index 0000000000..253272f76f --- /dev/null +++ b/backend/remote/testing.go @@ -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 +} diff --git a/backend/testing.go b/backend/testing.go index e509f82c2c..22dc99791d 100644 --- a/backend/testing.go +++ b/backend/testing.go @@ -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 == ErrNamedStatesNotSupported { - t.Logf("TestBackend: named states not supported in %T, skipping", b) - return + 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) } diff --git a/builtin/providers/terraform/provider_test.go b/builtin/providers/terraform/provider_test.go index 65f3ce4adb..0ba389b5c8 100644 --- a/builtin/providers/terraform/provider_test.go +++ b/builtin/providers/terraform/provider_test.go @@ -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, diff --git a/command/command.go b/command/command.go index 0cd11da087..815a6fa6de 100644 --- a/command/command.go +++ b/command/command.go @@ -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. diff --git a/command/command_test.go b/command/command_test.go index c0a8529c6e..79330f42bc 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -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 diff --git a/command/init.go b/command/init.go index b96cdc6eca..c831566d9e 100644 --- a/command/init.go +++ b/command/init.go @@ -140,8 +140,7 @@ func (c *InitCommand) Run(args []string) int { // the backend with an empty directory. empty, err := config.IsEmptyDir(path) if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error checking configuration: %s", err)) + c.Ui.Error(fmt.Sprintf("Error checking configuration: %s", err)) return 1 } if empty { @@ -229,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 } diff --git a/command/meta_backend_migrate.go b/command/meta_backend_migrate.go index 0c3610a8a1..dc8d503b2a 100644 --- a/command/meta_backend_migrate.go +++ b/command/meta_backend_migrate.go @@ -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 diff --git a/command/meta_backend_test.go b/command/meta_backend_test.go index e3e735fdd7..8e30593790 100644 --- a/command/meta_backend_test.go +++ b/command/meta_backend_test.go @@ -1422,6 +1422,112 @@ func TestMetaBackend_configuredChangeCopy_multiToMulti(t *testing.T) { } } +// 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") + } + } +} + // Unsetting a saved backend func TestMetaBackend_configuredUnset(t *testing.T) { // Create a temporary working directory that is empty diff --git a/command/test-fixtures/backend-change-multi-to-no-default-with-default/.terraform/terraform.tfstate b/command/test-fixtures/backend-change-multi-to-no-default-with-default/.terraform/terraform.tfstate new file mode 100644 index 0000000000..073bd7a822 --- /dev/null +++ b/command/test-fixtures/backend-change-multi-to-no-default-with-default/.terraform/terraform.tfstate @@ -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": [] + } + ] +} diff --git a/command/test-fixtures/backend-change-multi-to-no-default-with-default/local-state.tfstate b/command/test-fixtures/backend-change-multi-to-no-default-with-default/local-state.tfstate new file mode 100644 index 0000000000..88c1d86ec4 --- /dev/null +++ b/command/test-fixtures/backend-change-multi-to-no-default-with-default/local-state.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "backend-change" +} diff --git a/command/test-fixtures/backend-change-multi-to-no-default-with-default/main.tf b/command/test-fixtures/backend-change-multi-to-no-default-with-default/main.tf new file mode 100644 index 0000000000..93c5bced09 --- /dev/null +++ b/command/test-fixtures/backend-change-multi-to-no-default-with-default/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local-no-default" { + environment_dir = "envdir-new" + } +} diff --git a/command/test-fixtures/backend-change-multi-to-no-default-with-default/terraform.tfstate.d/env2/terraform.tfstate b/command/test-fixtures/backend-change-multi-to-no-default-with-default/terraform.tfstate.d/env2/terraform.tfstate new file mode 100644 index 0000000000..855a27f4cf --- /dev/null +++ b/command/test-fixtures/backend-change-multi-to-no-default-with-default/terraform.tfstate.d/env2/terraform.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "backend-change-env2" +} diff --git a/command/test-fixtures/backend-change-multi-to-no-default-without-default/.terraform/terraform.tfstate b/command/test-fixtures/backend-change-multi-to-no-default-without-default/.terraform/terraform.tfstate new file mode 100644 index 0000000000..073bd7a822 --- /dev/null +++ b/command/test-fixtures/backend-change-multi-to-no-default-without-default/.terraform/terraform.tfstate @@ -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": [] + } + ] +} diff --git a/command/test-fixtures/backend-change-multi-to-no-default-without-default/main.tf b/command/test-fixtures/backend-change-multi-to-no-default-without-default/main.tf new file mode 100644 index 0000000000..93c5bced09 --- /dev/null +++ b/command/test-fixtures/backend-change-multi-to-no-default-without-default/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local-no-default" { + environment_dir = "envdir-new" + } +} diff --git a/command/test-fixtures/backend-change-multi-to-no-default-without-default/terraform.tfstate.d/env2/terraform.tfstate b/command/test-fixtures/backend-change-multi-to-no-default-without-default/terraform.tfstate.d/env2/terraform.tfstate new file mode 100644 index 0000000000..855a27f4cf --- /dev/null +++ b/command/test-fixtures/backend-change-multi-to-no-default-without-default/terraform.tfstate.d/env2/terraform.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "backend-change-env2" +} diff --git a/main.go b/main.go index 523863e7b8..108f490358 100644 --- a/main.go +++ b/main.go @@ -11,9 +11,8 @@ 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" @@ -21,6 +20,7 @@ import ( "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" ) @@ -143,10 +143,16 @@ 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 { - credsSrc := credentialsSource(config) - services := disco.NewWithCredentialsSource(credsSrc) initCommands(config, services) } diff --git a/state/remote/testing.go b/state/remote/testing.go index b379b509df..bad22445e2 100644 --- a/state/remote/testing.go +++ b/state/remote/testing.go @@ -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)) } } diff --git a/website/docs/backends/types/remote.html.md b/website/docs/backends/types/remote.html.md new file mode 100644 index 0000000000..5429c2d622 --- /dev/null +++ b/website/docs/backends/types/remote.html.md @@ -0,0 +1,118 @@ +--- +layout: "backend-types" +page_title: "Backend Type: remote" +sidebar_current: "docs-backends-types-enhanced-remote" +description: |- + Terraform can store the state and run operations remotely, making it easier to version and work with in a team. +--- + +# remote + +**Kind: Enhanced** + +The remote backend stores state and runs operations remotely. In order +use this backend you need a Terraform Enterprise account or have Private +Terraform Enterprise running on-premises. + +### Commands + +Currently the remote backend supports the following Terraform commands: + + 1. fmt + 2. get + 3. init + 4. output + 5. plan + 6. providers + 7. show + 8. taint + 9. untaint + 10. validate + 11. version + 11. workspace + +### Workspaces +To work with remote workspaces we need either a name or a prefix. You will +get a configuration error when neither or both options are configured. + +#### Name +When a name is provided, that name is used to make a one-to-one mapping +between your local “default” workspace and a named remote workspace. This +option assumes you are not using workspaces when working with TF, so it +will act as a backend that does not support names states. + +#### Prefix +When a prefix is provided it will be used to filter and map workspaces that +can be used with a single configuration. This allows you to dynamically +filter and map all remote workspaces with a matching prefix. + +The prefix is added when making calls to the remote backend and stripped +again when receiving the responses. This way any locally used workspace +names will remain the same short names (e.g. “tst”, “acc”) while the remote +names will be mapped by adding the prefix. + +It is assumed that you are only using named workspaces when working with +Terraform and so the “default” workspace is ignored in this case. If there +is a state file for the “default” config, this will give an error during +`terraform init`. If the default workspace is selected when running the +`init` command, the `init` process will succeed but will end with a message +that tells you how to select an existing workspace or create a new one. + +## Example Configuration + +```hcl +terraform { + backend "remote" { + hostname = "app.terraform.io" + organization = "company" + token = "" + + workspaces { + name = "workspace" + prefix = "my-app-" + } + } +} +``` + +We recommend omitting the token which can be provided as an environment +variable or set as [credentials in the CLI Config File](/docs/commands/cli-config.html#credentials). + +## Example Reference + +```hcl +data "terraform_remote_state" "foo" { + backend = "remote" + + config { + organization = "company" + + workspaces { + name = "workspace" + } + } +} +``` + +## Configuration variables + +The following configuration options are supported: + +* `hostname` - (Optional) The remote backend hostname to connect to. Default + to app.terraform.io. +* `organization` - (Required) The name of the organization containing the + targeted workspace(s). +* `token` - (Optional) The token used to authenticate with the remote backend. + If `TFE_TOKEN` is set or credentials for the host are configured in the CLI + Config File, then this this will override any saved value for this. +* `workspaces` - (Required) Workspaces contains arguments used to filter down + to a set of workspaces to work on. Parameters defined below. + +The `workspaces` block supports the following keys: +* `name` - (Optional) A workspace name used to map the default workspace to a + named remote workspace. When configured only the default workspace can be + used. This option conflicts with `prefix`. +* `prefix` - (Optional) A prefix used to filter workspaces using a single + configuration. New workspaces will automatically be prefixed with this + prefix. If omitted only the default workspace can be used. This option + conflicts with `name`. diff --git a/website/docs/backends/types/terraform-enterprise.html.md b/website/docs/backends/types/terraform-enterprise.html.md index ecd87425ce..b4312217ae 100644 --- a/website/docs/backends/types/terraform-enterprise.html.md +++ b/website/docs/backends/types/terraform-enterprise.html.md @@ -8,6 +8,9 @@ description: |- # terraform enterprise +-> **Deprecated** Please use the new enhanced [remote](/docs/backends/types/remote.html) +backend for storing state and running remote operations in Terraform Enterprise. + **Kind: Standard (with no locking)** Reads and writes state from a [Terraform Enterprise](/docs/enterprise/index.html) diff --git a/website/layouts/backend-types.erb b/website/layouts/backend-types.erb index 072a45baa6..78b2187fcc 100644 --- a/website/layouts/backend-types.erb +++ b/website/layouts/backend-types.erb @@ -16,6 +16,9 @@ > local + > + remote +