Merge remote-tracking branch 'upstream/master' into patch-1

* upstream/master: (66 commits)
  lang/eval: more evalContext fixups
  Update CHANGELOG.md
  Cleanup after v0.12.10 release
  v0.12.10
  website / help: reconcile 'validate' command docs
  website: Document behavior of `self` object for provisioners
  vendor: switch to HCL 2.0 in the HCL repository
  Update communicator/ssh/communicator.go
  copy client pointer for keep-alive loop
  Update CHANGELOG.md
  slow down tfce polling to 1s
  typos. some code, some text.
  Remove -check-variables flag from the docs
  Merge cleanup, remove `license` parameter in favor of bool `accept_license`, adjust how license acceptance is done, update hab provisioner doc.
  vendor latest go-tfe
  clean up go mod for go-tfe
  tfce test additions
  update to go-tfe 0.3.23
  cost estimation status polling
  go-tfe dep update to 0.3.22
  ...
This commit is contained in:
Nicolas Lamirault 2019-10-08 16:45:06 +02:00
commit 470de23a64
No known key found for this signature in database
GPG Key ID: 65D6BB0526B7051F
323 changed files with 29038 additions and 1569 deletions

View File

@ -15,35 +15,35 @@ Specifically, we have provided checklists below for each type of issue and pull
request that can happen on the project. These checklists represent everything
we need to be able to review and respond quickly.
## HashiCorp vs. Community Providers
## HashiCorp, Official, and Community Providers
We separate providers out into what we call "HashiCorp Providers" and
"Community Providers".
We separate providers out into what we call "HashiCorp Providers", "Partner Providers" and "Community Providers".
HashiCorp providers are providers that we'll dedicate full time resources to
HashiCorp providers are providers that we dedicate full time engineers to
improving, supporting the latest features, and fixing bugs. These are providers
we understand deeply and are confident we have the resources to manage
ourselves.
Community providers are providers where we depend on the community to
Partner providers are providers where we depend on our partners to
contribute fixes and enhancements to improve. HashiCorp will run automated
tests and ensure these providers continue to work, but will not dedicate full
time resources to add new features to these providers. These providers are
time engineers to add new features to these providers. These providers are
available in official Terraform releases, but the functionality is primarily
contributed.
The current list of HashiCorp Providers is as follows:
All HashiCorp and Partner providers can be found in the (terraform-providers github organization)[https://github.com/terraform-providers].
Any provider issues should be opened in the provider's repository.
* `aws`
* `azurerm`
* `google`
* `opc`
Our testing standards are the same for both HashiCorp and Community providers,
Our testing standards are the same for both HashiCorp and Official providers,
and HashiCorp runs full acceptance test suites for every provider nightly to
ensure Terraform remains stable.
We make the distinction between these two types of providers to help
Community Providers are providers that are neither maintained nor tested by
HashiCorp. We can make no promises that these providers will work with any given
version of Terraform. These providers are not automatically installed by
`terraform init` and instead require manual installation.
We make the distinction between these types of providers to help
highlight the vast amounts of community effort that goes in to making Terraform
great, and to help contributors better understand the role HashiCorp employees
play in the various areas of the code base.
@ -52,9 +52,8 @@ play in the various areas of the code base.
### Issue Reporting Checklists
We welcome issues of all kinds including feature requests, bug reports, and
general questions. Below you'll find checklists with guidelines for well-formed
issues of each type.
We welcome feature requests and bug reports. Below you'll find checklists with
guidelines for well-formed issues of each type.
#### Bug Reports
@ -88,12 +87,15 @@ issues of each type.
#### Questions
- [ ] __Search for answers in Terraform documentation__: We're happy to answer
questions in GitHub Issues, but it helps reduce issue churn and maintainer
workload if you work to find answers to common questions in the
documentation. Often times Question issues result in documentation updates
to help future users, so if you don't find an answer, you can give us
pointers for where you'd expect to see it in the docs.
Please do not use GitHub to ask questions! Instead:
* __Search for answers in Terraform documentation__
* __Ask in the Community Forum__: Use [the community forum](https://discuss.hashicorp.com/c/terraform-core) for questions not answered by the documentation.
* __Request an update to the documentation__: If you find that the
documentation is confusing or incorrect, open an issue (or a pull request) and
let us know.
### Issue Lifecycle
@ -121,8 +123,6 @@ issues of each type.
Thank you for contributing! Here you'll find information on what to include in
your Pull Request to ensure it is accepted quickly.
* For pull requests that follow the guidelines, we expect to be able to review
and merge very quickly.
* Pull requests that don't follow the guidelines will be annotated with what
they're missing. A community or core team member may be able to swing around
and help finish up the work, but these PRs will generally hang out much
@ -170,82 +170,15 @@ easy for anybody to help us improve our docs.
site immediately, or is it referencing an upcoming version of Terraform and
should get pushed out with the next release?
#### Enhancement/Bugfix to a Resource
Working on existing resources is a great way to get started as a Terraform
contributor because you can work within existing code and tests to get a feel
for what to do.
- [ ] __Acceptance test coverage of new behavior__: Existing resources each
have a set of [acceptance tests][acctests] covering their functionality.
These tests should exercise all the behavior of the resource. Whether you are
adding something or fixing a bug, the idea is to have an acceptance test that
fails if your code were to be removed. Sometimes it is sufficient to
"enhance" an existing test by adding an assertion or tweaking the config
that is used, but often a new test is better to add. You can copy/paste an
existing test and follow the conventions you see there, modifying the test
to exercise the behavior of your code.
- [ ] __Documentation updates__: If your code makes any changes that need to
be documented, you should include those doc updates in the same PR. The
[Terraform website][website] source is in this repo and includes
instructions for getting a local copy of the site up and running if you'd
like to preview your changes.
- [ ] __Well-formed Code__: Do your best to follow existing conventions you
see in the codebase, and ensure your code is formatted with `go fmt`. (The
Travis CI build will fail if `go fmt` has not been run on incoming code.)
The PR reviewers can help out on this front, and may provide comments with
suggestions on how to improve the code.
#### New Resource
Implementing a new resource is a good way to learn more about how Terraform
interacts with upstream APIs. There are plenty of examples to draw from in the
existing resources, but you still get to implement something completely new.
- [ ] __Minimal LOC__: It can be inefficient for both the reviewer
and author to go through long feedback cycles on a big PR with many
resources. We therefore encourage you to only submit **1 resource at a time**.
- [ ] __Acceptance tests__: New resources should include acceptance tests
covering their behavior. See [Writing Acceptance
Tests](#writing-acceptance-tests) below for a detailed guide on how to
approach these.
- [ ] __Documentation__: Each resource gets a page in the Terraform
documentation. The [Terraform website][website] source is in this
repo and includes instructions for getting a local copy of the site up and
running if you'd like to preview your changes. For a resource, you'll want
to add a new file in the appropriate place and add a link to the sidebar for
that page.
- [ ] __Well-formed Code__: Do your best to follow existing conventions you
see in the codebase, and ensure your code is formatted with `go fmt`. (The
Travis CI build will fail if `go fmt` has not been run on incoming code.)
The PR reviewers can help out on this front, and may provide comments with
suggestions on how to improve the code.
#### New Provider
Implementing a new provider gives Terraform the ability to manage resources in
a whole new API. It's a larger undertaking, but brings major new functionality
into Terraform.
- [ ] __Minimal initial LOC__: Some providers may be big and it can be
inefficient for both reviewer & author to go through long feedback cycles
on a big PR with many resources. We encourage you to only submit
the necessary minimum in a single PR, ideally **just the first resource**
of the provider.
- [ ] __Acceptance tests__: Each provider should include an acceptance test
suite with tests for each resource should include acceptance tests covering
its behavior. See [Writing Acceptance Tests](#writing-acceptance-tests) below
for a detailed guide on how to approach these.
- [ ] __Documentation__: Each provider has a section in the Terraform
documentation. The [Terraform website][website] source is in this repo and
includes instructions for getting a local copy of the site up and running if
you'd like to preview your changes. For a provider, you'll want to add new
index file and individual pages for each resource.
- [ ] __Well-formed Code__: Do your best to follow existing conventions you
see in the codebase, and ensure your code is formatted with `go fmt`. (The
Travis CI build will fail if `go fmt` has not been run on incoming code.)
The PR reviewers can help out on this front, and may provide comments with
suggestions on how to improve the code.
Terraform Providers are external plugins, not in the Terraform codebase. Please
see the [Provider Development Program](https://www.terraform.io/guides/terraform-provider-development-program.html) documentation if you are interested in
submitting a new provider.
#### Core Bugfix/Enhancement
@ -281,7 +214,9 @@ get feedback early and often on the effort.
is complicated enough that there are often several ways to implement
something, each of which has different implications and tradeoffs. Working
through a plan of attack with the team before you dive into implementation
will help ensure that you're working in the right direction.
will help ensure that you're working in the right direction. Opening a GitHub
issue, or commenting on an existing issue, is a great way to get these
conversations started.
- [ ] __Unit tests__: Terraform's core is covered by hundreds of unit tests at
several different layers of abstraction. Generally the best place to start
is with a "Context Test". These are higher level test that interact
@ -304,9 +239,6 @@ get feedback early and often on the effort.
### Writing Acceptance Tests
Terraform includes an acceptance test harness that does most of the repetitive
work involved in testing a resource.
#### Acceptance Tests Often Cost Money to Run
Because acceptance tests create real resources, they often cost money to run.
@ -326,199 +258,8 @@ Acceptance tests can be run using the `testacc` target in the Terraform
expression. Prior to running the tests provider configuration details such as
access keys must be made available as environment variables.
For example, to run an acceptance test against the Azure Resource Manager
provider, the following environment variables must be set:
```sh
export ARM_SUBSCRIPTION_ID=...
export ARM_CLIENT_ID=...
export ARM_CLIENT_SECRET=...
export ARM_TENANT_ID=...
```
Tests can then be run by specifying the target provider and a regular
expression defining the tests to run:
```sh
$ make testacc TEST=./builtin/providers/azurerm TESTARGS='-run=TestAccAzureRMPublicIpStatic_update'
==> Checking that code complies with gofmt requirements...
go generate ./...
TF_ACC=1 go test ./builtin/providers/azurerm -v -run=TestAccAzureRMPublicIpStatic_update -timeout 120m
=== RUN TestAccAzureRMPublicIpStatic_update
--- PASS: TestAccAzureRMPublicIpStatic_update (177.48s)
PASS
ok github.com/hashicorp/terraform/builtin/providers/azurerm 177.504s
```
Entire resource test suites can be targeted by using the naming convention to
write the regular expression. For example, to run all tests of the
`azurerm_public_ip` resource rather than just the update test, you can start
testing like this:
```sh
$ make testacc TEST=./builtin/providers/azurerm TESTARGS='-run=TestAccAzureRMPublicIpStatic'
==> Checking that code complies with gofmt requirements...
go generate ./...
TF_ACC=1 go test ./builtin/providers/azurerm -v -run=TestAccAzureRMPublicIpStatic -timeout 120m
=== RUN TestAccAzureRMPublicIpStatic_basic
--- PASS: TestAccAzureRMPublicIpStatic_basic (137.74s)
=== RUN TestAccAzureRMPublicIpStatic_update
--- PASS: TestAccAzureRMPublicIpStatic_update (180.63s)
PASS
ok github.com/hashicorp/terraform/builtin/providers/azurerm 318.392s
```
#### Writing an Acceptance Test
Terraform has a framework for writing acceptance tests which minimises the
amount of boilerplate code necessary to use common testing patterns. The entry
point to the framework is the `resource.Test()` function.
Tests are divided into `TestStep`s. Each `TestStep` proceeds by applying some
Terraform configuration using the provider under test, and then verifying that
results are as expected by making assertions using the provider API. It is
common for a single test function to exercise both the creation of and updates
to a single resource. Most tests follow a similar structure.
1. Pre-flight checks are made to ensure that sufficient provider configuration
is available to be able to proceed - for example in an acceptance test
targeting AWS, `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` must be set prior
to running acceptance tests. This is common to all tests exercising a single
provider.
Each `TestStep` is defined in the call to `resource.Test()`. Most assertion
functions are defined out of band with the tests. This keeps the tests
readable, and allows reuse of assertion functions across different tests of the
same type of resource. The definition of a complete test looks like this:
```go
func TestAccAzureRMPublicIpStatic_update(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckAzureRMPublicIpDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAzureRMVPublicIpStatic_basic,
Check: resource.ComposeTestCheckFunc(
testCheckAzureRMPublicIpExists("azurerm_public_ip.test"),
),
},
},
})
}
```
When executing the test, the following steps are taken for each `TestStep`:
1. The Terraform configuration required for the test is applied. This is
responsible for configuring the resource under test, and any dependencies it
may have. For example, to test the `azurerm_public_ip` resource, an
`azurerm_resource_group` is required. This results in configuration which
looks like this:
```hcl
resource "azurerm_resource_group" "test" {
name = "acceptanceTestResourceGroup1"
location = "West US"
}
resource "azurerm_public_ip" "test" {
name = "acceptanceTestPublicIp1"
location = "West US"
resource_group_name = "${azurerm_resource_group.test.name}"
public_ip_address_allocation = "static"
}
```
1. Assertions are run using the provider API. These use the provider API
directly rather than asserting against the resource state. For example, to
verify that the `azurerm_public_ip` described above was created
successfully, a test function like this is used:
```go
func testCheckAzureRMPublicIpExists(name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
// Ensure we have enough information in state to look up in API
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("Not found: %s", name)
}
publicIPName := rs.Primary.Attributes["name"]
resourceGroup, hasResourceGroup := rs.Primary.Attributes["resource_group_name"]
if !hasResourceGroup {
return fmt.Errorf("Bad: no resource group found in state for public ip: %s", availSetName)
}
conn := testAccProvider.Meta().(*ArmClient).publicIPClient
resp, err := conn.Get(resourceGroup, publicIPName, "")
if err != nil {
return fmt.Errorf("Bad: Get on publicIPClient: %s", err)
}
if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("Bad: Public IP %q (resource group: %q) does not exist", name, resourceGroup)
}
return nil
}
}
```
Notice that the only information used from the Terraform state is the ID of
the resource - though in this case it is necessary to split the ID into
constituent parts in order to use the provider API. For computed properties,
we instead assert that the value saved in the Terraform state was the
expected value if possible. The testing framework provides helper functions
for several common types of check - for example:
```go
resource.TestCheckResourceAttr("azurerm_public_ip.test", "domain_name_label", "mylabel01"),
```
1. The resources created by the test are destroyed. This step happens
automatically, and is the equivalent of calling `terraform destroy`.
1. Assertions are made against the provider API to verify that the resources
have indeed been removed. If these checks fail, the test fails and reports
"dangling resources". The code to ensure that the `azurerm_public_ip` shown
above looks like this:
```go
func testCheckAzureRMPublicIpDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*ArmClient).publicIPClient
for _, rs := range s.RootModule().Resources {
if rs.Type != "azurerm_public_ip" {
continue
}
name := rs.Primary.Attributes["name"]
resourceGroup := rs.Primary.Attributes["resource_group_name"]
resp, err := conn.Get(resourceGroup, name, "")
if err != nil {
return nil
}
if resp.StatusCode != http.StatusNotFound {
return fmt.Errorf("Public IP still exists:\n%#v", resp.Properties)
}
}
return nil
}
```
These functions usually test only for the resource directly under test: we
skip the check that the `azurerm_resource_group` has been destroyed when
testing `azurerm_resource_group`, under the assumption that
`azurerm_resource_group` is tested independently in its own acceptance
tests.
[website]: https://github.com/hashicorp/terraform/tree/master/website
[acctests]: https://github.com/hashicorp/terraform#acceptance-tests
[community forum]: https://discuss.hashicorp.com/c/terraform-core
[ml]: https://groups.google.com/group/terraform-tool

View File

@ -1,17 +1,37 @@
## 0.12.9 (Unreleased)
NOTES:
* core: `ignore_changes` is now processed (in addition to existing behaviors) before the provider plan is run. This means that users may see fewer planned changes when using `ignore_changes`, as before this change, changes to ignored attributes were still being sent to CustomizeDiff in providers (which could mean cascading changes for some resources). This should be indicative that providers are no longer getting changes that were marked as ignored, but if unexpected plans are seen while using `ignore_changes`, investigate the settings in the `ignore_changes` block to ensure the appropriate attributes are set. [GH-22520]
ENHANCEMENTS:
* provisioners/habitat: `accept_license` argument available to automate accepting the EULA, now required by this client [GH-22745]
* config: add source addressing to unknown value errors in `for_each` [GH-22760]
## 0.12.11 (Unreleased)
BUG FIXES:
* command/console: support -var and -var-file flags [GH-22145]
* command/show: Fixed bug with wrong errors being returned or swallowed. [GH-22772]
* config: The `cidrhost`, `cidrsubnet`, and `cidrnetmask` functions now behave correctly with IPv6 prefixes that are short enough for the host portion to be greater than 64-bit or 32-bit (depending on the target architecture). [GH-22505]
* config: Fixed bug on empty sets with `for_each` [GH-22281]
* config: Clean up orphan modules in the presence of -target [GH-21313]
## 0.12.10 (October 07, 2019)
ENHANCEMENTS:
* `terraform plan` and `terraform apply` will now warn when the `-target` option is used, to draw attention to the fact that the result of applying the plan is likely to be incomplete, and to remind to re-run `terraform plan` with no targets afterwards to ensure that the configuration has converged. ([#22783](https://github.com/hashicorp/terraform/issues/22783))
* config: New function `parseint` for parsing strings containing digits as integers in various bases. ([#22747](https://github.com/hashicorp/terraform/issues/22747))
* config: New function `cidrsubnets`, which is a companion to the existing function `cidrsubnet` which can allocate multiple consecutive subnet prefixes (possibly of different prefix lengths) in a single call. ([#22858](https://github.com/hashicorp/terraform/issues/22858))
* backend/google: The GCS backend now supports OAuth2 token authentication. ([#21772](https://github.com/hashicorp/terraform/issues/21772))
* provisioner/habitat: Multiple updates and fixes, see PR for details ([#22705](https://github.com/hashicorp/terraform/issues/22705))
BUG FIXES:
* backend/manta: fix panic when `insecure_skip_tls_verify` was not set ([#22918](https://github.com/hashicorp/terraform/issues/22918))
## 0.12.9 (September 17, 2019)
NOTES:
* core: `ignore_changes` is now processed (in addition to existing behaviors) before the provider plan is run. This means that users may see fewer planned changes when using `ignore_changes`, as before this change, changes to ignored attributes were still being sent to CustomizeDiff in providers (which could mean cascading changes for some resources). This should be indicative that providers are no longer getting changes that were marked as ignored, but if unexpected plans are seen while using `ignore_changes`, investigate the settings in the `ignore_changes` block to ensure the appropriate attributes are set. ([#22520](https://github.com/hashicorp/terraform/issues/22520))
ENHANCEMENTS:
* provisioners/habitat: `accept_license` argument available to automate accepting the EULA, now required by this client ([#22745](https://github.com/hashicorp/terraform/issues/22745))
* config: add source addressing to unknown value errors in `for_each` ([#22760](https://github.com/hashicorp/terraform/issues/22760))
BUG FIXES:
* command/console: support -var and -var-file flags ([#22145](https://github.com/hashicorp/terraform/issues/22145))
* command/show: Fixed bug with wrong errors being returned or swallowed. ([#22772](https://github.com/hashicorp/terraform/issues/22772))
* config: The `cidrhost`, `cidrsubnet`, and `cidrnetmask` functions now behave correctly with IPv6 prefixes that are short enough for the host portion to be greater than 64-bit or 32-bit (depending on the target architecture). ([#22505](https://github.com/hashicorp/terraform/issues/22505))
* config: Fixed bug on empty sets with `for_each` ([#22281](https://github.com/hashicorp/terraform/issues/22281))
## 0.12.8 (September 04, 2019)

View File

@ -112,8 +112,7 @@ To update a dependency:
Terraform has a comprehensive [acceptance
test](http://en.wikipedia.org/wiki/Acceptance_testing) suite covering the
built-in providers. Our [Contributing Guide](https://github.com/hashicorp/terraform/blob/master/.github/CONTRIBUTING.md) includes details about how and when to write and run acceptance tests in order to help contributions get accepted quickly.
built-in providers.
### Cross Compilation and Building for Distribution

View File

@ -4,8 +4,8 @@ import (
"bytes"
"fmt"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"

View File

@ -3,8 +3,8 @@ package addrs
import (
"fmt"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/tfdiags"
)
@ -290,7 +290,7 @@ func parseResourceRef(mode ResourceMode, startRange hcl.Range, traversal hcl.Tra
// of the resource, but we don't have enough context here to decide
// so we'll let the caller resolve that ambiguity.
return &Reference{
Subject: resourceInstAddr,
Subject: resourceAddr,
SourceRange: tfdiags.SourceRangeFromHCL(rng),
}, diags
}

View File

@ -4,8 +4,8 @@ import (
"testing"
"github.com/go-test/deep"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
)
@ -114,12 +114,10 @@ func TestParseRef(t *testing.T) {
{
`data.external.foo`,
&Reference{
Subject: ResourceInstance{
Resource: Resource{
Mode: DataResourceMode,
Type: "external",
Name: "foo",
},
Subject: Resource{
Mode: DataResourceMode,
Type: "external",
Name: "foo",
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
@ -592,12 +590,10 @@ func TestParseRef(t *testing.T) {
{
`boop_instance.foo`,
&Reference{
Subject: ResourceInstance{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "boop_instance",
Name: "foo",
},
Subject: Resource{
Mode: ManagedResourceMode,
Type: "boop_instance",
Name: "foo",
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},

View File

@ -3,9 +3,9 @@ package addrs
import (
"fmt"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/tfdiags"
)

View File

@ -4,8 +4,8 @@ import (
"testing"
"github.com/go-test/deep"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/tfdiags"
)

View File

@ -5,8 +5,8 @@ import (
"github.com/hashicorp/terraform/tfdiags"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
// ProviderConfig is the address of a provider configuration.

View File

@ -5,8 +5,8 @@ import (
"github.com/go-test/deep"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
func TestParseProviderConfigCompact(t *testing.T) {

View File

@ -76,9 +76,9 @@ func (b *Local) opApply(
// Perform the plan
log.Printf("[INFO] backend/local: apply calling Plan")
plan, err := tfCtx.Plan()
if err != nil {
diags = diags.Append(err)
plan, planDiags := tfCtx.Plan()
diags = diags.Append(planDiags)
if planDiags.HasErrors() {
b.ReportResult(runningOp, diags)
return
}
@ -112,6 +112,13 @@ func (b *Local) opApply(
b.CLI.Output("")
}
// We'll show any accumulated warnings before we display the prompt,
// so the user can consider them when deciding how to answer.
if len(diags) > 0 {
b.ShowDiagnostics(diags)
diags = nil // reset so we won't show the same diagnostics again later
}
v, err := op.UIIn.Input(stopCtx, &terraform.InputOpts{
Id: "approve",
Query: query,

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"reflect"
"strings"
"testing"
"time"
@ -45,6 +46,9 @@ func prepareEtcdv3(t *testing.T) {
t.Log("etcd server tests require setting TF_ACC or TF_ETCDV3_TEST")
t.Skip()
}
if reflect.DeepEqual(etcdv3Endpoints, []string{""}) {
t.Fatal("etcd server tests require setting TF_ETCDV3_ENDPOINTS")
}
cleanupEtcdv3(t)
}

View File

@ -14,6 +14,7 @@ import (
"github.com/hashicorp/terraform/helper/pathorcontents"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/httpclient"
"golang.org/x/oauth2"
"golang.org/x/oauth2/jwt"
"google.golang.org/api/option"
)
@ -65,6 +66,15 @@ func New() backend.Backend {
Default: "",
},
"access_token": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{
"GOOGLE_OAUTH_ACCESS_TOKEN",
}, nil),
Description: "An OAuth2 token used for GCP authentication",
},
"encryption_key": {
Type: schema.TypeString,
Optional: true,
@ -116,12 +126,23 @@ func (b *Backend) configure(ctx context.Context) error {
var opts []option.ClientOption
creds := data.Get("credentials").(string)
if creds == "" {
// Add credential source
var creds string
var tokenSource oauth2.TokenSource
if v, ok := data.GetOk("access_token"); ok {
tokenSource = oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: v.(string),
})
} else if v, ok := data.GetOk("credentials"); ok {
creds = v.(string)
} else {
creds = os.Getenv("GOOGLE_CREDENTIALS")
}
if creds != "" {
if tokenSource != nil {
opts = append(opts, option.WithTokenSource(tokenSource))
} else if creds != "" {
var account accountFile
// to mirror how the provider works, we accept the file path or the contents

View File

@ -7,7 +7,7 @@ import (
"os"
"testing"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/helper/logging"

View File

@ -3,7 +3,7 @@ package inmem
import (
"testing"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state/remote"
)

View File

@ -53,7 +53,7 @@ func New() backend.Backend {
"insecure_skip_tls_verify": {
Type: schema.TypeBool,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TRITON_SKIP_TLS_VERIFY", ""),
DefaultFunc: schema.EnvDefaultFunc("TRITON_SKIP_TLS_VERIFY", false),
},
"path": {

View File

@ -18,6 +18,14 @@ func testACC(t *testing.T) {
t.Log("Manta backend tests require setting TF_ACC or TF_MANTA_TEST")
t.Skip()
}
skip = os.Getenv("TRITON_ACCOUNT") == "" && os.Getenv("SDC_ACCOUNT") == ""
if skip {
t.Fatal("Manta backend tests require setting TRITON_ACCOUNT or SDC_ACCOUNT")
}
skip = os.Getenv("TRITON_KEY_ID") == "" && os.Getenv("SDC_KEY_ID") == ""
if skip {
t.Fatal("Manta backend tests require setting TRITON_KEY_ID or SDC_KEY_ID")
}
}
func TestBackend_impl(t *testing.T) {

View File

@ -6,11 +6,12 @@ import (
"testing"
"time"
"strings"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/aliyun/aliyun-tablestore-go-sdk/tablestore"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/configs/hcl2shim"
"strings"
)
// verify that we are doing ACC tests or the OSS tests specifically
@ -20,6 +21,9 @@ func testACC(t *testing.T) {
t.Log("oss backend tests require setting TF_ACC or TF_OSS_TEST")
t.Skip()
}
if skip {
t.Fatal("oss backend tests require setting ALICLOUD_ACCESS_KEY or ALICLOUD_ACCESS_KEY_ID")
}
if os.Getenv("ALICLOUD_REGION") == "" {
os.Setenv("ALICLOUD_REGION", "cn-beijing")
}

View File

@ -7,6 +7,8 @@ import (
"fmt"
"io"
"math"
"strconv"
"strings"
"time"
tfe "github.com/hashicorp/go-tfe"
@ -227,6 +229,87 @@ func (b *Remote) parseVariableValues(op *backend.Operation) (terraform.InputValu
return result, diags
}
func (b *Remote) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error {
if r.CostEstimate == nil {
return nil
}
if b.CLI != nil {
b.CLI.Output("\n------------------------------------------------------------------------\n")
}
msgPrefix := "Cost estimation"
if b.CLI != nil {
b.CLI.Output(b.Colorize().Color(msgPrefix + ":\n"))
}
started := time.Now()
updated := started
for i := 0; ; i++ {
select {
case <-stopCtx.Done():
return stopCtx.Err()
case <-cancelCtx.Done():
return cancelCtx.Err()
case <-time.After(1 * time.Second):
}
// Retrieve the cost estimate to get its current status.
ce, err := b.client.CostEstimates.Read(stopCtx, r.CostEstimate.ID)
if err != nil {
return generalError("Failed to retrieve cost estimate", err)
}
switch ce.Status {
case tfe.CostEstimateFinished:
delta, err := strconv.ParseFloat(ce.DeltaMonthlyCost, 64)
if err != nil {
return generalError("Unexpected error", err)
}
sign := "+"
if delta < 0 {
sign = "-"
}
deltaRepr := strings.Replace(ce.DeltaMonthlyCost, "-", "", 1)
if b.CLI != nil {
b.CLI.Output(b.Colorize().Color(fmt.Sprintf("Resources: %d of %d estimated", ce.MatchedResourcesCount, ce.ResourcesCount)))
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(" $%s/mo %s$%s", ce.ProposedMonthlyCost, sign, deltaRepr)))
if len(r.PolicyChecks) == 0 && r.HasChanges && op.Type == backend.OperationTypeApply {
b.CLI.Output("\n------------------------------------------------------------------------")
}
}
return nil
case tfe.CostEstimatePending, tfe.CostEstimateQueued:
// Check if 30 seconds have passed since the last update.
current := time.Now()
if b.CLI != nil && (i == 0 || current.Sub(updated).Seconds() > 30) {
updated = current
elapsed := ""
// Calculate and set the elapsed time.
if i > 0 {
elapsed = fmt.Sprintf(
" (%s elapsed)", current.Sub(started).Truncate(30*time.Second))
}
b.CLI.Output(b.Colorize().Color("Waiting for cost estimate to complete..." + elapsed + "\n"))
}
continue
case tfe.CostEstimateErrored:
return fmt.Errorf(msgPrefix + " errored.")
case tfe.CostEstimateCanceled:
return fmt.Errorf(msgPrefix + " canceled.")
default:
return fmt.Errorf("Unknown or unexpected cost estimate state: %s", ce.Status)
}
}
return nil
}
func (b *Remote) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error {
if b.CLI != nil {
b.CLI.Output("\n------------------------------------------------------------------------\n")

View File

@ -21,6 +21,7 @@ import (
type mockClient struct {
Applies *mockApplies
ConfigurationVersions *mockConfigurationVersions
CostEstimates *mockCostEstimates
Organizations *mockOrganizations
Plans *mockPlans
PolicyChecks *mockPolicyChecks
@ -33,6 +34,7 @@ func newMockClient() *mockClient {
c := &mockClient{}
c.Applies = newMockApplies(c)
c.ConfigurationVersions = newMockConfigurationVersions(c)
c.CostEstimates = newMockCostEstimates(c)
c.Organizations = newMockOrganizations(c)
c.Plans = newMockPlans(c)
c.PolicyChecks = newMockPolicyChecks(c)
@ -212,6 +214,88 @@ func (m *mockConfigurationVersions) Upload(ctx context.Context, url, path string
return nil
}
type mockCostEstimates struct {
client *mockClient
estimations map[string]*tfe.CostEstimate
logs map[string]string
}
func newMockCostEstimates(client *mockClient) *mockCostEstimates {
return &mockCostEstimates{
client: client,
estimations: make(map[string]*tfe.CostEstimate),
logs: make(map[string]string),
}
}
// create is a helper function to create a mock cost estimation that uses the
// configured working directory to find the logfile.
func (m *mockCostEstimates) create(cvID, workspaceID string) (*tfe.CostEstimate, error) {
id := generateID("ce-")
ce := &tfe.CostEstimate{
ID: id,
MatchedResourcesCount: 1,
ResourcesCount: 1,
DeltaMonthlyCost: "0.00",
ProposedMonthlyCost: "0.00",
Status: tfe.CostEstimateFinished,
}
w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
logfile := filepath.Join(
m.client.ConfigurationVersions.uploadPaths[cvID],
w.WorkingDirectory,
"cost-estimate.log",
)
if _, err := os.Stat(logfile); os.IsNotExist(err) {
return nil, nil
}
m.logs[ce.ID] = logfile
m.estimations[ce.ID] = ce
return ce, nil
}
func (m *mockCostEstimates) Read(ctx context.Context, costEstimateID string) (*tfe.CostEstimate, error) {
ce, ok := m.estimations[costEstimateID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
return ce, nil
}
func (m *mockCostEstimates) Logs(ctx context.Context, costEstimateID string) (io.Reader, error) {
ce, ok := m.estimations[costEstimateID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
logfile, ok := m.logs[ce.ID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
if _, err := os.Stat(logfile); os.IsNotExist(err) {
return bytes.NewBufferString("logfile does not exist"), nil
}
logs, err := ioutil.ReadFile(logfile)
if err != nil {
return nil, err
}
ce.Status = tfe.CostEstimateFinished
return bytes.NewBuffer(logs), nil
}
// mockInput is a mock implementation of terraform.UIInput.
type mockInput struct {
answers map[string]string
@ -647,6 +731,11 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t
return nil, err
}
ce, err := m.client.CostEstimates.create(options.ConfigurationVersion.ID, options.Workspace.ID)
if err != nil {
return nil, err
}
p, err := m.client.Plans.create(options.ConfigurationVersion.ID, options.Workspace.ID)
if err != nil {
return nil, err
@ -658,13 +747,14 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t
}
r := &tfe.Run{
ID: generateID("run-"),
Actions: &tfe.RunActions{IsCancelable: true},
Apply: a,
HasChanges: false,
Permissions: &tfe.RunPermissions{},
Plan: p,
Status: tfe.RunPending,
ID: generateID("run-"),
Actions: &tfe.RunActions{IsCancelable: true},
Apply: a,
CostEstimate: ce,
HasChanges: false,
Permissions: &tfe.RunPermissions{},
Plan: p,
Status: tfe.RunPending,
}
if pc != nil {
@ -960,6 +1050,14 @@ func (m *mockWorkspaces) Read(ctx context.Context, organization, workspace strin
return w, nil
}
func (m *mockWorkspaces) ReadByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
w, ok := m.workspaceIDs[workspaceID]
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 {
@ -982,6 +1080,28 @@ func (m *mockWorkspaces) Update(ctx context.Context, organization, workspace str
return w, nil
}
func (m *mockWorkspaces) UpdateByID(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) {
w, ok := m.workspaceIDs[workspaceID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
if options.Name != nil {
w.Name = *options.Name
}
if options.TerraformVersion != nil {
w.TerraformVersion = *options.TerraformVersion
}
if options.WorkingDirectory != nil {
w.WorkingDirectory = *options.WorkingDirectory
}
delete(m.workspaceNames, w.Name)
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)
@ -990,6 +1110,14 @@ func (m *mockWorkspaces) Delete(ctx context.Context, organization, workspace str
return nil
}
func (m *mockWorkspaces) DeleteByID(ctx context.Context, workspaceID string) error {
if w, ok := m.workspaceIDs[workspaceID]; ok {
delete(m.workspaceIDs, w.Name)
}
delete(m.workspaceIDs, workspaceID)
return nil
}
func (m *mockWorkspaces) RemoveVCSConnection(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) {
w, ok := m.workspaceNames[workspace]
if !ok {
@ -999,6 +1127,15 @@ func (m *mockWorkspaces) RemoveVCSConnection(ctx context.Context, organization,
return w, nil
}
func (m *mockWorkspaces) RemoveVCSConnectionByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
w, ok := m.workspaceIDs[workspaceID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
w.VCSRepo = nil
return w, nil
}
func (m *mockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) {
w, ok := m.workspaceIDs[workspaceID]
if !ok {

View File

@ -316,6 +316,14 @@ to capture the filesystem context the remote workspace expects:
return r, nil
}
// Show any cost estimation output.
if r.CostEstimate != nil {
err = b.costEstimate(stopCtx, cancelCtx, op, r)
if err != nil {
return r, err
}
}
// Check any configured sentinel policies.
if len(r.PolicyChecks) > 0 {
err = b.checkPolicy(stopCtx, cancelCtx, op, r)

View File

@ -59,7 +59,7 @@ func TestRemote_planBasic(t *testing.T) {
t.Fatalf("expected remote backend header in output: %s", output)
}
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output)
t.Fatalf("expected plan summary in output: %s", output)
}
}
@ -113,7 +113,7 @@ func TestRemote_planLongLine(t *testing.T) {
t.Fatalf("expected remote backend header in output: %s", output)
}
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output)
t.Fatalf("expected plan summary in output: %s", output)
}
}
@ -374,7 +374,7 @@ func TestRemote_planNoChanges(t *testing.T) {
output := b.CLI.(*cli.MockUi).OutputWriter.String()
if !strings.Contains(output, "No changes. Infrastructure is up-to-date.") {
t.Fatalf("expected no changes in plan summery: %s", output)
t.Fatalf("expected no changes in plan summary: %s", output)
}
if !strings.Contains(output, "Sentinel Result: true") {
t.Fatalf("expected policy check result in output: %s", output)
@ -415,7 +415,7 @@ func TestRemote_planForceLocal(t *testing.T) {
t.Fatalf("unexpected remote backend header in output: %s", output)
}
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output)
t.Fatalf("expected plan summary in output: %s", output)
}
}
@ -446,7 +446,7 @@ func TestRemote_planWithoutOperationsEntitlement(t *testing.T) {
t.Fatalf("unexpected remote backend header in output: %s", output)
}
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output)
t.Fatalf("expected plan summary in output: %s", output)
}
}
@ -491,7 +491,7 @@ func TestRemote_planWorkspaceWithoutOperations(t *testing.T) {
t.Fatalf("unexpected remote backend header in output: %s", output)
}
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output)
t.Fatalf("expected plan summary in output: %s", output)
}
}
@ -562,7 +562,7 @@ func TestRemote_planLockTimeout(t *testing.T) {
t.Fatalf("expected lock timout error in output: %s", output)
}
if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("unexpected plan summery in output: %s", output)
t.Fatalf("unexpected plan summary in output: %s", output)
}
}
@ -654,7 +654,7 @@ func TestRemote_planWithWorkingDirectory(t *testing.T) {
t.Fatalf("expected remote backend header in output: %s", output)
}
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output)
t.Fatalf("expected plan summary in output: %s", output)
}
}
@ -709,7 +709,41 @@ func TestRemote_planWithWorkingDirectoryFromCurrentPath(t *testing.T) {
t.Fatalf("expected remote backend header in output: %s", output)
}
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output)
t.Fatalf("expected plan summary in output: %s", output)
}
}
func TestRemote_planCostEstimation(t *testing.T) {
b, bCleanup := testBackendDefault(t)
defer bCleanup()
op, configCleanup := testOperationPlan(t, "./testdata/plan-cost-estimation")
defer configCleanup()
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.Result != backend.OperationSuccess {
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
}
if run.PlanEmpty {
t.Fatalf("expected a non-empty plan")
}
output := b.CLI.(*cli.MockUi).OutputWriter.String()
if !strings.Contains(output, "Running plan in the remote backend") {
t.Fatalf("expected remote backend header in output: %s", output)
}
if !strings.Contains(output, "Resources: 1 of 1 estimated") {
t.Fatalf("expected cost estimate result in output: %s", output)
}
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summary in output: %s", output)
}
}
@ -743,7 +777,7 @@ func TestRemote_planPolicyPass(t *testing.T) {
t.Fatalf("expected policy check result in output: %s", output)
}
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output)
t.Fatalf("expected plan summary in output: %s", output)
}
}
@ -782,7 +816,7 @@ func TestRemote_planPolicyHardFail(t *testing.T) {
t.Fatalf("expected policy check result in output: %s", output)
}
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output)
t.Fatalf("expected plan summary in output: %s", output)
}
}
@ -821,7 +855,7 @@ func TestRemote_planPolicySoftFail(t *testing.T) {
t.Fatalf("expected policy check result in output: %s", output)
}
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output)
t.Fatalf("expected plan summary in output: %s", output)
}
}

View File

@ -0,0 +1,5 @@
Cost estimation:
Waiting for cost estimation to complete...
Resources: 1 of 1 estimated
$25.488/mo +$25.488

View File

@ -1,5 +1,4 @@
Terraform v0.11.7
Terraform v0.12.9
Configuring remote state backend...
Initializing Terraform configuration...
Refreshing Terraform state in-memory prior to plan...

View File

@ -115,6 +115,7 @@ func testBackend(t *testing.T, obj cty.Value) (*Remote, func()) {
b.CLI = cli.NewMockUi()
b.client.Applies = mc.Applies
b.client.ConfigurationVersions = mc.ConfigurationVersions
b.client.CostEstimates = mc.CostEstimates
b.client.Organizations = mc.Organizations
b.client.Plans = mc.Plans
b.client.PolicyChecks = mc.PolicyChecks

View File

@ -6,8 +6,8 @@ import (
"testing"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcldec"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"

View File

@ -3,7 +3,7 @@ package backend
import (
"fmt"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"

View File

@ -646,12 +646,7 @@ resource "test_resource_nested_set" "foo" {
}
}
`),
Check: resource.ComposeTestCheckFunc(
func(s *terraform.State) error {
fmt.Println(s)
return nil
},
),
Check: resource.ComposeTestCheckFunc(),
},
},
})

View File

@ -14,6 +14,11 @@ func testResourceTimeout() *schema.Resource {
Update: testResourceTimeoutUpdate,
Delete: testResourceTimeoutDelete,
// Due to the schema version also being stashed in the private/meta
// data, we need to ensure that it does not overwrite the map
// containing the timeouts.
SchemaVersion: 1,
Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(time.Second),
Update: schema.DefaultTimeout(time.Second),

View File

@ -0,0 +1,376 @@
package habitat
import (
"bytes"
"errors"
"fmt"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/terraform"
"path"
"path/filepath"
"strings"
"text/template"
)
const installURL = "https://raw.githubusercontent.com/habitat-sh/habitat/master/components/hab/install.sh"
const systemdUnit = `[Unit]
Description=Habitat Supervisor
[Service]
ExecStart=/bin/hab sup run{{ .SupOptions }}
Restart=on-failure
{{ if .GatewayAuthToken -}}
Environment="HAB_SUP_GATEWAY_AUTH_TOKEN={{ .GatewayAuthToken }}"
{{ end -}}
{{ if .BuilderAuthToken -}}
Environment="HAB_AUTH_TOKEN={{ .BuilderAuthToken }}"
{{ end -}}
[Install]
WantedBy=default.target
`
func (p *provisioner) linuxInstallHabitat(o terraform.UIOutput, comm communicator.Communicator) error {
// Download the hab installer
if err := p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("curl --silent -L0 %s > install.sh", installURL))); err != nil {
return err
}
// Run the install script
var command string
if p.Version == "" {
command = fmt.Sprintf("bash ./install.sh ")
} else {
command = fmt.Sprintf("bash ./install.sh -v %s", p.Version)
}
if err := p.runCommand(o, comm, p.linuxGetCommand(command)); err != nil {
return err
}
// Accept the license
if p.AcceptLicense {
var cmd string
if p.UseSudo == true {
cmd = "env HAB_LICENSE=accept sudo -E /bin/bash -c 'hab -V'"
} else {
cmd = "env HAB_LICENSE=accept /bin/bash -c 'hab -V'"
}
if err := p.runCommand(o, comm, cmd); err != nil {
return err
}
}
// Create the hab user
if err := p.createHabUser(o, comm); err != nil {
return err
}
// Cleanup the installer
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("rm -f install.sh")))
}
func (p *provisioner) createHabUser(o terraform.UIOutput, comm communicator.Communicator) error {
var addUser bool
// Install busybox to get us the user tools we need
if err := p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("hab install core/busybox"))); err != nil {
return err
}
// Check for existing hab user
if err := p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("hab pkg exec core/busybox id hab"))); err != nil {
o.Output("No existing hab user detected, creating...")
addUser = true
}
if addUser {
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("hab pkg exec core/busybox adduser -D -g \"\" hab")))
}
return nil
}
func (p *provisioner) linuxStartHabitat(o terraform.UIOutput, comm communicator.Communicator) error {
// Install the supervisor first
var command string
if p.Version == "" {
command += p.linuxGetCommand(fmt.Sprintf("hab install core/hab-sup"))
} else {
command += p.linuxGetCommand(fmt.Sprintf("hab install core/hab-sup/%s", p.Version))
}
if err := p.runCommand(o, comm, command); err != nil {
return err
}
// Build up supervisor options
options := ""
if p.PermanentPeer {
options += " --permanent-peer"
}
if p.ListenCtl != "" {
options += fmt.Sprintf(" --listen-ctl %s", p.ListenCtl)
}
if p.ListenGossip != "" {
options += fmt.Sprintf(" --listen-gossip %s", p.ListenGossip)
}
if p.ListenHTTP != "" {
options += fmt.Sprintf(" --listen-http %s", p.ListenHTTP)
}
if p.Peer != "" {
options += fmt.Sprintf(" %s", p.Peer)
}
if len(p.Peers) > 0 {
if len(p.Peers) == 1 {
options += fmt.Sprintf(" --peer %s", p.Peers[0])
} else {
options += fmt.Sprintf(" --peer %s", strings.Join(p.Peers, " --peer "))
}
}
if p.RingKey != "" {
options += fmt.Sprintf(" --ring %s", p.RingKey)
}
if p.URL != "" {
options += fmt.Sprintf(" --url %s", p.URL)
}
if p.Channel != "" {
options += fmt.Sprintf(" --channel %s", p.Channel)
}
if p.Events != "" {
options += fmt.Sprintf(" --events %s", p.Events)
}
if p.Organization != "" {
options += fmt.Sprintf(" --org %s", p.Organization)
}
if p.HttpDisable == true {
options += fmt.Sprintf(" --http-disable")
}
if p.AutoUpdate == true {
options += fmt.Sprintf(" --auto-update")
}
p.SupOptions = options
// Start hab depending on service type
switch p.ServiceType {
case "unmanaged":
return p.linuxStartHabitatUnmanaged(o, comm, options)
case "systemd":
return p.linuxStartHabitatSystemd(o, comm, options)
default:
return errors.New("unsupported service type")
}
}
// This func is a little different than the others since we need to expose HAB_AUTH_TOKEN to a shell
// sub-process that's actually running the supervisor.
func (p *provisioner) linuxStartHabitatUnmanaged(o terraform.UIOutput, comm communicator.Communicator, options string) error {
var token string
// Create the sup directory for the log file
if err := p.runCommand(o, comm, p.linuxGetCommand("mkdir -p /hab/sup/default && chmod o+w /hab/sup/default")); err != nil {
return err
}
// Set HAB_AUTH_TOKEN if provided
if p.BuilderAuthToken != "" {
token = fmt.Sprintf("env HAB_AUTH_TOKEN=%s ", p.BuilderAuthToken)
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("(%ssetsid hab sup run%s > /hab/sup/default/sup.log 2>&1 <&1 &) ; sleep 1", token, options)))
}
func (p *provisioner) linuxStartHabitatSystemd(o terraform.UIOutput, comm communicator.Communicator, options string) error {
// Create a new template and parse the client config into it
unitString := template.Must(template.New("hab-supervisor.service").Parse(systemdUnit))
var buf bytes.Buffer
err := unitString.Execute(&buf, p)
if err != nil {
return fmt.Errorf("error executing %s.service template: %s", p.ServiceName, err)
}
if err := p.linuxUploadSystemdUnit(o, comm, &buf); err != nil {
return err
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("systemctl enable %s && systemctl start %s", p.ServiceName, p.ServiceName)))
}
func (p *provisioner) linuxUploadSystemdUnit(o terraform.UIOutput, comm communicator.Communicator, contents *bytes.Buffer) error {
destination := fmt.Sprintf("/etc/systemd/system/%s.service", p.ServiceName)
if p.UseSudo {
tempPath := fmt.Sprintf("/tmp/%s.service", p.ServiceName)
if err := comm.Upload(tempPath, contents); err != nil {
return err
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("mv %s %s", tempPath, destination)))
}
return comm.Upload(destination, contents)
}
func (p *provisioner) linuxUploadRingKey(o terraform.UIOutput, comm communicator.Communicator) error {
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf(`echo -e "%s" | hab ring key import`, p.RingKeyContent)))
}
func (p *provisioner) linuxUploadCtlSecret(o terraform.UIOutput, comm communicator.Communicator) error {
destination := fmt.Sprintf("/hab/sup/default/CTL_SECRET")
// Create the destination directory
err := p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("mkdir -p %s", filepath.Dir(destination))))
if err != nil {
return err
}
keyContent := strings.NewReader(p.CtlSecret)
if p.UseSudo {
tempPath := fmt.Sprintf("/tmp/CTL_SECRET")
if err := comm.Upload(tempPath, keyContent); err != nil {
return err
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("mv %s %s && chown root:root %s && chmod 0600 %s", tempPath, destination, destination, destination)))
}
return comm.Upload(destination, keyContent)
}
//
// Habitat Services
//
func (p *provisioner) linuxStartHabitatService(o terraform.UIOutput, comm communicator.Communicator, service Service) error {
var options string
if err := p.linuxInstallHabitatPackage(o, comm, service); err != nil {
return err
}
if err := p.uploadUserTOML(o, comm, service); err != nil {
return err
}
// Upload service group key
if service.ServiceGroupKey != "" {
err := p.uploadServiceGroupKey(o, comm, service.ServiceGroupKey)
if err != nil {
return err
}
}
if service.Topology != "" {
options += fmt.Sprintf(" --topology %s", service.Topology)
}
if service.Strategy != "" {
options += fmt.Sprintf(" --strategy %s", service.Strategy)
}
if service.Channel != "" {
options += fmt.Sprintf(" --channel %s", service.Channel)
}
if service.URL != "" {
options += fmt.Sprintf(" --url %s", service.URL)
}
if service.Group != "" {
options += fmt.Sprintf(" --group %s", service.Group)
}
for _, bind := range service.Binds {
options += fmt.Sprintf(" --bind %s", bind.toBindString())
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("hab svc load %s %s", service.Name, options)))
}
// In the future we'll remove the dedicated install once the synchronous load feature in hab-sup is
// available. Until then we install here to provide output and a noisy failure mechanism because
// if you install with the pkg load, it occurs asynchronously and fails quietly.
func (p *provisioner) linuxInstallHabitatPackage(o terraform.UIOutput, comm communicator.Communicator, service Service) error {
var options string
if service.Channel != "" {
options += fmt.Sprintf(" --channel %s", service.Channel)
}
if service.URL != "" {
options += fmt.Sprintf(" --url %s", service.URL)
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("hab pkg install %s %s", service.Name, options)))
}
func (p *provisioner) uploadServiceGroupKey(o terraform.UIOutput, comm communicator.Communicator, key string) error {
keyName := strings.Split(key, "\n")[1]
o.Output("Uploading service group key: " + keyName)
keyFileName := fmt.Sprintf("%s.box.key", keyName)
destPath := path.Join("/hab/cache/keys", keyFileName)
keyContent := strings.NewReader(key)
if p.UseSudo {
tempPath := path.Join("/tmp", keyFileName)
if err := comm.Upload(tempPath, keyContent); err != nil {
return err
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("mv %s %s", tempPath, destPath)))
}
return comm.Upload(destPath, keyContent)
}
func (p *provisioner) uploadUserTOML(o terraform.UIOutput, comm communicator.Communicator, service Service) error {
// Create the hab svc directory to lay down the user.toml before loading the service
o.Output("Uploading user.toml for service: " + service.Name)
destDir := fmt.Sprintf("/hab/user/%s/config", service.getPackageName(service.Name))
command := p.linuxGetCommand(fmt.Sprintf("mkdir -p %s", destDir))
if err := p.runCommand(o, comm, command); err != nil {
return err
}
userToml := strings.NewReader(service.UserTOML)
if p.UseSudo {
if err := comm.Upload(fmt.Sprintf("/tmp/user-%s.toml", service.getServiceNameChecksum()), userToml); err != nil {
return err
}
command = p.linuxGetCommand(fmt.Sprintf("mv /tmp/user-%s.toml %s/user.toml", service.getServiceNameChecksum(), destDir))
return p.runCommand(o, comm, command)
}
return comm.Upload(path.Join(destDir, "user.toml"), userToml)
}
func (p *provisioner) linuxGetCommand(command string) string {
// Always set HAB_NONINTERACTIVE & HAB_NOCOLORING
env := fmt.Sprintf("env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true")
// Set builder auth token
if p.BuilderAuthToken != "" {
env += fmt.Sprintf(" HAB_AUTH_TOKEN=%s", p.BuilderAuthToken)
}
if p.UseSudo {
command = fmt.Sprintf("%s sudo -E /bin/bash -c '%s'", env, command)
} else {
command = fmt.Sprintf("%s /bin/bash -c '%s'", env, command)
}
return command
}

View File

@ -0,0 +1,348 @@
package habitat
import (
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
"testing"
)
const linuxDefaultSystemdUnitFileContents = `[Unit]
Description=Habitat Supervisor
[Service]
ExecStart=/bin/hab sup run --peer host1 --peer 1.2.3.4 --auto-update
Restart=on-failure
[Install]
WantedBy=default.target`
const linuxCustomSystemdUnitFileContents = `[Unit]
Description=Habitat Supervisor
[Service]
ExecStart=/bin/hab sup run --listen-ctl 192.168.0.1:8443 --listen-gossip 192.168.10.1:9443 --listen-http 192.168.20.1:8080 --peer host1 --peer host2 --peer 1.2.3.4 --peer 5.6.7.8 --peer foo.example.com
Restart=on-failure
Environment="HAB_SUP_GATEWAY_AUTH_TOKEN=ea7-beef"
Environment="HAB_AUTH_TOKEN=dead-beef"
[Install]
WantedBy=default.target`
func TestLinuxProvisioner_linuxInstallHabitat(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Commands map[string]bool
}{
"Installation with sudo": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": true,
"use_sudo": true,
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'curl --silent -L0 https://raw.githubusercontent.com/habitat-sh/habitat/master/components/hab/install.sh > install.sh'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'bash ./install.sh -v 0.79.1'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab install core/busybox'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab pkg exec core/busybox adduser -D -g \"\" hab'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'rm -f install.sh'": true,
},
},
"Installation without sudo": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": true,
"use_sudo": false,
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'curl --silent -L0 https://raw.githubusercontent.com/habitat-sh/habitat/master/components/hab/install.sh > install.sh'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'bash ./install.sh -v 0.79.1'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'hab install core/busybox'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'hab pkg exec core/busybox adduser -D -g \"\" hab'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'rm -f install.sh'": true,
},
},
"Installation with Habitat license acceptance": {
Config: map[string]interface{}{
"version": "0.81.0",
"accept_license": true,
"auto_update": true,
"use_sudo": true,
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'curl --silent -L0 https://raw.githubusercontent.com/habitat-sh/habitat/master/components/hab/install.sh > install.sh'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'bash ./install.sh -v 0.81.0'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab install core/busybox'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab pkg exec core/busybox adduser -D -g \"\" hab'": true,
"env HAB_LICENSE=accept sudo -E /bin/bash -c 'hab -V'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'rm -f install.sh'": true,
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
err = p.linuxInstallHabitat(o, c)
if err != nil {
t.Fatalf("Test %q failed: %v", k, err)
}
}
}
func TestLinuxProvisioner_linuxStartHabitat(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Commands map[string]bool
Uploads map[string]string
}{
"Start systemd Habitat with sudo": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": true,
"use_sudo": true,
"service_name": "hab-sup",
"peer": "--peer host1",
"peers": []interface{}{"1.2.3.4"},
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab install core/hab-sup/0.79.1'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'systemctl enable hab-sup && systemctl start hab-sup'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'mv /tmp/hab-sup.service /etc/systemd/system/hab-sup.service'": true,
},
Uploads: map[string]string{
"/tmp/hab-sup.service": linuxDefaultSystemdUnitFileContents,
},
},
"Start systemd Habitat without sudo": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": true,
"use_sudo": false,
"service_name": "hab-sup",
"peer": "--peer host1",
"peers": []interface{}{"1.2.3.4"},
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'hab install core/hab-sup/0.79.1'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'systemctl enable hab-sup && systemctl start hab-sup'": true,
},
Uploads: map[string]string{
"/etc/systemd/system/hab-sup.service": linuxDefaultSystemdUnitFileContents,
},
},
"Start unmanaged Habitat with sudo": {
Config: map[string]interface{}{
"version": "0.81.0",
"license": "accept-no-persist",
"auto_update": true,
"use_sudo": true,
"service_type": "unmanaged",
"peer": "--peer host1",
"peers": []interface{}{"1.2.3.4"},
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab install core/hab-sup/0.81.0'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'mkdir -p /hab/sup/default && chmod o+w /hab/sup/default'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c '(setsid hab sup run --peer host1 --peer 1.2.3.4 --auto-update > /hab/sup/default/sup.log 2>&1 <&1 &) ; sleep 1'": true,
},
Uploads: map[string]string{
"/etc/systemd/system/hab-sup.service": linuxDefaultSystemdUnitFileContents,
},
},
"Start Habitat with custom config": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": false,
"use_sudo": true,
"service_name": "hab-sup",
"peer": "--peer host1 --peer host2",
"peers": []interface{}{"1.2.3.4", "5.6.7.8", "foo.example.com"},
"listen_ctl": "192.168.0.1:8443",
"listen_gossip": "192.168.10.1:9443",
"listen_http": "192.168.20.1:8080",
"builder_auth_token": "dead-beef",
"gateway_auth_token": "ea7-beef",
"ctl_secret": "bad-beef",
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true HAB_AUTH_TOKEN=dead-beef sudo -E /bin/bash -c 'hab install core/hab-sup/0.79.1'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true HAB_AUTH_TOKEN=dead-beef sudo -E /bin/bash -c 'systemctl enable hab-sup && systemctl start hab-sup'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true HAB_AUTH_TOKEN=dead-beef sudo -E /bin/bash -c 'mv /tmp/hab-sup.service /etc/systemd/system/hab-sup.service'": true,
},
Uploads: map[string]string{
"/tmp/hab-sup.service": linuxCustomSystemdUnitFileContents,
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
c.Uploads = tc.Uploads
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
err = p.linuxStartHabitat(o, c)
if err != nil {
t.Fatalf("Test %q failed: %v", k, err)
}
}
}
func TestLinuxProvisioner_linuxUploadRingKey(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Commands map[string]bool
}{
"Upload ring key": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": true,
"use_sudo": true,
"service_name": "hab-sup",
"peers": []interface{}{"1.2.3.4"},
"ring_key": "test-ring",
"ring_key_content": "dead-beef",
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'echo -e \"dead-beef\" | hab ring key import'": true,
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
err = p.linuxUploadRingKey(o, c)
if err != nil {
t.Fatalf("Test %q failed: %v", k, err)
}
}
}
func TestLinuxProvisioner_linuxStartHabitatService(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Commands map[string]bool
Uploads map[string]string
}{
"Start Habitat service with sudo": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": false,
"use_sudo": true,
"service_name": "hab-sup",
"peers": []interface{}{"1.2.3.4"},
"ring_key": "test-ring",
"ring_key_content": "dead-beef",
"service": []interface{}{
map[string]interface{}{
"name": "core/foo",
"topology": "standalone",
"strategy": "none",
"channel": "stable",
"user_toml": "[config]\nlisten = 0.0.0.0:8080",
"bind": []interface{}{
map[string]interface{}{
"alias": "backend",
"service": "bar",
"group": "default",
},
},
},
map[string]interface{}{
"name": "core/bar",
"topology": "standalone",
"strategy": "rolling",
"channel": "staging",
"user_toml": "[config]\nlisten = 0.0.0.0:443",
},
},
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab pkg install core/foo --channel stable'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'mkdir -p /hab/user/foo/config'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'mv /tmp/user-a5b83ec1b302d109f41852ae17379f75c36dff9bc598aae76b6f7c9cd425fd76.toml /hab/user/foo/config/user.toml'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab svc load core/foo --topology standalone --strategy none --channel stable --bind backend:bar.default'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab pkg install core/bar --channel staging'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'mkdir -p /hab/user/bar/config'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'mv /tmp/user-6466ae3283ae1bd4737b00367bc676c6465b25682169ea5f7da222f3f078a5bf.toml /hab/user/bar/config/user.toml'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab svc load core/bar --topology standalone --strategy rolling --channel staging'": true,
},
Uploads: map[string]string{
"/tmp/user-a5b83ec1b302d109f41852ae17379f75c36dff9bc598aae76b6f7c9cd425fd76.toml": "[config]\nlisten = 0.0.0.0:8080",
"/tmp/user-6466ae3283ae1bd4737b00367bc676c6465b25682169ea5f7da222f3f078a5bf.toml": "[config]\nlisten = 0.0.0.0:443",
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
c.Uploads = tc.Uploads
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
var errs []error
for _, s := range p.Services {
err = p.linuxStartHabitatService(o, c, s)
if err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
for _, e := range errs {
t.Logf("Test %q failed: %v", k, e)
t.Fail()
}
}
}
}

View File

@ -1,55 +1,38 @@
package habitat
import (
"bytes"
"context"
"crypto/sha256"
"errors"
"fmt"
"io"
"net/url"
"path"
"strings"
"text/template"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/configs/hcl2shim"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
"github.com/hashicorp/terraform/terraform"
linereader "github.com/mitchellh/go-linereader"
"github.com/mitchellh/go-linereader"
)
const installURL = "https://raw.githubusercontent.com/habitat-sh/habitat/master/components/hab/install.sh"
const systemdUnit = `
[Unit]
Description=Habitat Supervisor
[Service]
ExecStart=/bin/hab sup run {{ .SupOptions }}
Restart=on-failure
{{ if .BuilderAuthToken -}}
Environment="HAB_AUTH_TOKEN={{ .BuilderAuthToken }}"
{{ end -}}
[Install]
WantedBy=default.target
`
var serviceTypes = map[string]bool{"unmanaged": true, "systemd": true}
var updateStrategies = map[string]bool{"at-once": true, "rolling": true, "none": true}
var topologies = map[string]bool{"leader": true, "standalone": true}
type provisionFn func(terraform.UIOutput, communicator.Communicator) error
type provisioner struct {
Version string
AutoUpdate bool
HttpDisable bool
Services []Service
PermanentPeer bool
ListenCtl string
ListenGossip string
ListenHTTP string
Peer string
Peers []string
RingKey string
RingKeyContent string
CtlSecret string
SkipInstall bool
UseSudo bool
ServiceType string
@ -57,13 +40,24 @@ type provisioner struct {
URL string
Channel string
Events string
OverrideName string
Organization string
GatewayAuthToken string
BuilderAuthToken string
SupOptions string
AcceptLicense bool
installHabitat provisionFn
startHabitat provisionFn
uploadRingKey provisionFn
uploadCtlSecret provisionFn
startHabitatService provisionServiceFn
osType string
}
type provisionFn func(terraform.UIOutput, communicator.Communicator) error
type provisionServiceFn func(terraform.UIOutput, communicator.Communicator, Service) error
func Provisioner() terraform.ResourceProvisioner {
return &schema.Provisioner{
Schema: map[string]*schema.Schema{
@ -71,14 +65,30 @@ func Provisioner() terraform.ResourceProvisioner {
Type: schema.TypeString,
Optional: true,
},
"auto_update": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"http_disable": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"peer": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"service_type": &schema.Schema{
Type: schema.TypeString,
"peers": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
Default: "systemd",
},
"service_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "systemd",
ValidateFunc: validation.StringInSlice([]string{"systemd", "unmanaged"}, false),
},
"service_name": &schema.Schema{
Type: schema.TypeString,
@ -99,6 +109,10 @@ func Provisioner() terraform.ResourceProvisioner {
Optional: true,
Default: false,
},
"listen_ctl": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"listen_gossip": &schema.Schema{
Type: schema.TypeString,
Optional: true,
@ -115,9 +129,25 @@ func Provisioner() terraform.ResourceProvisioner {
Type: schema.TypeString,
Optional: true,
},
"ctl_secret": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"url": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) {
u, err := url.Parse(val.(string))
if err != nil {
errs = append(errs, fmt.Errorf("invalid URL specified for %q: %v", key, err))
}
if u.Scheme == "" {
errs = append(errs, fmt.Errorf("invalid URL specified for %q (scheme must be specified)", key))
}
return warns, errs
},
},
"channel": &schema.Schema{
Type: schema.TypeString,
@ -127,11 +157,11 @@ func Provisioner() terraform.ResourceProvisioner {
Type: schema.TypeString,
Optional: true,
},
"override_name": &schema.Schema{
"organization": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"organization": &schema.Schema{
"gateway_auth_token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
@ -173,19 +203,20 @@ func Provisioner() terraform.ResourceProvisioner {
Optional: true,
},
"topology": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"leader", "standalone"}, false),
},
"user_toml": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"strategy": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"none", "rolling", "at-once"}, false),
},
"channel": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
@ -196,6 +227,18 @@ func Provisioner() terraform.ResourceProvisioner {
"url": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) {
u, err := url.Parse(val.(string))
if err != nil {
errs = append(errs, fmt.Errorf("invalid URL specified for %q: %v", key, err))
}
if u.Scheme == "" {
errs = append(errs, fmt.Errorf("invalid URL specified for %q (scheme must be specified)", key))
}
return warns, errs
},
},
"application": &schema.Schema{
Type: schema.TypeString,
@ -205,10 +248,6 @@ func Provisioner() terraform.ResourceProvisioner {
Type: schema.TypeString,
Optional: true,
},
"override_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"service_key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
@ -233,6 +272,30 @@ func applyFn(ctx context.Context) error {
return err
}
// Automatically determine the OS type
switch t := s.Ephemeral.ConnInfo["type"]; t {
case "ssh", "":
p.osType = "linux"
case "winrm":
p.osType = "windows"
default:
return fmt.Errorf("unsupported connection type: %s", t)
}
switch p.osType {
case "linux":
p.installHabitat = p.linuxInstallHabitat
p.uploadRingKey = p.linuxUploadRingKey
p.uploadCtlSecret = p.linuxUploadCtlSecret
p.startHabitat = p.linuxStartHabitat
p.startHabitatService = p.linuxStartHabitatService
case "windows":
return fmt.Errorf("windows is not supported yet for the habitat provisioner")
default:
return fmt.Errorf("unsupported os type: %s", p.osType)
}
// Get a new communicator
comm, err := communicator.New(s)
if err != nil {
return err
@ -241,6 +304,7 @@ func applyFn(ctx context.Context) error {
retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
defer cancel()
// Wait and retry until we establish the connection
err = communicator.Retry(retryCtx, func() error {
return comm.Connect(o)
})
@ -252,7 +316,7 @@ func applyFn(ctx context.Context) error {
if !p.SkipInstall {
o.Output("Installing habitat...")
if err := p.installHab(o, comm); err != nil {
if err := p.installHabitat(o, comm); err != nil {
return err
}
}
@ -264,15 +328,22 @@ func applyFn(ctx context.Context) error {
}
}
if p.CtlSecret != "" {
o.Output("Uploading ctl secret...")
if err := p.uploadCtlSecret(o, comm); err != nil {
return err
}
}
o.Output("Starting the habitat supervisor...")
if err := p.startHab(o, comm); err != nil {
if err := p.startHabitat(o, comm); err != nil {
return err
}
if p.Services != nil {
for _, service := range p.Services {
o.Output("Starting service: " + service.Name)
if err := p.startHabService(o, comm, service); err != nil {
if err := p.startHabitatService(o, comm, service); err != nil {
return err
}
}
@ -282,17 +353,11 @@ func applyFn(ctx context.Context) error {
}
func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
serviceType, ok := c.Get("service_type")
if ok {
if !serviceTypes[serviceType.(string)] {
es = append(es, errors.New(serviceType.(string)+" is not a valid service_type."))
}
}
builderURL, ok := c.Get("url")
if ok {
if _, err := url.ParseRequestURI(builderURL.(string)); err != nil {
es = append(es, errors.New(builderURL.(string)+" is not a valid URL."))
ringKeyContent, ok := c.Get("ring_key_content")
if ok && ringKeyContent != "" && ringKeyContent != hcl2shim.UnknownVariableValue {
ringKey, ringOk := c.Get("ring_key")
if ringOk && ringKey == "" {
es = append(es, errors.New("if ring_key_content is specified, ring_key must be specified as well"))
}
}
@ -319,30 +384,12 @@ func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
// Validate service level configs
services, ok := c.Get("service")
if ok {
for i, svc := range services.([]interface{}) {
service, ok := svc.(map[string]interface{})
if !ok {
es = append(es, fmt.Errorf("service %d: must be a block", i))
continue
}
strategy, ok := service["strategy"].(string)
if ok && !updateStrategies[strategy] {
es = append(es, errors.New(strategy+" is not a valid update strategy."))
}
topology, ok := service["topology"].(string)
if ok && !topologies[topology] {
es = append(es, errors.New(topology+" is not a valid topology"))
}
builderURL, ok := service["url"].(string)
if ok {
if _, err := url.ParseRequestURI(builderURL); err != nil {
es = append(es, errors.New(builderURL+" is not a valid URL."))
}
}
data, dataOk := services.(string)
if dataOk {
es = append(es, fmt.Errorf("service '%v': must be a block", data))
}
}
return ws, es
}
@ -358,20 +405,23 @@ type Service struct {
UserTOML string
AppName string
Environment string
OverrideName string
ServiceGroupKey string
}
func (s *Service) getPackageName(fullName string) string {
return strings.Split(fullName, "/")[1]
}
func (s *Service) getServiceNameChecksum() string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(s.Name)))
}
type Bind struct {
Alias string
Service string
Group string
}
func (s *Service) getPackageName(fullName string) string {
return strings.Split(fullName, "/")[1]
}
func (b *Bind) toBindString() string {
return fmt.Sprintf("%s:%s.%s", b.Alias, b.Service, b.Group)
}
@ -379,7 +429,10 @@ func (b *Bind) toBindString() string {
func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
p := &provisioner{
Version: d.Get("version").(string),
AutoUpdate: d.Get("auto_update").(bool),
HttpDisable: d.Get("http_disable").(bool),
Peer: d.Get("peer").(string),
Peers: getPeers(d.Get("peers").([]interface{})),
Services: getServices(d.Get("service").(*schema.Set).List()),
UseSudo: d.Get("use_sudo").(bool),
AcceptLicense: d.Get("accept_license").(bool),
@ -387,20 +440,30 @@ func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
ServiceName: d.Get("service_name").(string),
RingKey: d.Get("ring_key").(string),
RingKeyContent: d.Get("ring_key_content").(string),
CtlSecret: d.Get("ctl_secret").(string),
PermanentPeer: d.Get("permanent_peer").(bool),
ListenCtl: d.Get("listen_ctl").(string),
ListenGossip: d.Get("listen_gossip").(string),
ListenHTTP: d.Get("listen_http").(string),
URL: d.Get("url").(string),
Channel: d.Get("channel").(string),
Events: d.Get("events").(string),
OverrideName: d.Get("override_name").(string),
Organization: d.Get("organization").(string),
BuilderAuthToken: d.Get("builder_auth_token").(string),
GatewayAuthToken: d.Get("gateway_auth_token").(string),
}
return p, nil
}
func getPeers(v []interface{}) []string {
peers := make([]string, 0, len(v))
for _, rawPeerData := range v {
peers = append(peers, rawPeerData.(string))
}
return peers
}
func getServices(v []interface{}) []Service {
services := make([]Service, 0, len(v))
for _, rawServiceData := range v {
@ -413,7 +476,6 @@ func getServices(v []interface{}) []Service {
url := (serviceData["url"].(string))
app := (serviceData["application"].(string))
env := (serviceData["environment"].(string))
override := (serviceData["override_name"].(string))
userToml := (serviceData["user_toml"].(string))
serviceGroupKey := (serviceData["service_key"].(string))
var bindStrings []string
@ -438,7 +500,6 @@ func getServices(v []interface{}) []Service {
Binds: binds,
AppName: app,
Environment: env,
OverrideName: override,
ServiceGroupKey: serviceGroupKey,
}
services = append(services, service)
@ -463,331 +524,6 @@ func getBinds(v []interface{}) []Bind {
return binds
}
func (p *provisioner) uploadRingKey(o terraform.UIOutput, comm communicator.Communicator) error {
command := fmt.Sprintf("echo '%s' | hab ring key import", p.RingKeyContent)
if p.UseSudo {
command = fmt.Sprintf("echo '%s' | sudo hab ring key import", p.RingKeyContent)
}
return p.runCommand(o, comm, command)
}
func (p *provisioner) installHab(o terraform.UIOutput, comm communicator.Communicator) error {
// Build the install command
command := fmt.Sprintf("curl -L0 %s > install.sh", installURL)
if err := p.runCommand(o, comm, command); err != nil {
return err
}
// Run the install script
if p.Version == "" {
command = fmt.Sprintf("env HAB_NONINTERACTIVE=true bash ./install.sh ")
} else {
command = fmt.Sprintf("env HAB_NONINTERACTIVE=true bash ./install.sh -v %s", p.Version)
}
if p.UseSudo {
command = fmt.Sprintf("sudo %s", command)
}
if err := p.runCommand(o, comm, command); err != nil {
return err
}
// Accept the license
if p.AcceptLicense {
command = fmt.Sprintf("export HAB_LICENSE=accept; hab -V")
if p.UseSudo {
command = fmt.Sprintf("sudo HAB_LICENSE=accept hab -V")
}
if err := p.runCommand(o, comm, command); err != nil {
return err
}
}
if err := p.createHabUser(o, comm); err != nil {
return err
}
return p.runCommand(o, comm, fmt.Sprintf("rm -f install.sh"))
}
func (p *provisioner) startHab(o terraform.UIOutput, comm communicator.Communicator) error {
// Install the supervisor first
var command string
if p.Version == "" {
command += fmt.Sprintf("hab install core/hab-sup")
} else {
command += fmt.Sprintf("hab install core/hab-sup/%s", p.Version)
}
if p.UseSudo {
command = fmt.Sprintf("sudo -E %s", command)
}
command = fmt.Sprintf("env HAB_NONINTERACTIVE=true %s", command)
if err := p.runCommand(o, comm, command); err != nil {
return err
}
// Build up sup options
options := ""
if p.PermanentPeer {
options += " -I"
}
if p.ListenGossip != "" {
options += fmt.Sprintf(" --listen-gossip %s", p.ListenGossip)
}
if p.ListenHTTP != "" {
options += fmt.Sprintf(" --listen-http %s", p.ListenHTTP)
}
if p.Peer != "" {
options += fmt.Sprintf(" --peer %s", p.Peer)
}
if p.RingKey != "" {
options += fmt.Sprintf(" --ring %s", p.RingKey)
}
if p.URL != "" {
options += fmt.Sprintf(" --url %s", p.URL)
}
if p.Channel != "" {
options += fmt.Sprintf(" --channel %s", p.Channel)
}
if p.Events != "" {
options += fmt.Sprintf(" --events %s", p.Events)
}
if p.OverrideName != "" {
options += fmt.Sprintf(" --override-name %s", p.OverrideName)
}
if p.Organization != "" {
options += fmt.Sprintf(" --org %s", p.Organization)
}
p.SupOptions = options
switch p.ServiceType {
case "unmanaged":
return p.startHabUnmanaged(o, comm, options)
case "systemd":
return p.startHabSystemd(o, comm, options)
default:
return errors.New("Unsupported service type")
}
}
func (p *provisioner) startHabUnmanaged(o terraform.UIOutput, comm communicator.Communicator, options string) error {
// Create the sup directory for the log file
var command string
var token string
if p.UseSudo {
command = "sudo mkdir -p /hab/sup/default && sudo chmod o+w /hab/sup/default"
} else {
command = "mkdir -p /hab/sup/default && chmod o+w /hab/sup/default"
}
if err := p.runCommand(o, comm, command); err != nil {
return err
}
if p.BuilderAuthToken != "" {
token = fmt.Sprintf("env HAB_AUTH_TOKEN=%s", p.BuilderAuthToken)
}
if p.UseSudo {
command = fmt.Sprintf("(%s setsid sudo -E hab sup run %s > /hab/sup/default/sup.log 2>&1 &) ; sleep 1", token, options)
} else {
command = fmt.Sprintf("(%s setsid hab sup run %s > /hab/sup/default/sup.log 2>&1 <&1 &) ; sleep 1", token, options)
}
return p.runCommand(o, comm, command)
}
func (p *provisioner) startHabSystemd(o terraform.UIOutput, comm communicator.Communicator, options string) error {
// Create a new template and parse the client config into it
unitString := template.Must(template.New("hab-supervisor.service").Parse(systemdUnit))
var buf bytes.Buffer
err := unitString.Execute(&buf, p)
if err != nil {
return fmt.Errorf("Error executing %s template: %s", "hab-supervisor.service", err)
}
var command string
if p.UseSudo {
command = fmt.Sprintf("sudo echo '%s' | sudo tee /etc/systemd/system/%s.service > /dev/null", &buf, p.ServiceName)
} else {
command = fmt.Sprintf("echo '%s' | tee /etc/systemd/system/%s.service > /dev/null", &buf, p.ServiceName)
}
if err := p.runCommand(o, comm, command); err != nil {
return err
}
if p.UseSudo {
command = fmt.Sprintf("sudo systemctl enable hab-supervisor && sudo systemctl start hab-supervisor")
} else {
command = fmt.Sprintf("systemctl enable hab-supervisor && systemctl start hab-supervisor")
}
return p.runCommand(o, comm, command)
}
func (p *provisioner) createHabUser(o terraform.UIOutput, comm communicator.Communicator) error {
addUser := false
// Install busybox to get us the user tools we need
command := fmt.Sprintf("env HAB_NONINTERACTIVE=true hab install core/busybox")
if p.UseSudo {
command = fmt.Sprintf("sudo %s", command)
}
if err := p.runCommand(o, comm, command); err != nil {
return err
}
// Check for existing hab user
command = fmt.Sprintf("hab pkg exec core/busybox id hab")
if p.UseSudo {
command = fmt.Sprintf("sudo %s", command)
}
if err := p.runCommand(o, comm, command); err != nil {
o.Output("No existing hab user detected, creating...")
addUser = true
}
if addUser {
command = fmt.Sprintf("hab pkg exec core/busybox adduser -D -g \"\" hab")
if p.UseSudo {
command = fmt.Sprintf("sudo %s", command)
}
return p.runCommand(o, comm, command)
}
return nil
}
// In the future we'll remove the dedicated install once the synchronous load feature in hab-sup is
// available. Until then we install here to provide output and a noisy failure mechanism because
// if you install with the pkg load, it occurs asynchronously and fails quietly.
func (p *provisioner) installHabPackage(o terraform.UIOutput, comm communicator.Communicator, service Service) error {
var command string
options := ""
if service.Channel != "" {
options += fmt.Sprintf(" --channel %s", service.Channel)
}
if service.URL != "" {
options += fmt.Sprintf(" --url %s", service.URL)
}
if p.UseSudo {
command = fmt.Sprintf("env HAB_NONINTERACTIVE=true sudo -E hab pkg install %s %s", service.Name, options)
} else {
command = fmt.Sprintf("env HAB_NONINTERACTIVE=true hab pkg install %s %s", service.Name, options)
}
if p.BuilderAuthToken != "" {
command = fmt.Sprintf("env HAB_AUTH_TOKEN=%s %s", p.BuilderAuthToken, command)
}
return p.runCommand(o, comm, command)
}
func (p *provisioner) startHabService(o terraform.UIOutput, comm communicator.Communicator, service Service) error {
var command string
if err := p.installHabPackage(o, comm, service); err != nil {
return err
}
if err := p.uploadUserTOML(o, comm, service); err != nil {
return err
}
// Upload service group key
if service.ServiceGroupKey != "" {
p.uploadServiceGroupKey(o, comm, service.ServiceGroupKey)
}
options := ""
if service.Topology != "" {
options += fmt.Sprintf(" --topology %s", service.Topology)
}
if service.Strategy != "" {
options += fmt.Sprintf(" --strategy %s", service.Strategy)
}
if service.Channel != "" {
options += fmt.Sprintf(" --channel %s", service.Channel)
}
if service.URL != "" {
options += fmt.Sprintf(" --url %s", service.URL)
}
if service.Group != "" {
options += fmt.Sprintf(" --group %s", service.Group)
}
for _, bind := range service.Binds {
options += fmt.Sprintf(" --bind %s", bind.toBindString())
}
command = fmt.Sprintf("hab svc load %s %s", service.Name, options)
if p.UseSudo {
command = fmt.Sprintf("sudo -E %s", command)
}
if p.BuilderAuthToken != "" {
command = fmt.Sprintf("env HAB_AUTH_TOKEN=%s %s", p.BuilderAuthToken, command)
}
return p.runCommand(o, comm, command)
}
func (p *provisioner) uploadServiceGroupKey(o terraform.UIOutput, comm communicator.Communicator, key string) error {
keyName := strings.Split(key, "\n")[1]
o.Output("Uploading service group key: " + keyName)
keyFileName := fmt.Sprintf("%s.box.key", keyName)
destPath := path.Join("/hab/cache/keys", keyFileName)
keyContent := strings.NewReader(key)
if p.UseSudo {
tempPath := path.Join("/tmp", keyFileName)
if err := comm.Upload(tempPath, keyContent); err != nil {
return err
}
command := fmt.Sprintf("sudo mv %s %s", tempPath, destPath)
return p.runCommand(o, comm, command)
}
return comm.Upload(destPath, keyContent)
}
func (p *provisioner) uploadUserTOML(o terraform.UIOutput, comm communicator.Communicator, service Service) error {
// Create the hab svc directory to lay down the user.toml before loading the service
o.Output("Uploading user.toml for service: " + service.Name)
destDir := fmt.Sprintf("/hab/svc/%s", service.getPackageName(service.Name))
command := fmt.Sprintf("mkdir -p %s", destDir)
if p.UseSudo {
command = fmt.Sprintf("sudo %s", command)
}
if err := p.runCommand(o, comm, command); err != nil {
return err
}
userToml := strings.NewReader(service.UserTOML)
if p.UseSudo {
if err := comm.Upload("/tmp/user.toml", userToml); err != nil {
return err
}
command = fmt.Sprintf("sudo mv /tmp/user.toml %s", destDir)
return p.runCommand(o, comm, command)
}
return comm.Upload(path.Join(destDir, "user.toml"), userToml)
}
func (p *provisioner) copyOutput(o terraform.UIOutput, r io.Reader) {
lr := linereader.New(r)
for line := range lr.Ch {
@ -811,7 +547,7 @@ func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communi
}
if err := comm.Start(cmd); err != nil {
return fmt.Errorf("Error executing command %q: %v", cmd.Command, err)
return fmt.Errorf("error executing command %q: %v", cmd.Command, err)
}
if err := cmd.Wait(); err != nil {
@ -830,7 +566,7 @@ func getBindFromString(bind string) (Bind, error) {
return false
})
if len(t) != 3 {
return Bind{}, errors.New("Invalid bind specification: " + bind)
return Bind{}, errors.New("invalid bind specification: " + bind)
}
return Bind{Alias: t[0], Service: t[1], Group: t[2]}, nil
}

View File

@ -19,7 +19,7 @@ func TestProvisioner(t *testing.T) {
func TestResourceProvisioner_Validate_good(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"peer": "1.2.3.4",
"peers": []interface{}{"1.2.3.4"},
"version": "0.32.0",
"service_type": "systemd",
"accept_license": false,
@ -37,15 +37,16 @@ func TestResourceProvisioner_Validate_good(t *testing.T) {
func TestResourceProvisioner_Validate_bad(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"service_type": "invalidtype",
"url": "badurl",
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
//Two errors, one for service_type, other for missing required accept_license argument
if len(errs) != 2 {
t.Fatalf("Should have one errors, got %d", len(errs))
// 3 errors, bad service_type, bad url, missing accept_license
if len(errs) != 3 {
t.Fatalf("Should have three errors, got %d", len(errs))
}
}
@ -67,7 +68,21 @@ func TestResourceProvisioner_Validate_bad_service_config(t *testing.T) {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) != 3 {
t.Fatalf("Should have three errors")
t.Fatalf("Should have three errors, got %d", len(errs))
}
}
func TestResourceProvisioner_Validate_bad_service_definition(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"service": "core/vault",
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) != 3 {
t.Fatalf("Should have three errors, got %d", len(errs))
}
}

View File

@ -8,7 +8,7 @@ import (
"path/filepath"
"strings"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/configs/configupgrade"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"

View File

@ -4,8 +4,8 @@ import (
"fmt"
"strings"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/tfdiags"
)

View File

@ -11,9 +11,9 @@ import (
"path/filepath"
"strings"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl2/hclwrite"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/mitchellh/cli"
"github.com/hashicorp/terraform/configs"

View File

@ -7,9 +7,9 @@ import (
"sort"
"strings"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcled"
"github.com/hashicorp/hcl2/hclparse"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcled"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/hashicorp/terraform/tfdiags"
"github.com/mitchellh/colorstring"
wordwrap "github.com/mitchellh/go-wordwrap"

View File

@ -7,8 +7,8 @@ import (
"os"
"strings"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/backend"
@ -178,7 +178,11 @@ func (c *ImportCommand) Run(args []string) int {
// We assume the same module as the resource address here, which
// may get resolved to an inherited provider when we construct the
// import graph inside ctx.Import, called below.
providerAddr = resourceRelAddr.DefaultProviderConfig().Absolute(addr.Module)
if rc != nil && rc.ProviderConfigRef != nil {
providerAddr = rc.ProviderConfigAddr().Absolute(addr.Module)
} else {
providerAddr = resourceRelAddr.DefaultProviderConfig().Absolute(addr.Module)
}
}
// Check for user-supplied plugin path
@ -336,9 +340,10 @@ Options:
-no-color If specified, output won't contain any color.
-provider=provider Specific provider to use for import. This is used for
specifying aliases, such as "aws.eu". Defaults to the
normal provider prefix of the resource being imported.
-provider=provider Deprecated: Override the provider configuration to use
when importing the object. By default, Terraform uses the
provider specified in the configuration for the target
resource, and that is the best behavior in most cases.
-state=PATH Path to the source state file. Defaults to the configured
backend, or "terraform.tfstate"

View File

@ -536,6 +536,99 @@ func TestImport_customProvider(t *testing.T) {
testStateOutput(t, statePath, testImportCustomProviderStr)
}
// This tests behavior when the provider name does not match the implied
// provider name
func TestImport_providerNameMismatch(t *testing.T) {
defer testChdir(t, testFixturePath("import-provider-mismatch"))()
statePath := testTempFile(t)
p := testProvider()
ui := new(cli.MockUi)
c := &ImportCommand{
Meta: Meta{
testingOverrides: &testingOverrides{
ProviderResolver: providers.ResolverFixed(
map[string]providers.Factory{
"test-beta": providers.FactoryFixed(p),
},
),
},
Ui: ui,
},
}
configured := false
p.ConfigureNewFn = func(req providers.ConfigureRequest) providers.ConfigureResponse {
configured = true
cfg := req.Config
if !cfg.Type().HasAttribute("foo") {
return providers.ConfigureResponse{
Diagnostics: tfdiags.Diagnostics{}.Append(fmt.Errorf("configuration has no foo argument")),
}
}
if got, want := cfg.GetAttr("foo"), cty.StringVal("baz"); !want.RawEquals(got) {
return providers.ConfigureResponse{
Diagnostics: tfdiags.Diagnostics{}.Append(fmt.Errorf("foo argument is %#v, but want %#v", got, want)),
}
}
return providers.ConfigureResponse{}
}
p.ImportResourceStateFn = nil
p.ImportResourceStateResponse = providers.ImportResourceStateResponse{
ImportedResources: []providers.ImportedResource{
{
TypeName: "test_instance",
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("yay"),
}),
},
},
}
p.GetSchemaReturn = &terraform.ProviderSchema{
Provider: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {Type: cty.String, Optional: true},
},
},
ResourceTypes: map[string]*configschema.Block{
"test_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true, Computed: true},
},
},
},
}
args := []string{
"-provider", "test-beta",
"-state", statePath,
"test_instance.foo",
"bar",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Verify that the test-beta provider was configured
if !configured {
t.Fatal("Configure should be called")
}
if !p.ImportResourceStateCalled {
t.Fatal("ImportResourceState (provider 'test-beta') should be called")
}
if !p.ReadResourceCalled {
t.Fatal("ReadResource (provider 'test-beta' should be called")
}
testStateOutput(t, statePath, testImportProviderMismatchStr)
}
func TestImport_allowMissingResourceConfig(t *testing.T) {
defer testChdir(t, testFixturePath("import-missing-resource-config"))()
@ -845,3 +938,9 @@ test_instance.foo:
ID = yay
provider = provider.test.alias
`
const testImportProviderMismatchStr = `
test_instance.foo:
ID = yay
provider = provider.test-beta
`

View File

@ -7,7 +7,7 @@ import (
"sort"
"strings"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform-config-inspect/tfconfig"
"github.com/posener/complete"
"github.com/zclconf/go-cty/cty"

View File

@ -3,8 +3,8 @@ package jsonconfig
import (
"encoding/json"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcldec"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/lang"
"github.com/zclconf/go-cty/cty"
@ -64,7 +64,7 @@ func marshalExpressions(body hcl.Body, schema *configschema.Block) expressions {
// need the low-level schema.
lowSchema := hcldec.ImpliedSchema(schema.DecoderSpec())
// (lowSchema is an hcl.BodySchema:
// https://godoc.org/github.com/hashicorp/hcl2/hcl#BodySchema )
// https://godoc.org/github.com/hashicorp/hcl/v2/hcl#BodySchema )
// Use the low-level schema with the body to decode one level We'll just
// ignore any additional content that's not covered by the schema, which

View File

@ -7,8 +7,8 @@ import (
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/configs/configschema"
)

View File

@ -14,8 +14,8 @@ import (
"strings"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcldec"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/command/clistate"
"github.com/hashicorp/terraform/configs"

View File

@ -7,8 +7,8 @@ import (
"path/filepath"
"sort"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform-config-inspect/tfconfig"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/configs/configload"

View File

@ -6,9 +6,9 @@ import (
"os"
"strings"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
hcljson "github.com/hashicorp/hcl2/hcl/json"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
hcljson "github.com/hashicorp/hcl/v2/json"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/terraform"

View File

@ -0,0 +1,7 @@
provider "test-beta" {
foo = "baz"
}
resource "test_instance" "foo" {
provider = "test-beta"
}

View File

@ -230,10 +230,10 @@ Usage: terraform validate [options] [dir]
configuration and not accessing any remote services such as remote state,
provider APIs, etc.
Validate runs checks that verify whether a configuration is
internally-consistent, regardless of any provided variables or existing
state. It is thus primarily useful for general verification of reusable
modules, including correctness of attribute names and value types.
Validate runs checks that verify whether a configuration is syntactically
valid and internally consistent, regardless of any provided variables or
existing state. It is thus primarily useful for general verification of
reusable modules, including correctness of attribute names and value types.
It is safe to run this command automatically, for example as a post-save
check in a text editor or as a test step for a re-usable module in a CI
@ -247,14 +247,16 @@ Usage: terraform validate [options] [dir]
If dir is not specified, then the current directory will be used.
To verify configuration in the context of a particular run (a particular
target workspace, operation variables, etc), use the terraform plan
subcommand instead, which includes an implied validation check.
target workspace, input variable values, etc), use the 'terraform plan'
command instead, which includes an implied validation check.
Options:
-json Produce output in a machine-readable JSON format, suitable for
use in e.g. text editor integrations.
use in text editor integrations and other automated systems.
Always disables color.
-no-color If specified, output won't contain any color.
`
return strings.TrimSpace(helpText)
}

View File

@ -235,6 +235,10 @@ func (c *Communicator) Connect(o terraform.UIOutput) (err error) {
// Start a keepalive goroutine to help maintain the connection for
// long-running commands.
log.Printf("[DEBUG] starting ssh KeepAlives")
// We want a local copy of the ssh client pointer, so that a reconnect
// doesn't race with the running keep-alive loop.
sshClient := c.client
go func() {
defer cancelKeepAlive()
// Along with the KeepAlives generating packets to keep the tcp
@ -249,7 +253,7 @@ func (c *Communicator) Connect(o terraform.UIOutput) (err error) {
for {
select {
case <-t.C:
_, _, err := c.client.SendRequest("keepalive@terraform.io", true, nil)
_, _, err := sshClient.SendRequest("keepalive@terraform.io", true, nil)
respCh <- err
case <-ctx.Done():
return

View File

@ -8,7 +8,7 @@ import (
"strconv"
"strings"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2 "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/helper/hilmapstructure"
"github.com/hashicorp/terraform/plugin/discovery"

View File

@ -8,7 +8,7 @@ import (
"github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/configs/hcl2shim"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2 "github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/function"

View File

@ -3,8 +3,8 @@ package config
import (
"testing"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2syntax "github.com/hashicorp/hcl2/hcl/hclsyntax"
hcl2 "github.com/hashicorp/hcl/v2"
hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)

View File

@ -5,9 +5,9 @@ import (
"sort"
"strings"
gohcl2 "github.com/hashicorp/hcl2/gohcl"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2parse "github.com/hashicorp/hcl2/hclparse"
hcl2 "github.com/hashicorp/hcl/v2"
gohcl2 "github.com/hashicorp/hcl/v2/gohcl"
hcl2parse "github.com/hashicorp/hcl/v2/hclparse"
"github.com/hashicorp/terraform/configs/hcl2shim"
"github.com/zclconf/go-cty/cty"
)

View File

@ -6,8 +6,8 @@ import (
"github.com/zclconf/go-cty/cty"
gohcl2 "github.com/hashicorp/hcl2/gohcl"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2 "github.com/hashicorp/hcl/v2"
gohcl2 "github.com/hashicorp/hcl/v2/gohcl"
)
func TestHCL2ConfigurableConfigurable(t *testing.T) {

View File

@ -10,7 +10,7 @@ import (
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2 "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast"
"github.com/mitchellh/copystructure"

View File

@ -5,7 +5,7 @@ import (
"reflect"
"testing"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2 "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/configs/hcl2shim"
)

View File

@ -1,8 +1,8 @@
package configs
import (
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcldec"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/zclconf/go-cty/cty"
)

View File

@ -1,8 +1,8 @@
package configs
import (
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)

View File

@ -4,7 +4,7 @@ import (
"sort"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/addrs"
)

View File

@ -4,7 +4,7 @@ import (
"sort"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/addrs"
)

View File

@ -11,7 +11,7 @@ import (
"github.com/davecgh/go-spew/spew"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
)
func TestBuildConfig(t *testing.T) {

View File

@ -4,7 +4,7 @@ import (
"fmt"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/configs"
)

View File

@ -9,7 +9,7 @@ import (
"time"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/internal/modsdir"
"github.com/spf13/afero"

View File

@ -8,7 +8,7 @@ import (
"testing"
"github.com/go-test/deep"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
)

View File

@ -1,7 +1,7 @@
package configschema
import (
"github.com/hashicorp/hcl2/hcldec"
"github.com/hashicorp/hcl/v2/hcldec"
)
var mapLabelNames = []string{"key"}

View File

@ -6,9 +6,9 @@ import (
"github.com/apparentlymart/go-dump/dump"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcldec"
"github.com/hashicorp/hcl2/hcltest"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/zclconf/go-cty/cty"
)

View File

@ -1,7 +1,7 @@
package configschema
import (
"github.com/hashicorp/hcl2/hcldec"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
)

View File

@ -4,8 +4,8 @@ import (
"fmt"
"sort"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/helper/didyoumean"

View File

@ -3,8 +3,8 @@ package configschema
import (
"testing"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)

View File

@ -3,8 +3,8 @@ package configupgrade
import (
"log"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2syntax "github.com/hashicorp/hcl2/hcl/hclsyntax"
hcl2 "github.com/hashicorp/hcl/v2"
hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
@ -75,28 +75,27 @@ func (d analysisData) GetForEachAttr(addr addrs.ForEachAttr, rng tfdiags.SourceR
return cty.DynamicVal, nil
}
func (d analysisData) GetResourceInstance(instAddr addrs.ResourceInstance, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
log.Printf("[TRACE] configupgrade: Determining type for %s", instAddr)
addr := instAddr.Resource
func (d analysisData) GetResource(addr addrs.Resource, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
log.Printf("[TRACE] configupgrade: Determining type for %s", addr)
// Our analysis pass should've found a suitable schema for every resource
// type in the module.
providerType, ok := d.an.ResourceProviderType[addr]
if !ok {
// Should not be possible, since analysis visits every resource block.
log.Printf("[TRACE] configupgrade: analysis.GetResourceInstance doesn't have a provider type for %s", addr)
log.Printf("[TRACE] configupgrade: analysis.GetResource doesn't have a provider type for %s", addr)
return cty.DynamicVal, nil
}
providerSchema, ok := d.an.ProviderSchemas[providerType]
if !ok {
// Should not be possible, since analysis loads schema for every provider.
log.Printf("[TRACE] configupgrade: analysis.GetResourceInstance doesn't have a provider schema for for %q", providerType)
log.Printf("[TRACE] configupgrade: analysis.GetResource doesn't have a provider schema for for %q", providerType)
return cty.DynamicVal, nil
}
schema, _ := providerSchema.SchemaForResourceAddr(addr)
if schema == nil {
// Should not be possible, since analysis loads schema for every provider.
log.Printf("[TRACE] configupgrade: analysis.GetResourceInstance doesn't have a schema for for %s", addr)
log.Printf("[TRACE] configupgrade: analysis.GetResource doesn't have a schema for for %s", addr)
return cty.DynamicVal, nil
}
@ -106,19 +105,11 @@ func (d analysisData) GetResourceInstance(instAddr addrs.ResourceInstance, rng t
// return a list or a single object type depending on whether count is
// set and whether an instance key is given in the address.
if d.an.ResourceHasCount[addr] {
if instAddr.Key == addrs.NoKey {
log.Printf("[TRACE] configupgrade: %s refers to counted instance without a key, so result is a list of %#v", instAddr, objTy)
return cty.UnknownVal(cty.List(objTy)), nil
}
log.Printf("[TRACE] configupgrade: %s refers to counted instance with a key, so result is single object", instAddr)
return cty.UnknownVal(objTy), nil
log.Printf("[TRACE] configupgrade: %s refers to counted instance, so result is a list of %#v", addr, objTy)
return cty.UnknownVal(cty.List(objTy)), nil
}
if instAddr.Key != addrs.NoKey {
log.Printf("[TRACE] configupgrade: %s refers to non-counted instance with a key, which is invalid", instAddr)
return cty.DynamicVal, nil
}
log.Printf("[TRACE] configupgrade: %s refers to non-counted instance without a key, so result is single object", instAddr)
log.Printf("[TRACE] configupgrade: %s refers to non-counted instance, so result is single object", addr)
return cty.UnknownVal(objTy), nil
}

View File

@ -11,8 +11,8 @@ import (
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/tfdiags"
"github.com/hashicorp/hcl2/hcl"
hcl2syntax "github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax"
version "github.com/hashicorp/go-version"
)

View File

@ -4,7 +4,7 @@ import (
"reflect"
"testing"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
)
func TestMaybeAlreadyUpgraded(t *testing.T) {

View File

@ -6,8 +6,8 @@ import (
"github.com/hashicorp/terraform/tfdiags"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2write "github.com/hashicorp/hcl2/hclwrite"
hcl2 "github.com/hashicorp/hcl/v2"
hcl2write "github.com/hashicorp/hcl/v2/hclwrite"
)
// Upgrade takes some input module sources and produces a new ModuleSources

View File

@ -11,8 +11,8 @@ import (
hcl1ast "github.com/hashicorp/hcl/hcl/ast"
hcl1token "github.com/hashicorp/hcl/hcl/token"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2syntax "github.com/hashicorp/hcl2/hcl/hclsyntax"
hcl2 "github.com/hashicorp/hcl/v2"
hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/lang/blocktoattr"
"github.com/hashicorp/terraform/registry/regsrc"

View File

@ -7,8 +7,8 @@ import (
"strconv"
"strings"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2syntax "github.com/hashicorp/hcl2/hcl/hclsyntax"
hcl2 "github.com/hashicorp/hcl/v2"
hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
hcl1ast "github.com/hashicorp/hcl/hcl/ast"

View File

@ -16,7 +16,7 @@ import (
hcl1printer "github.com/hashicorp/hcl/hcl/printer"
hcl1token "github.com/hashicorp/hcl/hcl/token"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2 "github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"

View File

@ -1,7 +1,7 @@
package configs
import (
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
)
func decodeDependsOn(attr *hcl.Attribute) ([]hcl.Traversal, hcl.Diagnostics) {

View File

@ -3,7 +3,7 @@ package hcl2shim
import (
"fmt"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2 "github.com/hashicorp/hcl/v2"
)
// SingleAttrBody is a weird implementation of hcl2.Body that acts as if

View File

@ -3,7 +3,7 @@ package configs
import (
"fmt"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/addrs"
)

View File

@ -3,9 +3,9 @@ package configs
import (
"fmt"
"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
// ModuleCall represents a "module" block in a module or file.

View File

@ -5,7 +5,7 @@ import (
"testing"
"github.com/go-test/deep"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
)
func TestLoadModuleCall(t *testing.T) {

View File

@ -5,7 +5,7 @@ import (
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)

View File

@ -1,7 +1,7 @@
package configs
import (
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
)
// MergeBodies creates a new HCL body that contains a combination of the

View File

@ -3,8 +3,8 @@ package configs
import (
"testing"
"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/zclconf/go-cty/cty"
)

View File

@ -3,10 +3,10 @@ package configs
import (
"fmt"
"github.com/hashicorp/hcl2/ext/typeexpr"
"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/typeexpr"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"

View File

@ -4,8 +4,8 @@ import (
"fmt"
"strings"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hclparse"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/spf13/afero"
)

View File

@ -1,7 +1,7 @@
package configs
import (
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
)
// LoadConfigFile reads the file at the given path and parses it as a config

View File

@ -6,7 +6,7 @@ import (
"path/filepath"
"strings"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
)
// LoadConfigDir reads the .tf and .tf.json files in the given directory

View File

@ -5,7 +5,7 @@ import (
"path/filepath"
"testing"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
)
// TestParseLoadConfigFileSuccess is a simple test that just verifies that

View File

@ -8,7 +8,7 @@ import (
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/spf13/afero"
)

View File

@ -1,7 +1,7 @@
package configs
import (
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
)

View File

@ -3,9 +3,9 @@ package configs
import (
"fmt"
"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/addrs"
)

View File

@ -3,7 +3,7 @@ package configs
import (
"fmt"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
)
// Provisioner represents a "provisioner" block when used within a

View File

@ -3,9 +3,9 @@ package configs
import (
"fmt"
"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/addrs"
)

View File

@ -3,8 +3,8 @@ package configs
import (
"fmt"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)

View File

@ -3,7 +3,7 @@ package configs
import (
"testing"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
)

View File

@ -1,8 +1,8 @@
package configs
import (
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
// exprIsNativeQuotedString determines whether the given expression looks like

View File

@ -4,7 +4,7 @@ import (
"fmt"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)

View File

@ -124,13 +124,13 @@ representing the entire configuration.
Terraform expects configuration files written in the Terraform language, which
is a DSL built on top of
[HCL](https://github.com/hashicorp/hcl2). Some parts of the configuration
[HCL](https://github.com/hashicorp/hcl). Some parts of the configuration
cannot be interpreted until we build and walk the graph, since they depend
on the outcome of other parts of the configuration, and so these parts of
the configuration remain represented as the low-level HCL types
[hcl.Body](https://godoc.org/github.com/hashicorp/hcl2/hcl#Body)
[hcl.Body](https://godoc.org/github.com/hashicorp/hcl/v2/hcl#Body)
and
[hcl.Expression](https://godoc.org/github.com/hashicorp/hcl2/hcl#Expression),
[hcl.Expression](https://godoc.org/github.com/hashicorp/hcl/v2/hcl#Expression),
allowing Terraform to interpret them at a more appropriate time.
## State Manager

6
go.mod
View File

@ -64,11 +64,11 @@ require (
github.com/hashicorp/go-retryablehttp v0.5.2
github.com/hashicorp/go-rootcerts v1.0.0
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 // indirect
github.com/hashicorp/go-tfe v0.3.16
github.com/hashicorp/go-tfe v0.3.23
github.com/hashicorp/go-uuid v1.0.1
github.com/hashicorp/go-version v1.1.0
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f
github.com/hashicorp/hcl2 v0.0.0-20190821123243-0c888d1241f6
github.com/hashicorp/hcl/v2 v2.0.0
github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590
github.com/hashicorp/logutils v1.0.0
github.com/hashicorp/memberlist v0.1.0 // indirect
@ -119,7 +119,7 @@ require (
github.com/xanzy/ssh-agent v0.2.1
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect
github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557
github.com/zclconf/go-cty v1.0.1-0.20190708163926-19588f92a98f
github.com/zclconf/go-cty v1.1.0
github.com/zclconf/go-cty-yaml v1.0.1
go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect

10
go.sum
View File

@ -198,8 +198,8 @@ github.com/hashicorp/go-slug v0.3.0 h1:L0c+AvH/J64iMNF4VqRaRku2DMTEuHioPVS7kMjWI
github.com/hashicorp/go-slug v0.3.0/go.mod h1:I5tq5Lv0E2xcNXNkmx7BSfzi1PsJ2cNjs3cC3LwyhK8=
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 h1:7YOlAIO2YWnJZkQp7B5eFykaIY7C9JndqAFQyVV5BhM=
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-tfe v0.3.16 h1:GS2yv580p0co4j3FBVaC6Zahd9mxdCGehhJ0qqzFMH0=
github.com/hashicorp/go-tfe v0.3.16/go.mod h1:SuPHR+OcxvzBZNye7nGPfwZTEyd3rWPfLVbCgyZPezM=
github.com/hashicorp/go-tfe v0.3.23 h1:kd9hlFQvGubNF/CpF7T5AP/xU8uLUq8ANbI5xRDVSms=
github.com/hashicorp/go-tfe v0.3.23/go.mod h1:SuPHR+OcxvzBZNye7nGPfwZTEyd3rWPfLVbCgyZPezM=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
@ -210,6 +210,8 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws=
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl/v2 v2.0.0 h1:efQznTz+ydmQXq3BOnRa3AXzvCeTq1P4dKj/z5GLlY8=
github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90=
github.com/hashicorp/hcl2 v0.0.0-20190821123243-0c888d1241f6 h1:JImQpEeUQ+0DPFMaWzLA0GdUNPaUlCXLpfiqkSZBUfc=
github.com/hashicorp/hcl2 v0.0.0-20190821123243-0c888d1241f6/go.mod h1:Cxv+IJLuBiEhQ7pBYGEuORa0nr4U994pE8mYLuFd7v0=
github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590 h1:2yzhWGdgQUWZUCNK+AoO35V+HTsgEmcM4J9IkArh7PI=
@ -388,8 +390,8 @@ github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q
github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557 h1:Jpn2j6wHkC9wJv5iMfJhKqrZJx3TahFx+7sbZ7zQdxs=
github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v1.0.1-0.20190708163926-19588f92a98f h1:sq2p8SN6ji66CFEQFIWLlD/gFmGtr5hBrOzv5nLlGfA=
github.com/zclconf/go-cty v1.0.1-0.20190708163926-19588f92a98f/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v1.1.0 h1:uJwc9HiBOCpoKIObTQaLR+tsEXx1HBHnOsOOpcdhZgw=
github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty-yaml v1.0.1 h1:up11wlgAaDvlAGENcFDnZgkn0qUJurso7k6EpURKNF8=
github.com/zclconf/go-cty-yaml v1.0.1/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=

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