tools/terraform-bundle: accept custom plugins from a local directory
To make it easier to include third-party plugins in generated bundles, we'll now search a local directory for available plugins and skip attempting to install from releases.hashicorp.com if a dependency can be satisfied locally.
This commit is contained in:
parent
a20dbb4378
commit
60bc16305a
|
@ -53,6 +53,12 @@ providers {
|
||||||
# two expressions match different versions then _both_ are included in
|
# two expressions match different versions then _both_ are included in
|
||||||
# the bundle archive.
|
# the bundle archive.
|
||||||
google = ["~> 1.0", "~> 2.0"]
|
google = ["~> 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 build with the operating
|
||||||
|
# system and architecture that terraform enterprise is running, e.g. linux and amd64
|
||||||
|
customplugin = ["0.1"]
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -100,6 +106,13 @@ this composite version number so that bundle archives can be easily
|
||||||
distinguished from official release archives and from each other when multiple
|
distinguished from official release archives and from each other when multiple
|
||||||
bundles contain the same core Terraform version.
|
bundles contain the same core Terraform version.
|
||||||
|
|
||||||
|
To include custom plugins in the bundle file, create a local directory "./plugins"
|
||||||
|
and put all the plugins you want to include there. 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-*-v*". In
|
||||||
|
addition, ensure that the plugin is build using the same operating system and
|
||||||
|
architecture used for terraform enterprise. Typically this will be linux and amd64.
|
||||||
|
|
||||||
## Provider Resolution Behavior
|
## Provider Resolution Behavior
|
||||||
|
|
||||||
Terraform's provider resolution behavior is such that if a given constraint
|
Terraform's provider resolution behavior is such that if a given constraint
|
||||||
|
@ -112,13 +125,6 @@ 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
|
found in the bundle, Terraform _will_ attempt to satisfy that dependency by
|
||||||
automatic installation from the official repository.
|
automatic installation from the official repository.
|
||||||
|
|
||||||
To disable automatic installation altogether -- and thus cause a hard failure
|
|
||||||
if no local plugins match -- the `-plugin-dir` option can be passed to
|
|
||||||
`terraform init`, giving the directory into which the bundle was extracted.
|
|
||||||
The presence of this option overrides all of the normal automatic discovery
|
|
||||||
and installation behavior, and thus forces the use of only the plugins that
|
|
||||||
can be found in the directory indicated.
|
|
||||||
|
|
||||||
The downloaded provider archives are verified using the same signature check
|
The downloaded provider archives are verified using the same signature check
|
||||||
that is used for auto-installed plugins, using Hashicorp's release key. At
|
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;
|
this time, the core Terraform archive itself is _not_ verified in this way;
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
|
|
||||||
getter "github.com/hashicorp/go-getter"
|
getter "github.com/hashicorp/go-getter"
|
||||||
"github.com/hashicorp/terraform/plugin"
|
"github.com/hashicorp/terraform/plugin"
|
||||||
"github.com/hashicorp/terraform/plugin/discovery"
|
discovery "github.com/hashicorp/terraform/plugin/discovery"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,10 +23,66 @@ type PackageCommand struct {
|
||||||
ui cli.Ui
|
ui cli.Ui
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shameless stackoverflow copy + pasta https://stackoverflow.com/questions/21060945/simple-way-to-copy-a-file-in-golang
|
||||||
|
func CopyFile(src, dst string) (err error) {
|
||||||
|
sfi, err := os.Stat(src)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !sfi.Mode().IsRegular() {
|
||||||
|
// cannot copy non-regular files (e.g., directories,
|
||||||
|
// symlinks, devices, etc.)
|
||||||
|
return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
|
||||||
|
}
|
||||||
|
dfi, err := os.Stat(dst)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !(dfi.Mode().IsRegular()) {
|
||||||
|
return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
|
||||||
|
}
|
||||||
|
if os.SameFile(sfi, dfi) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = os.Link(src, dst); err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = copyFileContents(src, dst)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// see above
|
||||||
|
func copyFileContents(src, dst string) (err error) {
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
out, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
cerr := out.Close()
|
||||||
|
if err == nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if _, err = io.Copy(out, in); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = out.Sync()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (c *PackageCommand) Run(args []string) int {
|
func (c *PackageCommand) Run(args []string) int {
|
||||||
flags := flag.NewFlagSet("package", flag.ExitOnError)
|
flags := flag.NewFlagSet("package", flag.ExitOnError)
|
||||||
osPtr := flags.String("os", "", "Target operating system")
|
osPtr := flags.String("os", "", "Target operating system")
|
||||||
archPtr := flags.String("arch", "", "Target CPU architecture")
|
archPtr := flags.String("arch", "", "Target CPU architecture")
|
||||||
|
pluginDirPtr := flags.String("plugin-dir", "", "Path to custom plugins directory")
|
||||||
err := flags.Parse(args)
|
err := flags.Parse(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ui.Error(err.Error())
|
c.ui.Error(err.Error())
|
||||||
|
@ -35,12 +91,16 @@ func (c *PackageCommand) Run(args []string) int {
|
||||||
|
|
||||||
osName := runtime.GOOS
|
osName := runtime.GOOS
|
||||||
archName := runtime.GOARCH
|
archName := runtime.GOARCH
|
||||||
|
pluginDir := "./plugins"
|
||||||
if *osPtr != "" {
|
if *osPtr != "" {
|
||||||
osName = *osPtr
|
osName = *osPtr
|
||||||
}
|
}
|
||||||
if *archPtr != "" {
|
if *archPtr != "" {
|
||||||
archName = *archPtr
|
archName = *archPtr
|
||||||
}
|
}
|
||||||
|
if *pluginDirPtr != "" {
|
||||||
|
pluginDir = *pluginDirPtr
|
||||||
|
}
|
||||||
|
|
||||||
if flags.NArg() != 1 {
|
if flags.NArg() != 1 {
|
||||||
c.ui.Error("Configuration filename is required")
|
c.ui.Error("Configuration filename is required")
|
||||||
|
@ -70,10 +130,17 @@ func (c *PackageCommand) Run(args []string) int {
|
||||||
|
|
||||||
coreZipURL := c.coreURL(config.Terraform.Version, osName, archName)
|
coreZipURL := c.coreURL(config.Terraform.Version, osName, archName)
|
||||||
err = getter.Get(workDir, coreZipURL)
|
err = getter.Get(workDir, coreZipURL)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ui.Error(fmt.Sprintf("Failed to fetch core package from %s: %s", coreZipURL, err))
|
c.ui.Error(fmt.Sprintf("Failed to fetch core package from %s: %s", coreZipURL, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.ui.Info(fmt.Sprintf("Fetching 3rd party plugins in directory: %s", pluginDir))
|
||||||
|
dirs := []string{pluginDir} //FindPlugins requires an array
|
||||||
|
localPlugins := discovery.FindPlugins("provider", dirs)
|
||||||
|
for k, _ := range localPlugins {
|
||||||
|
c.ui.Info(fmt.Sprintf("plugin: %s (%s)", k.Name, k.Version))
|
||||||
|
}
|
||||||
installer := &discovery.ProviderInstaller{
|
installer := &discovery.ProviderInstaller{
|
||||||
Dir: workDir,
|
Dir: workDir,
|
||||||
|
|
||||||
|
@ -92,22 +159,32 @@ func (c *PackageCommand) Run(args []string) int {
|
||||||
Ui: c.ui,
|
Ui: c.ui,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(config.Providers) > 0 {
|
for name, constraintStrs := range config.Providers {
|
||||||
c.ui.Output(fmt.Sprintf("Checking for available provider plugins on %s...",
|
for _, constraintStr := range constraintStrs {
|
||||||
discovery.GetReleaseHost()))
|
c.ui.Output(fmt.Sprintf("- Resolving %q provider (%s)...",
|
||||||
|
name, constraintStr))
|
||||||
|
foundPlugins := discovery.PluginMetaSet{}
|
||||||
|
constraint := constraintStr.MustParse()
|
||||||
|
for plugin, _ := range localPlugins {
|
||||||
|
if plugin.Name == name && constraint.Allows(plugin.Version.MustParse()) {
|
||||||
|
foundPlugins.Add(plugin)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, constraints := range config.Providers {
|
if len(foundPlugins) > 0 {
|
||||||
for _, constraint := range constraints {
|
plugin := foundPlugins.Newest()
|
||||||
c.ui.Output(fmt.Sprintf("- Resolving %q provider (%s)...",
|
CopyFile(plugin.Path, workDir+"/terraform-provider-"+plugin.Name+"-v"+plugin.Version.MustParse().String()) //put into temp dir
|
||||||
name, constraint))
|
} else { //attempt to get from the public registry if not found locally
|
||||||
_, err := installer.Get(name, constraint.MustParse())
|
c.ui.Output(fmt.Sprintf("- Checking for provider plugin on %s...",
|
||||||
|
discovery.GetReleaseHost()))
|
||||||
|
_, err := installer.Get(name, constraint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ui.Error(fmt.Sprintf("- Failed to resolve %s provider %s: %s", name, constraint, err))
|
c.ui.Error(fmt.Sprintf("- Failed to resolve %s provider %s: %s", name, constraint, err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(workDir)
|
files, err := ioutil.ReadDir(workDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -208,6 +285,8 @@ Options:
|
||||||
-arch=name Target CPU architecture the archive will be built for. Defaults
|
-arch=name Target CPU architecture the archive will be built for. Defaults
|
||||||
to that of the system where the command is being run.
|
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
|
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
|
a fixed set of providers together on a server, so that Terraform's provider
|
||||||
auto-installation mechanism can be avoided.
|
auto-installation mechanism can be avoided.
|
||||||
|
@ -234,6 +313,12 @@ not a normal Terraform configuration file. The file format looks like this:
|
||||||
# two expressions match different versions then _both_ are included in
|
# two expressions match different versions then _both_ are included in
|
||||||
# the bundle archive.
|
# the bundle archive.
|
||||||
google = ["~> 1.0", "~> 2.0"]
|
google = ["~> 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-*-v*, and must be built with the operating
|
||||||
|
#system and architecture that terraform enterprise is running, e.g. linux and amd64
|
||||||
|
customplugin = ["0.1"]
|
||||||
}
|
}
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
Loading…
Reference in New Issue