mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
tools: remove terraform-bundle. (#28876)
* tools: remove terraform-bundle. terraform-bundle is no longer supported in the main branch of terraform. Users can build terraform-bundle from terraform tagged v0.15 and older. * add a README pointing users to the v0.15 branch
This commit is contained in:
parent
79c61095ee
commit
5a48530f47
@ -93,7 +93,7 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Run Go E2E Tests
|
name: Run Go E2E Tests
|
||||||
command: |
|
command: |
|
||||||
gotestsum --format=short-verbose --junitfile $TEST_RESULTS_DIR/gotestsum-report.xml -- -p 2 -cover -coverprofile=cov_e2e.part ./internal/command/e2etest ./tools/terraform-bundle/e2etest
|
gotestsum --format=short-verbose --junitfile $TEST_RESULTS_DIR/gotestsum-report.xml -- -p 2 -cover -coverprofile=cov_e2e.part ./internal/command/e2etest
|
||||||
|
|
||||||
# save coverage report parts
|
# save coverage report parts
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
|
@ -58,7 +58,7 @@ func NewDir(baseDir string) *Dir {
|
|||||||
// running.
|
// running.
|
||||||
//
|
//
|
||||||
// This is primarily intended for portable unit testing and not particularly
|
// This is primarily intended for portable unit testing and not particularly
|
||||||
// useful in "real" callers, with the exception of terraform-bundle.
|
// useful in "real" callers.
|
||||||
func NewDirWithPlatform(baseDir string, platform getproviders.Platform) *Dir {
|
func NewDirWithPlatform(baseDir string, platform getproviders.Platform) *Dir {
|
||||||
return &Dir{
|
return &Dir{
|
||||||
baseDir: baseDir,
|
baseDir: baseDir,
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
## 0.14.0
|
|
||||||
|
|
||||||
BUG FIXES:
|
|
||||||
* fix packaging for custom plugins ([#26394](https://github.com/hashicorp/terraform/pull/26394))
|
|
||||||
|
|
||||||
## 0.13.0 (August 10, 2020)
|
|
||||||
|
|
||||||
> This is a list of changes relative to terraform-bundle tagged v0.12.
|
|
||||||
|
|
||||||
Breaking Changes:
|
|
||||||
* Terraform v0.13.0 has introduced a new hierarchical namespace for providers. Terraform v0.13 requires a new directory layout in order to discover locally-installed provider plugins, and terraform-bundle has been updated to match. Please see the [README](README.md) to learn more about the new directory layout.
|
|
@ -1,214 +1,6 @@
|
|||||||
# terraform-bundle
|
# terraform-bundle
|
||||||
|
|
||||||
`terraform-bundle` is a helper program to create "bundle archives", which are
|
terraform-bundle is no longer actively maintained. We recommend that you switch
|
||||||
zip files that contain both a particular version of Terraform and a number
|
to one of the [alternative provider installation methods](https://www.terraform.io/docs/cli/config/config-file.html#provider-installation)
|
||||||
of provider plugins.
|
introduced in Terraform v0.13. To continue using terraform-bundle, you can build
|
||||||
|
terraform-bundle from the v0.15 branch of the terraform repository.
|
||||||
Normally `terraform init` will download and install the plugins necessary to
|
|
||||||
work with a particular configuration, but sometimes Terraform is deployed in
|
|
||||||
a network that, for one reason or another, cannot access the official
|
|
||||||
plugin repository for automatic download.
|
|
||||||
|
|
||||||
In some cases, this can be solved by installing provider plugins into the
|
|
||||||
[user plugins directory](https://www.terraform.io/docs/configuration/providers.html#third-party-plugins).
|
|
||||||
However, this doesn't always meet the needs of automated deployments.
|
|
||||||
|
|
||||||
`terraform-bundle` provides an alternative, by allowing the auto-download
|
|
||||||
process to be run out-of-band on a separate machine that _does_ have access
|
|
||||||
to the repository. The result is a zip file that can be extracted onto the
|
|
||||||
target system to install both the desired Terraform version and a selection
|
|
||||||
of providers, thus avoiding the need for on-the-fly plugin installation.
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
To build `terraform-bundle` from source, set up a Terraform development
|
|
||||||
environment per [Terraform's own README](../../README.md) and then install
|
|
||||||
this tool from within it:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ go install ./tools/terraform-bundle
|
|
||||||
```
|
|
||||||
|
|
||||||
This will install `terraform-bundle` in `$GOPATH/bin`, which is assumed by
|
|
||||||
the rest of this README to be in `PATH`.
|
|
||||||
|
|
||||||
`terraform-bundle` is a repackaging of the module installation functionality
|
|
||||||
from Terraform itself, so for best results you should build from the tag
|
|
||||||
relating to the version of Terraform you plan to use. For example, use the v0.12
|
|
||||||
tag to build a version of terraform-bundle compatible with Terraform v0.12*.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
`terraform-bundle` uses a simple configuration file to define what should
|
|
||||||
be included in a bundle. This is designed so that it can be checked into
|
|
||||||
version control and used by an automated build and deploy process.
|
|
||||||
|
|
||||||
The configuration file format works as follows:
|
|
||||||
|
|
||||||
```hcl
|
|
||||||
terraform {
|
|
||||||
# Version of Terraform to include in the bundle. An exact version number
|
|
||||||
# is required.
|
|
||||||
version = "0.10.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Define which provider plugins are to be included
|
|
||||||
providers {
|
|
||||||
# Include the newest "aws" provider version in the 1.0 series.
|
|
||||||
aws = {
|
|
||||||
versions = ["~> 1.0"]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Include both the newest 1.0 and 2.0 versions of the "google" provider.
|
|
||||||
# Each item in these lists allows a distinct version to be added. If the
|
|
||||||
# two expressions match different versions then _both_ are included in
|
|
||||||
# the bundle archive.
|
|
||||||
google = {
|
|
||||||
versions = ["~> 1.0", "~> 2.0"]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Include a custom plugin to the bundle. Will search for the plugin in the
|
|
||||||
# plugins directory and package it with the bundle archive. Plugin must have
|
|
||||||
# a name of the form: terraform-provider-*, and must be built with the operating
|
|
||||||
# system and architecture that terraform enterprise is running, e.g. linux and amd64.
|
|
||||||
customplugin = {
|
|
||||||
versions = ["0.1"]
|
|
||||||
source = "myorg/customplugin"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
The `terraform` block defines which version of Terraform will be included
|
|
||||||
in the bundle. An exact version is required here.
|
|
||||||
|
|
||||||
The `providers` block defines zero or more providers to include in the bundle
|
|
||||||
along with core Terraform. Each attribute is a provider name, and its value is a
|
|
||||||
block with the list of version constraints and (optional) source. For each given
|
|
||||||
constraint, `terraform-bundle` will find the newest available version matching
|
|
||||||
the constraint and include it in the bundle.
|
|
||||||
|
|
||||||
It is allowed to specify multiple constraints for the same provider, in which
|
|
||||||
case multiple versions can be included in the resulting bundle. Each constraint
|
|
||||||
string given results in a separate plugin in the bundle, unless two constraints
|
|
||||||
resolve to the same concrete plugin.
|
|
||||||
|
|
||||||
Including multiple versions of the same provider allows several configurations
|
|
||||||
running on the same system to share an installation of the bundle and to
|
|
||||||
choose a version using version constraints within the main Terraform
|
|
||||||
configuration. This avoids the need to upgrade all configurations to newer
|
|
||||||
versions in lockstep.
|
|
||||||
|
|
||||||
After creating the configuration file, e.g. `terraform-bundle.hcl`, a bundle
|
|
||||||
zip file can be produced as follows:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ terraform-bundle package terraform-bundle.hcl
|
|
||||||
```
|
|
||||||
|
|
||||||
By default the bundle package will target the operating system and CPU
|
|
||||||
architecture where the tool is being run. To override this, use the `-os` and
|
|
||||||
`-arch` options. For example, to build a bundle for on-premises Terraform
|
|
||||||
Enterprise:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ terraform-bundle package -os=linux -arch=amd64 terraform-bundle.hcl
|
|
||||||
```
|
|
||||||
|
|
||||||
The bundle file is assigned a name that includes the core Terraform version
|
|
||||||
number, a timestamp to the nearest hour of when the bundle was built, and the
|
|
||||||
target OS and CPU architecture. It is recommended to refer to a bundle using
|
|
||||||
this composite version number so that bundle archives can be easily
|
|
||||||
distinguished from official release archives and from each other when multiple
|
|
||||||
bundles contain the same core Terraform version.
|
|
||||||
|
|
||||||
## Custom Plugins
|
|
||||||
To include custom plugins in the bundle file, create a local directory named
|
|
||||||
`./.plugins` and put all the plugins you want to include there, under the
|
|
||||||
required [sub directory](#plugins-directory-layout). Optionally, you can use the
|
|
||||||
`-plugin-dir` flag to specify a location where to find the plugins. To be
|
|
||||||
recognized as a valid plugin, the file must have a name of the form
|
|
||||||
`terraform-provider-<NAME>`. In addition, ensure that the plugin is built using
|
|
||||||
the same operating system and architecture used for Terraform Enterprise.
|
|
||||||
Typically this will be `linux` and `amd64`.
|
|
||||||
|
|
||||||
### Plugins Directory Layout
|
|
||||||
To include custom plugins in the bundle file, you must specify a "source"
|
|
||||||
attribute in the configuration and place the plugin in the appropriate
|
|
||||||
subdirectory under `./.plugins`. The directory must have the following layout:
|
|
||||||
|
|
||||||
```
|
|
||||||
./.plugins/$SOURCEHOST/$SOURCENAMESPACE/$NAME/$VERSION/$OS_$ARCH/
|
|
||||||
```
|
|
||||||
|
|
||||||
When installing custom plugins, you may choose any arbitrary identifier for the
|
|
||||||
$SOURCEHOST and $SOURCENAMESPACE subdirectories.
|
|
||||||
|
|
||||||
For example, given the following configuration and a plugin built for Terraform Enterprise:
|
|
||||||
|
|
||||||
```
|
|
||||||
providers {
|
|
||||||
customplugin = {
|
|
||||||
versions = ["0.1"]
|
|
||||||
source = "example.com/myorg/customplugin"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The binary must be placed in the following directory:
|
|
||||||
|
|
||||||
```
|
|
||||||
./.plugins/example.com/myorg/customplugin/0.1/linux_amd64/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Provider Resolution Behavior
|
|
||||||
|
|
||||||
Terraform's provider resolution behavior is such that if a given constraint
|
|
||||||
can be resolved by any plugin already installed on the system it will use
|
|
||||||
the newest matching plugin and not attempt automatic installation.
|
|
||||||
|
|
||||||
Therefore if automatic installation is not desired, it is important to ensure
|
|
||||||
that version constraints within Terraform configurations do not exclude all
|
|
||||||
of the versions available from the bundle. If a suitable version cannot be
|
|
||||||
found in the bundle, Terraform _will_ attempt to satisfy that dependency by
|
|
||||||
automatic installation from the official repository.
|
|
||||||
|
|
||||||
For full details about provider resolution, see
|
|
||||||
[How Terraform Works: Plugin Discovery](https://www.terraform.io/docs/extend/how-terraform-works.html#discovery).
|
|
||||||
|
|
||||||
The downloaded provider archives are verified using the same signature check
|
|
||||||
that is used for auto-installed plugins, using Hashicorp's release key. At
|
|
||||||
this time, the core Terraform archive itself is _not_ verified in this way;
|
|
||||||
that may change in a future version of this tool.
|
|
||||||
|
|
||||||
## Installing a Bundle in Terraform Enterprise
|
|
||||||
|
|
||||||
If using a Terraform Enterprise instance in an "air-gapped"
|
|
||||||
environment, this tool can produce a custom Terraform version package, which
|
|
||||||
includes a set of provider plugins along with core Terraform.
|
|
||||||
|
|
||||||
To create a suitable bundle, use the `-os` and `-arch` options as described
|
|
||||||
above to produce a bundle targeting `linux_amd64`. You can then place this
|
|
||||||
archive on an HTTP server reachable by the Terraform Enterprise hosts and
|
|
||||||
install it as per
|
|
||||||
[Administration: Managing Terraform Versions](https://www.terraform.io/docs/enterprise/admin/resources.html#managing-terraform-versions).
|
|
||||||
|
|
||||||
After clicking the "Add Terraform Version" button:
|
|
||||||
|
|
||||||
1. In the "Version" field, enter the generated bundle version from the bundle
|
|
||||||
filename, which will be of the form `N.N.N-bundleYYYYMMDDHH`.
|
|
||||||
2. In the "URL" field, enter the URL where the generated bundle archive can be found.
|
|
||||||
3. In the "SHA256 Checksum" field, enter the SHA256 hash of the file, which can
|
|
||||||
be found by running `sha256sum <FILE>` or `shasum -a256 <FILE>`.
|
|
||||||
|
|
||||||
The new bundle version can then be selected as the Terraform version for
|
|
||||||
any workspace. When selected, configurations that require only plugins
|
|
||||||
included in the bundle will run without trying to auto-install.
|
|
||||||
|
|
||||||
Note that the above does _not_ apply to Terraform Pro, or to Terraform Premium
|
|
||||||
when not running a private install. In these packages, Terraform versions
|
|
||||||
are managed centrally across _all_ organizations and so custom bundles are not
|
|
||||||
supported.
|
|
||||||
|
|
||||||
For more information on the available Terraform Enterprise packages, see
|
|
||||||
[the Terraform product site](https://www.hashicorp.com/products/terraform/).
|
|
||||||
|
@ -1,87 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl"
|
|
||||||
"github.com/hashicorp/terraform/internal/addrs"
|
|
||||||
"github.com/hashicorp/terraform/internal/getproviders"
|
|
||||||
"github.com/hashicorp/terraform/internal/plugin/discovery"
|
|
||||||
)
|
|
||||||
|
|
||||||
var zeroThirteen = discovery.ConstraintStr(">= 0.13.0").MustParse()
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Terraform TerraformConfig `hcl:"terraform"`
|
|
||||||
Providers map[string]ProviderConfig `hcl:"providers"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TerraformConfig struct {
|
|
||||||
Version discovery.VersionStr `hcl:"version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProviderConfig struct {
|
|
||||||
Versions []string `hcl:"versions"`
|
|
||||||
Source string `hcl:"source"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadConfig(src []byte, filename string) (*Config, error) {
|
|
||||||
config := &Config{}
|
|
||||||
err := hcl.Decode(config, string(src))
|
|
||||||
if err != nil {
|
|
||||||
return config, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = config.validate()
|
|
||||||
return config, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadConfigFile(filename string) (*Config, error) {
|
|
||||||
src, err := ioutil.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return LoadConfig(src, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) validate() error {
|
|
||||||
if c.Terraform.Version == "" {
|
|
||||||
return fmt.Errorf("terraform.version is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
var v discovery.Version
|
|
||||||
var err error
|
|
||||||
if v, err = c.Terraform.Version.Parse(); err != nil {
|
|
||||||
return fmt.Errorf("terraform.version: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !zeroThirteen.Allows(v) {
|
|
||||||
return fmt.Errorf("this version of terraform-bundle can only build bundles for Terraform v0.13 and later; build terraform-bundle from a release tag (such as v0.12.*) to construct bundles for earlier versions")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Providers == nil {
|
|
||||||
c.Providers = map[string]ProviderConfig{}
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, cs := range c.Providers {
|
|
||||||
if cs.Source != "" {
|
|
||||||
_, diags := addrs.ParseProviderSourceString(cs.Source)
|
|
||||||
if diags.HasErrors() {
|
|
||||||
return fmt.Errorf("providers.%s: %s", k, diags.Err().Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(cs.Versions) > 0 {
|
|
||||||
for _, c := range cs.Versions {
|
|
||||||
if _, err := getproviders.ParseVersionConstraints(c); err != nil {
|
|
||||||
return fmt.Errorf("providers.%s: %s", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("provider.%s: required \"versions\" argument not found", k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
// terraform bundle e2e tests
|
|
||||||
package e2etest
|
|
@ -1,39 +0,0 @@
|
|||||||
package e2etest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/internal/e2e"
|
|
||||||
)
|
|
||||||
|
|
||||||
var bundleBin string
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
teardown := setup()
|
|
||||||
code := m.Run()
|
|
||||||
teardown()
|
|
||||||
os.Exit(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setup() func() {
|
|
||||||
tmpFilename := e2e.GoBuild("github.com/hashicorp/terraform/tools/terraform-bundle", "terraform-bundle")
|
|
||||||
bundleBin = tmpFilename
|
|
||||||
|
|
||||||
return func() {
|
|
||||||
os.Remove(tmpFilename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func canAccessNetwork() bool {
|
|
||||||
// We re-use the flag normally used for acceptance tests since that's
|
|
||||||
// established as a way to opt-in to reaching out to real systems that
|
|
||||||
// may suffer transient errors.
|
|
||||||
return os.Getenv("TF_ACC") != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func skipIfCannotAccessNetwork(t *testing.T) {
|
|
||||||
if !canAccessNetwork() {
|
|
||||||
t.Skip("network access not allowed; use TF_ACC=1 to enable")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,229 +0,0 @@
|
|||||||
package e2etest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/zip"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/internal/e2e"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPackage_empty(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
// This test reaches out to releases.hashicorp.com to download the
|
|
||||||
// template provider, so it can only run if network access is allowed.
|
|
||||||
// We intentionally don't try to stub this here, because there's already
|
|
||||||
// a stubbed version of this in the "command" package and so the goal here
|
|
||||||
// is to test the interaction with the real repository.
|
|
||||||
skipIfCannotAccessNetwork(t)
|
|
||||||
|
|
||||||
fixturePath := filepath.Join("testdata", "empty")
|
|
||||||
tfBundle := e2e.NewBinary(bundleBin, fixturePath)
|
|
||||||
defer tfBundle.Close()
|
|
||||||
|
|
||||||
stdout, stderr, err := tfBundle.Run("package", "terraform-bundle.hcl")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if stderr != "" {
|
|
||||||
t.Errorf("unexpected stderr output:\n%s", stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(stdout, "Fetching Terraform 0.13.0 core package...") {
|
|
||||||
t.Errorf("success message is missing from output:\n%s", stdout)
|
|
||||||
}
|
|
||||||
if !strings.Contains(stdout, "Creating terraform_0.13.0-bundle") {
|
|
||||||
t.Errorf("success message is missing from output:\n%s", stdout)
|
|
||||||
}
|
|
||||||
if !strings.Contains(stdout, "All done!") {
|
|
||||||
t.Errorf("success message is missing from output:\n%s", stdout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPackage_manyProviders(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// This test reaches out to releases.hashicorp.com to download providers, so
|
|
||||||
// it can only run if network access is allowed. We intentionally don't try
|
|
||||||
// to stub this here, because there's already a stubbed version of this in
|
|
||||||
// the "command" package and so the goal here is to test the interaction
|
|
||||||
// with the real repository.
|
|
||||||
skipIfCannotAccessNetwork(t)
|
|
||||||
|
|
||||||
fixturePath := filepath.Join("testdata", "many-providers")
|
|
||||||
tfBundle := e2e.NewBinary(bundleBin, fixturePath)
|
|
||||||
defer tfBundle.Close()
|
|
||||||
|
|
||||||
stdout, stderr, err := tfBundle.Run("package", "terraform-bundle.hcl")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if stderr != "" {
|
|
||||||
t.Errorf("unexpected stderr output:\n%s", stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Here we have to check each provider separately
|
|
||||||
// because it's internally held in a map (i.e. not guaranteed order)
|
|
||||||
|
|
||||||
if !strings.Contains(stdout, `- Finding hashicorp/aws versions matching "~> 2.26.0"...
|
|
||||||
- Installing hashicorp/aws v2.26.0...`) {
|
|
||||||
t.Errorf("success message is missing from output:\n%s", stdout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(stdout, `- Finding hashicorp/kubernetes versions matching "1.8.0"...
|
|
||||||
- Installing hashicorp/kubernetes v1.8.0...
|
|
||||||
- Finding hashicorp/kubernetes versions matching "1.8.1"...
|
|
||||||
- Installing hashicorp/kubernetes v1.8.1...
|
|
||||||
- Finding hashicorp/kubernetes versions matching "1.9.0"...
|
|
||||||
- Installing hashicorp/kubernetes v1.9.0...`) {
|
|
||||||
t.Errorf("success message is missing from output:\n%s", stdout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(stdout, `- Finding hashicorp/null versions matching "2.1.0"...
|
|
||||||
- Installing hashicorp/null v2.1.0...`) {
|
|
||||||
t.Errorf("success message is missing from output:\n%s", stdout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(stdout, "Fetching Terraform 0.13.0 core package...") {
|
|
||||||
t.Errorf("success message is missing from output:\n%s", stdout)
|
|
||||||
}
|
|
||||||
if !strings.Contains(stdout, "Creating terraform_0.13.0-bundle") {
|
|
||||||
t.Errorf("success message is missing from output:\n%s", stdout)
|
|
||||||
}
|
|
||||||
if !strings.Contains(stdout, "All done!") {
|
|
||||||
t.Errorf("success message is missing from output:\n%s", stdout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the contents of the created zipfile
|
|
||||||
files, err := ioutil.ReadDir(tfBundle.WorkDir())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error reading workdir: %s", err)
|
|
||||||
}
|
|
||||||
for _, file := range files {
|
|
||||||
if strings.Contains(file.Name(), "terraform_0.13.0-bundle") {
|
|
||||||
read, err := zip.OpenReader(filepath.Join(tfBundle.WorkDir(), file.Name()))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to open archive: %s", err)
|
|
||||||
}
|
|
||||||
defer read.Close()
|
|
||||||
|
|
||||||
expectedFiles := map[string]struct{}{
|
|
||||||
"terraform": {},
|
|
||||||
testProviderBinaryPath("null", "2.1.0"): {},
|
|
||||||
testProviderBinaryPath("aws", "2.26.0"): {},
|
|
||||||
testProviderBinaryPath("kubernetes", "1.8.0"): {},
|
|
||||||
testProviderBinaryPath("kubernetes", "1.8.1"): {},
|
|
||||||
testProviderBinaryPath("kubernetes", "1.9.0"): {},
|
|
||||||
}
|
|
||||||
extraFiles := make(map[string]struct{})
|
|
||||||
|
|
||||||
for _, file := range read.File {
|
|
||||||
if _, exists := expectedFiles[file.Name]; exists {
|
|
||||||
if !file.FileInfo().Mode().IsRegular() {
|
|
||||||
t.Errorf("Expected file is not a regular file: %s", file.Name)
|
|
||||||
}
|
|
||||||
delete(expectedFiles, file.Name)
|
|
||||||
} else {
|
|
||||||
extraFiles[file.Name] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(expectedFiles) != 0 {
|
|
||||||
t.Errorf("missing expected file(s): %#v", expectedFiles)
|
|
||||||
}
|
|
||||||
if len(extraFiles) != 0 {
|
|
||||||
t.Errorf("found extra unexpected file(s): %#v", extraFiles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPackage_localProviders(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// This test reaches out to releases.hashicorp.com to download terrafrom, so
|
|
||||||
// it can only run if network access is allowed. The providers are installed
|
|
||||||
// from the local cache.
|
|
||||||
skipIfCannotAccessNetwork(t)
|
|
||||||
|
|
||||||
fixturePath := filepath.Join("testdata", "local-providers")
|
|
||||||
tfBundle := e2e.NewBinary(bundleBin, fixturePath)
|
|
||||||
defer tfBundle.Close()
|
|
||||||
|
|
||||||
// we explicitly specify the platform so that tests can find the local binary under the expected directory
|
|
||||||
stdout, stderr, err := tfBundle.Run("package", "-os=darwin", "-arch=amd64", "terraform-bundle.hcl")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if stderr != "" {
|
|
||||||
t.Errorf("unexpected stderr output:\n%s", stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Here we have to check each provider separately
|
|
||||||
// because it's internally held in a map (i.e. not guaranteed order)
|
|
||||||
if !strings.Contains(stdout, "Fetching Terraform 0.13.0 core package...") {
|
|
||||||
t.Errorf("success message is missing from output:\n%s", stdout)
|
|
||||||
}
|
|
||||||
if !strings.Contains(stdout, "Creating terraform_0.13.0-bundle") {
|
|
||||||
t.Errorf("success message is missing from output:\n%s", stdout)
|
|
||||||
}
|
|
||||||
if !strings.Contains(stdout, "All done!") {
|
|
||||||
t.Errorf("success message is missing from output:\n%s", stdout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the contents of the created zipfile
|
|
||||||
files, err := ioutil.ReadDir(tfBundle.WorkDir())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error reading workdir: %s", err)
|
|
||||||
}
|
|
||||||
for _, file := range files {
|
|
||||||
if strings.Contains(file.Name(), "terraform_0.13.0-bundle") {
|
|
||||||
read, err := zip.OpenReader(filepath.Join(tfBundle.WorkDir(), file.Name()))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to open archive: %s", err)
|
|
||||||
}
|
|
||||||
defer read.Close()
|
|
||||||
|
|
||||||
expectedFiles := map[string]struct{}{
|
|
||||||
"terraform": {},
|
|
||||||
"plugins/example.com/myorg/mycloud/0.1.0/darwin_amd64/terraform-provider-mycloud": {},
|
|
||||||
}
|
|
||||||
extraFiles := make(map[string]struct{})
|
|
||||||
|
|
||||||
for _, file := range read.File {
|
|
||||||
if _, exists := expectedFiles[file.Name]; exists {
|
|
||||||
if !file.FileInfo().Mode().IsRegular() {
|
|
||||||
t.Errorf("Expected file is not a regular file: %s", file.Name)
|
|
||||||
}
|
|
||||||
delete(expectedFiles, file.Name)
|
|
||||||
} else {
|
|
||||||
extraFiles[file.Name] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(expectedFiles) != 0 {
|
|
||||||
t.Errorf("missing expected file(s): %#v", expectedFiles)
|
|
||||||
}
|
|
||||||
if len(extraFiles) != 0 {
|
|
||||||
t.Errorf("found extra unexpected file(s): %#v", extraFiles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// testProviderBinaryPath takes a provider name (assumed to be a hashicorp
|
|
||||||
// provider) and version and returns the expected binary path, relative to the
|
|
||||||
// archive, for the plugin.
|
|
||||||
func testProviderBinaryPath(provider, version string) string {
|
|
||||||
os := runtime.GOOS
|
|
||||||
arch := runtime.GOARCH
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"plugins/registry.terraform.io/hashicorp/%s/%s/%s_%s/terraform-provider-%s_v%s_x4",
|
|
||||||
provider, version, os, arch, provider, version,
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
terraform {
|
|
||||||
version = "0.13.0"
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
I am a fake binary.
|
|
@ -1,11 +0,0 @@
|
|||||||
terraform {
|
|
||||||
version = "0.13.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
providers {
|
|
||||||
// this provider is installed in .plugins
|
|
||||||
mycloud = {
|
|
||||||
versions = ["0.1"]
|
|
||||||
source = "example.com/myorg/mycloud"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
terraform {
|
|
||||||
version = "0.13.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
providers {
|
|
||||||
aws = {
|
|
||||||
versions = ["~> 2.26.0"]
|
|
||||||
}
|
|
||||||
|
|
||||||
kubernetes = {
|
|
||||||
versions = ["1.8.0", "1.8.1", "1.9.0"]
|
|
||||||
}
|
|
||||||
|
|
||||||
null = {
|
|
||||||
versions = ["2.1.0"]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
// terraform-bundle is a tool to create "bundle archives" that contain both
|
|
||||||
// a particular version of Terraform and a set of providers for use with it.
|
|
||||||
//
|
|
||||||
// Such bundles are useful for distributing a Terraform version and a set
|
|
||||||
// of providers to a system out-of-band, in situations where Terraform's
|
|
||||||
// auto-installer cannot be used due to firewall rules, "air-gapped" systems,
|
|
||||||
// etc.
|
|
||||||
//
|
|
||||||
// When using bundle archives, it's suggested to use a version numbering
|
|
||||||
// scheme that adds a suffix that identifies the archive as being a bundle,
|
|
||||||
// to make it easier to distinguish bundle archives from the normal separated
|
|
||||||
// release archives. This tool by default produces files with the following
|
|
||||||
// naming scheme:
|
|
||||||
//
|
|
||||||
// terraform_0.10.0-bundle2017070302_linux_amd64.zip
|
|
||||||
//
|
|
||||||
// The user is free to rename these files, since the archive filename has
|
|
||||||
// no significance to Terraform itself and the generated pseudo-version number
|
|
||||||
// is not referenced within the archive contents.
|
|
||||||
//
|
|
||||||
// If using such a bundle with an on-premises Terraform Enterprise installation,
|
|
||||||
// it's recommended to use the generated version number (or a modification
|
|
||||||
// thereof) as the tool version within Terraform Enterprise, so that
|
|
||||||
// bundle archives can be distinguished from official releases and from
|
|
||||||
// each other even if the same core Terraform version is used.
|
|
||||||
//
|
|
||||||
// Terraform providers in general release more often than core, so it is
|
|
||||||
// intended that this tool can be used to periodically upgrade providers
|
|
||||||
// within certain constraints and produce a new bundle containing these
|
|
||||||
// upgraded provider versions. A bundle archive can include multiple versions
|
|
||||||
// of the same provider, allowing configurations containing provider version
|
|
||||||
// constrants to be gradually migrated to newer versions.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
tfversion "github.com/hashicorp/terraform/version"
|
|
||||||
"github.com/mitchellh/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
ui := &cli.ColoredUi{
|
|
||||||
OutputColor: cli.UiColorNone,
|
|
||||||
InfoColor: cli.UiColorNone,
|
|
||||||
ErrorColor: cli.UiColorRed,
|
|
||||||
WarnColor: cli.UiColorYellow,
|
|
||||||
|
|
||||||
Ui: &cli.BasicUi{
|
|
||||||
Reader: os.Stdin,
|
|
||||||
Writer: os.Stdout,
|
|
||||||
ErrorWriter: os.Stderr,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Terraform's code tends to produce noisy logs, since Terraform itself
|
|
||||||
// suppresses them by default. To avoid polluting our console, we'll do
|
|
||||||
// the same.
|
|
||||||
if os.Getenv("TF_LOG") == "" {
|
|
||||||
log.SetOutput(ioutil.Discard)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := cli.NewCLI("terraform-bundle", tfversion.Version)
|
|
||||||
c.Args = os.Args[1:]
|
|
||||||
c.Commands = map[string]cli.CommandFactory{
|
|
||||||
"package": func() (cli.Command, error) {
|
|
||||||
return &PackageCommand{
|
|
||||||
ui: ui,
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
exitStatus, err := c.Run()
|
|
||||||
if err != nil {
|
|
||||||
ui.Error(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Exit(exitStatus)
|
|
||||||
}
|
|
@ -1,423 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/zip"
|
|
||||||
"context"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
getter "github.com/hashicorp/go-getter"
|
|
||||||
"github.com/hashicorp/terraform-svchost/disco"
|
|
||||||
"github.com/hashicorp/terraform/internal/addrs"
|
|
||||||
"github.com/hashicorp/terraform/internal/depsfile"
|
|
||||||
"github.com/hashicorp/terraform/internal/getproviders"
|
|
||||||
"github.com/hashicorp/terraform/internal/httpclient"
|
|
||||||
discovery "github.com/hashicorp/terraform/internal/plugin/discovery"
|
|
||||||
"github.com/hashicorp/terraform/internal/providercache"
|
|
||||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
||||||
"github.com/hashicorp/terraform/version"
|
|
||||||
"github.com/mitchellh/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
var releaseHost = "https://releases.hashicorp.com"
|
|
||||||
|
|
||||||
var pluginDir = ".plugins"
|
|
||||||
|
|
||||||
type PackageCommand struct {
|
|
||||||
ui cli.Ui
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PackageCommand) Run(args []string) int {
|
|
||||||
flags := flag.NewFlagSet("package", flag.ExitOnError)
|
|
||||||
osPtr := flags.String("os", "", "Target operating system")
|
|
||||||
archPtr := flags.String("arch", "", "Target CPU architecture")
|
|
||||||
pluginDirPtr := flags.String("plugin-dir", "", "Path to custom plugins directory")
|
|
||||||
err := flags.Parse(args)
|
|
||||||
if err != nil {
|
|
||||||
c.ui.Error(err.Error())
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
osName := runtime.GOOS
|
|
||||||
archName := runtime.GOARCH
|
|
||||||
if *osPtr != "" {
|
|
||||||
osName = *osPtr
|
|
||||||
}
|
|
||||||
if *archPtr != "" {
|
|
||||||
archName = *archPtr
|
|
||||||
}
|
|
||||||
if *pluginDirPtr != "" {
|
|
||||||
pluginDir = *pluginDirPtr
|
|
||||||
}
|
|
||||||
|
|
||||||
if flags.NArg() != 1 {
|
|
||||||
c.ui.Error("Configuration filename is required")
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
configFn := flags.Arg(0)
|
|
||||||
|
|
||||||
config, err := LoadConfigFile(configFn)
|
|
||||||
if err != nil {
|
|
||||||
c.ui.Error(fmt.Sprintf("Failed to read config: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpDir, err := ioutil.TempDir("", "terraform-bundle")
|
|
||||||
if err != nil {
|
|
||||||
c.ui.Error(fmt.Sprintf("Could not create temporary dir: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
// symlinked tmp directories can cause odd behaviors.
|
|
||||||
workDir, err := filepath.EvalSymlinks(tmpDir)
|
|
||||||
if err != nil {
|
|
||||||
c.ui.Error(fmt.Sprintf("Error evaulating symlinks: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(workDir)
|
|
||||||
|
|
||||||
c.ui.Info(fmt.Sprintf("Fetching Terraform %s core package...", config.Terraform.Version))
|
|
||||||
|
|
||||||
coreZipURL := c.coreURL(config.Terraform.Version, osName, archName)
|
|
||||||
err = getter.Get(workDir, coreZipURL)
|
|
||||||
if err != nil {
|
|
||||||
c.ui.Error(fmt.Sprintf("Failed to fetch core package from %s: %s", coreZipURL, err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the list of required providers from the config
|
|
||||||
reqs := make(map[addrs.Provider][]string)
|
|
||||||
for name, provider := range config.Providers {
|
|
||||||
var fqn addrs.Provider
|
|
||||||
var diags tfdiags.Diagnostics
|
|
||||||
if provider.Source != "" {
|
|
||||||
fqn, diags = addrs.ParseProviderSourceString(provider.Source)
|
|
||||||
if diags.HasErrors() {
|
|
||||||
c.ui.Error(fmt.Sprintf("Invalid provider source string: %s", provider.Source))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fqn = addrs.NewDefaultProvider(name)
|
|
||||||
}
|
|
||||||
reqs[fqn] = provider.Versions
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up the provider installer
|
|
||||||
platform := getproviders.Platform{
|
|
||||||
OS: osName,
|
|
||||||
Arch: archName,
|
|
||||||
}
|
|
||||||
installdir := providercache.NewDirWithPlatform(filepath.Join(workDir, "plugins"), platform)
|
|
||||||
|
|
||||||
services := disco.New()
|
|
||||||
services.SetUserAgent(httpclient.TerraformUserAgent(version.String()))
|
|
||||||
var sources []getproviders.MultiSourceSelector
|
|
||||||
|
|
||||||
// Find any local providers first so we can exclude these from the registry
|
|
||||||
// install. We'll just silently ignore any errors and assume it would fail
|
|
||||||
// real installation later too.
|
|
||||||
foundLocally := map[addrs.Provider]struct{}{}
|
|
||||||
|
|
||||||
if absPluginDir, err := filepath.Abs(pluginDir); err == nil {
|
|
||||||
c.ui.Info(fmt.Sprintf("Local plugin directory %q found; scanning for provider binaries.", pluginDir))
|
|
||||||
if _, err := os.Stat(absPluginDir); err == nil {
|
|
||||||
localSource := getproviders.NewFilesystemMirrorSource(absPluginDir)
|
|
||||||
if available, err := localSource.AllAvailablePackages(); err == nil {
|
|
||||||
for found := range available {
|
|
||||||
c.ui.Info(fmt.Sprintf("Found provider %q in %q.", found.String(), pluginDir))
|
|
||||||
foundLocally[found] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sources = append(sources, getproviders.MultiSourceSelector{
|
|
||||||
Source: localSource,
|
|
||||||
})
|
|
||||||
if len(foundLocally) == 0 {
|
|
||||||
c.ui.Info(fmt.Sprintf("No local providers found in %q.", pluginDir))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
c.ui.Info(fmt.Sprintf("No %q directory found, skipping local provider discovery.", pluginDir))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anything we found in local directories above is excluded from being
|
|
||||||
// looked up via the registry source we're about to construct.
|
|
||||||
var directExcluded getproviders.MultiSourceMatchingPatterns
|
|
||||||
for addr := range foundLocally {
|
|
||||||
directExcluded = append(directExcluded, addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the registry source, minus any providers found in the local pluginDir.
|
|
||||||
sources = append(sources, getproviders.MultiSourceSelector{
|
|
||||||
Source: getproviders.NewMemoizeSource(getproviders.NewRegistrySource(services)),
|
|
||||||
Exclude: directExcluded,
|
|
||||||
})
|
|
||||||
|
|
||||||
installer := providercache.NewInstaller(installdir, getproviders.MultiSource(sources))
|
|
||||||
|
|
||||||
err = c.ensureProviderVersions(installer, reqs)
|
|
||||||
if err != nil {
|
|
||||||
c.ui.Error(err.Error())
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove the selections.json file created by the provider installer
|
|
||||||
os.Remove(filepath.Join(workDir, "plugins", "selections.json"))
|
|
||||||
|
|
||||||
// If we get this far then our workDir now contains the union of the
|
|
||||||
// contents of all the zip files we downloaded above. We can now create
|
|
||||||
// our output file.
|
|
||||||
outFn := c.bundleFilename(config.Terraform.Version, time.Now(), osName, archName)
|
|
||||||
c.ui.Info(fmt.Sprintf("Creating %s ...", outFn))
|
|
||||||
outF, err := os.OpenFile(outFn, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
c.ui.Error(fmt.Sprintf("Failed to create %s: %s", outFn, err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
outZ := zip.NewWriter(outF)
|
|
||||||
defer func() {
|
|
||||||
err := outZ.Close()
|
|
||||||
if err != nil {
|
|
||||||
c.ui.Error(fmt.Sprintf("Failed to close %s: %s", outFn, err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
err = outF.Close()
|
|
||||||
if err != nil {
|
|
||||||
c.ui.Error(fmt.Sprintf("Failed to close %s: %s", outFn, err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// recursively walk the workDir to get a list of all binary filepaths
|
|
||||||
err = filepath.Walk(workDir,
|
|
||||||
func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if info.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// maybe symlinks
|
|
||||||
linkPath, err := filepath.EvalSymlinks(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
linkInfo, err := os.Stat(linkPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if linkInfo.IsDir() {
|
|
||||||
// The only time we should encounter a symlink directory is when we
|
|
||||||
// have a locally-installed provider, so we will grab the provider
|
|
||||||
// binary from that file.
|
|
||||||
files, err := ioutil.ReadDir(linkPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, file := range files {
|
|
||||||
if strings.Contains(file.Name(), "terraform-provider") {
|
|
||||||
relPath, _ := filepath.Rel(workDir, path)
|
|
||||||
return addZipFile(
|
|
||||||
filepath.Join(linkPath, file.Name()), // the link to this provider binary
|
|
||||||
filepath.Join(relPath, file.Name()), // the expected directory for the binary
|
|
||||||
file, outZ,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// This shouldn't happen - we should always find a provider
|
|
||||||
// binary and exit the loop - but on the chance it does not,
|
|
||||||
// just continue.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// provider plugins need to be created in the same relative directory structure
|
|
||||||
absPath, err := filepath.Abs(linkPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
relPath, err := filepath.Rel(workDir, absPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return addZipFile(path, relPath, info, outZ)
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
c.ui.Error(err.Error())
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
c.ui.Info("All done!")
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// addZipFile is a helper function intneded to simplify customizing the file
|
|
||||||
// path when adding a file to the zip archive. The relPath is specified for
|
|
||||||
// provider binaries, which need to be zipped into the full directory hierarchy.
|
|
||||||
func addZipFile(fn, relPath string, info os.FileInfo, outZ *zip.Writer) error {
|
|
||||||
hdr, err := zip.FileInfoHeader(info)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to add zip entry for %s: %s", fn, err)
|
|
||||||
}
|
|
||||||
hdr.Method = zip.Deflate // be sure to compress files
|
|
||||||
hdr.Name = relPath // we need the full, relative path to the provider binary
|
|
||||||
w, err := outZ.CreateHeader(hdr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to add zip entry for %s: %s", fn, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := os.Open(fn)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to open %s: %s", fn, err)
|
|
||||||
}
|
|
||||||
_, err = io.Copy(w, r)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to write %s to bundle: %s", fn, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PackageCommand) bundleFilename(version discovery.VersionStr, time time.Time, osName, archName string) string {
|
|
||||||
time = time.UTC()
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"terraform_%s-bundle%04d%02d%02d%02d_%s_%s.zip",
|
|
||||||
version,
|
|
||||||
time.Year(), time.Month(), time.Day(), time.Hour(),
|
|
||||||
osName, archName,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PackageCommand) coreURL(version discovery.VersionStr, osName, archName string) string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"%s/terraform/%s/terraform_%s_%s_%s.zip",
|
|
||||||
releaseHost, version, version, osName, archName,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PackageCommand) Synopsis() string {
|
|
||||||
return "Produces a bundle archive"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PackageCommand) Help() string {
|
|
||||||
return `Usage: terraform-bundle package [options] <config-file>
|
|
||||||
|
|
||||||
Uses the given bundle configuration file to produce a zip file in the
|
|
||||||
current working directory containing a Terraform binary along with zero or
|
|
||||||
more provider plugin binaries.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-os=name Target operating system the archive will be built for. Defaults
|
|
||||||
to that of the system where the command is being run.
|
|
||||||
|
|
||||||
-arch=name Target CPU architecture the archive will be built for. Defaults
|
|
||||||
to that of the system where the command is being run.
|
|
||||||
|
|
||||||
-plugin-dir=path The path to the custom plugins directory. Defaults to "./plugins".
|
|
||||||
|
|
||||||
The resulting zip file can be used to more easily install Terraform and
|
|
||||||
a fixed set of providers together on a server, so that Terraform's provider
|
|
||||||
auto-installation mechanism can be avoided.
|
|
||||||
|
|
||||||
To build an archive for Terraform Enterprise, use:
|
|
||||||
-os=linux -arch=amd64
|
|
||||||
|
|
||||||
Note that the given configuration file is a format specific to this command,
|
|
||||||
not a normal Terraform configuration file. The file format looks like this:
|
|
||||||
|
|
||||||
terraform {
|
|
||||||
# Version of Terraform to include in the bundle. An exact version number
|
|
||||||
# is required.
|
|
||||||
version = "0.13.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Define which provider plugins are to be included
|
|
||||||
providers {
|
|
||||||
# Include the newest "aws" provider version in the 1.0 series.
|
|
||||||
aws = {
|
|
||||||
versions = ["~> 1.0"]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Include both the newest 1.0 and 2.0 versions of the "google" provider.
|
|
||||||
# Each item in these lists allows a distinct version to be added. If the
|
|
||||||
# two expressions match different versions then _both_ are included in
|
|
||||||
# the bundle archive.
|
|
||||||
google = {
|
|
||||||
versions = ["~> 1.0", "~> 2.0"]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Include a custom plugin to the bundle. Will search for the plugin in the
|
|
||||||
# plugins directory, and package it with the bundle archive. Plugin must
|
|
||||||
# have a name of the form: terraform-provider-*, and must be built with
|
|
||||||
# the operating system and architecture that terraform enterprise is running,
|
|
||||||
# e.g. linux and amd64.
|
|
||||||
# See the README for more information on the source attribute and plugin
|
|
||||||
# directory layout.
|
|
||||||
customplugin = {
|
|
||||||
versions = ["0.1"]
|
|
||||||
source = "example.com/myorg/customplugin"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureProviderVersions is a wrapper around
|
|
||||||
// providercache.EnsureProviderVersions which allows installing multiple
|
|
||||||
// versions of a given provider.
|
|
||||||
func (c *PackageCommand) ensureProviderVersions(installer *providercache.Installer, reqs map[addrs.Provider][]string) error {
|
|
||||||
mode := providercache.InstallNewProvidersOnly
|
|
||||||
evts := &providercache.InstallerEvents{
|
|
||||||
ProviderAlreadyInstalled: func(provider addrs.Provider, selectedVersion getproviders.Version) {
|
|
||||||
c.ui.Info(fmt.Sprintf("- Using previously-installed %s v%s", provider.ForDisplay(), selectedVersion))
|
|
||||||
},
|
|
||||||
QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) {
|
|
||||||
if len(versionConstraints) > 0 {
|
|
||||||
c.ui.Info(fmt.Sprintf("- Finding %s versions matching %q...", provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints)))
|
|
||||||
} else {
|
|
||||||
c.ui.Info(fmt.Sprintf("- Finding latest version of %s...", provider.ForDisplay()))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) {
|
|
||||||
c.ui.Info(fmt.Sprintf("- Installing %s v%s...", provider.ForDisplay(), version))
|
|
||||||
},
|
|
||||||
QueryPackagesFailure: func(provider addrs.Provider, err error) {
|
|
||||||
c.ui.Error(fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s.", provider.ForDisplay(), err))
|
|
||||||
},
|
|
||||||
FetchPackageFailure: func(provider addrs.Provider, version getproviders.Version, err error) {
|
|
||||||
c.ui.Error(fmt.Sprintf("Error while installing %s v%s: %s.", provider.ForDisplay(), version, err))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := evts.OnContext(context.TODO())
|
|
||||||
for provider, versions := range reqs {
|
|
||||||
for _, constraint := range versions {
|
|
||||||
req := make(getproviders.Requirements, 1)
|
|
||||||
cstr, err := getproviders.ParseVersionConstraints(constraint)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req[provider] = cstr
|
|
||||||
|
|
||||||
// We always start with no locks here, because we want to take
|
|
||||||
// the newest version matching the given version constraint, and
|
|
||||||
// never consider anything that might've been selected before.
|
|
||||||
locks := depsfile.NewLocks()
|
|
||||||
|
|
||||||
_, err = installer.EnsureProviderVersions(ctx, locks, req, mode)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user