From bf8d932d233fbc764c5cc9660c1be7310853e3ae Mon Sep 17 00:00:00 2001 From: Quentin Machu Date: Thu, 16 Mar 2017 11:51:51 +0100 Subject: [PATCH 1/2] provider/local: Implement a new local_file resource This commit adds the ability to provision files locally. This is useful for cases where TerraForm generates assets such as TLS certificates or templated documents that need to be saved locally. - While output variables can be used to return values to the user, it is not extremly suitable for large content or when many of these are generated, nor is it practical for operators to manually save them on disk. - While `local-exec` could be used with an `echo`, this provider works across platforms and do not require any convoluted escaping. --- builtin/bins/provider-localfile/main.go | 12 +++ builtin/providers/local/provider.go | 15 ++++ builtin/providers/local/provider_test.go | 18 ++++ .../providers/local/resource_local_file.go | 84 +++++++++++++++++++ .../local/resource_local_file_test.go | 56 +++++++++++++ command/internal_plugin_list.go | 2 + .../docs/providers/local/index.html.markdown | 19 +++++ .../docs/providers/local/r/file.html.md | 30 +++++++ website/source/layouts/docs.erb | 4 + website/source/layouts/local.erb | 24 ++++++ 10 files changed, 264 insertions(+) create mode 100644 builtin/bins/provider-localfile/main.go create mode 100644 builtin/providers/local/provider.go create mode 100644 builtin/providers/local/provider_test.go create mode 100644 builtin/providers/local/resource_local_file.go create mode 100644 builtin/providers/local/resource_local_file_test.go create mode 100644 website/source/docs/providers/local/index.html.markdown create mode 100644 website/source/docs/providers/local/r/file.html.md create mode 100644 website/source/layouts/local.erb diff --git a/builtin/bins/provider-localfile/main.go b/builtin/bins/provider-localfile/main.go new file mode 100644 index 000000000..4a98ecfdd --- /dev/null +++ b/builtin/bins/provider-localfile/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/localfile" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: localfile.Provider, + }) +} diff --git a/builtin/providers/local/provider.go b/builtin/providers/local/provider.go new file mode 100644 index 000000000..ee048c689 --- /dev/null +++ b/builtin/providers/local/provider.go @@ -0,0 +1,15 @@ +package local + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{}, + ResourcesMap: map[string]*schema.Resource{ + "local_file": resourceLocalFile(), + }, + } +} diff --git a/builtin/providers/local/provider_test.go b/builtin/providers/local/provider_test.go new file mode 100644 index 000000000..7385ffe3a --- /dev/null +++ b/builtin/providers/local/provider_test.go @@ -0,0 +1,18 @@ +package local + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testProviders = map[string]terraform.ResourceProvider{ + "local": Provider(), +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} diff --git a/builtin/providers/local/resource_local_file.go b/builtin/providers/local/resource_local_file.go new file mode 100644 index 000000000..6f6da1b94 --- /dev/null +++ b/builtin/providers/local/resource_local_file.go @@ -0,0 +1,84 @@ +package local + +import ( + "crypto/sha1" + "encoding/hex" + "io/ioutil" + "os" + "path" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceLocalFile() *schema.Resource { + return &schema.Resource{ + Create: resourceLocalFileCreate, + Read: resourceLocalFileRead, + Delete: resourceLocalFileDelete, + + Schema: map[string]*schema.Schema{ + "content": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "filename": { + Type: schema.TypeString, + Description: "Path to the output file", + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceLocalFileRead(d *schema.ResourceData, _ interface{}) error { + // If the output file doesn't exist, mark the resource for creation. + outputPath := d.Get("filename").(string) + if _, err := os.Stat(outputPath); os.IsNotExist(err) { + d.SetId("") + return nil + } + + // Verify that the content of the destination file matches the content we + // expect. Otherwise, the file might have been modified externally and we + // must reconcile. + outputContent, err := ioutil.ReadFile(outputPath) + if err != nil { + return err + } + + outputChecksum := sha1.Sum([]byte(outputContent)) + if hex.EncodeToString(outputChecksum[:]) != d.Id() { + d.SetId("") + return nil + } + + return nil +} + +func resourceLocalFileCreate(d *schema.ResourceData, _ interface{}) error { + content := d.Get("content").(string) + destination := d.Get("filename").(string) + + destinationDir := path.Dir(destination) + if _, err := os.Stat(destinationDir); err != nil { + if err := os.MkdirAll(destinationDir, 0777); err != nil { + return err + } + } + + if err := ioutil.WriteFile(destination, []byte(content), 0777); err != nil { + return err + } + + checksum := sha1.Sum([]byte(content)) + d.SetId(hex.EncodeToString(checksum[:])) + + return nil +} + +func resourceLocalFileDelete(d *schema.ResourceData, _ interface{}) error { + os.Remove(d.Get("filename").(string)) + return nil +} diff --git a/builtin/providers/local/resource_local_file_test.go b/builtin/providers/local/resource_local_file_test.go new file mode 100644 index 000000000..33a44f0bb --- /dev/null +++ b/builtin/providers/local/resource_local_file_test.go @@ -0,0 +1,56 @@ +package local + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "testing" + + r "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestLocalFile_Basic(t *testing.T) { + var cases = []struct { + path string + content string + config string + }{ + { + "local_file", + "This is some content", + `resource "local_file" "file" { + content = "This is some content" + filename = "local_file" + }`, + }, + } + + for _, tt := range cases { + r.UnitTest(t, r.TestCase{ + Providers: testProviders, + Steps: []r.TestStep{ + { + Config: tt.config, + Check: func(s *terraform.State) error { + content, err := ioutil.ReadFile(tt.path) + if err != nil { + return fmt.Errorf("config:\n%s\n,got: %s\n", tt.config, err) + } + if string(content) != tt.content { + return fmt.Errorf("config:\n%s\ngot:\n%s\nwant:\n%s\n", tt.config, content, tt.content) + } + return nil + }, + }, + }, + CheckDestroy: func(*terraform.State) error { + if _, err := os.Stat(tt.path); os.IsNotExist(err) { + return nil + } + return errors.New("local_file did not get destroyed") + }, + }) + } +} diff --git a/command/internal_plugin_list.go b/command/internal_plugin_list.go index 2f48908c7..83d4a4a30 100644 --- a/command/internal_plugin_list.go +++ b/command/internal_plugin_list.go @@ -39,6 +39,7 @@ import ( influxdbprovider "github.com/hashicorp/terraform/builtin/providers/influxdb" kubernetesprovider "github.com/hashicorp/terraform/builtin/providers/kubernetes" libratoprovider "github.com/hashicorp/terraform/builtin/providers/librato" + localprovider "github.com/hashicorp/terraform/builtin/providers/local" logentriesprovider "github.com/hashicorp/terraform/builtin/providers/logentries" mailgunprovider "github.com/hashicorp/terraform/builtin/providers/mailgun" mysqlprovider "github.com/hashicorp/terraform/builtin/providers/mysql" @@ -115,6 +116,7 @@ var InternalProviders = map[string]plugin.ProviderFunc{ "influxdb": influxdbprovider.Provider, "kubernetes": kubernetesprovider.Provider, "librato": libratoprovider.Provider, + "local": localprovider.Provider, "logentries": logentriesprovider.Provider, "mailgun": mailgunprovider.Provider, "mysql": mysqlprovider.Provider, diff --git a/website/source/docs/providers/local/index.html.markdown b/website/source/docs/providers/local/index.html.markdown new file mode 100644 index 000000000..cc83fd241 --- /dev/null +++ b/website/source/docs/providers/local/index.html.markdown @@ -0,0 +1,19 @@ +--- +layout: "local" +page_title: "Provider: Local" +sidebar_current: "docs-local-index" +description: |- + The Local provider is used to manage local resources (i.e. files). +--- + +# Local Provider + +The Local provider is used to manage local resources (i.e. files). + +Use the navigation to the left to read about the available resources. + +## Example Usage + +``` +provider "local" {} +``` diff --git a/website/source/docs/providers/local/r/file.html.md b/website/source/docs/providers/local/r/file.html.md new file mode 100644 index 000000000..f0e3c8628 --- /dev/null +++ b/website/source/docs/providers/local/r/file.html.md @@ -0,0 +1,30 @@ +--- +layout: "local" +page_title: "Local: local_file" +sidebar_current: "docs-local-resource-file" +description: |- + Generates a local file from content. +--- + +# local\_file + +Generates a local file from a given content. + +## Example Usage + +``` +data "local_file" "foo" { + content = "foo!" + filename = "${path.module}/foo.bar" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `content` - (required) The content of file to create. + +* `filename` - (required) The path of the file to create. + +NOTE: Any required parent folders are created automatically. Additionally, any existing file will get overwritten. \ No newline at end of file diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 347eefbe4..868ce88e1 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -304,6 +304,10 @@ Librato + > + Local + + > Logentries diff --git a/website/source/layouts/local.erb b/website/source/layouts/local.erb new file mode 100644 index 000000000..bf4f56acc --- /dev/null +++ b/website/source/layouts/local.erb @@ -0,0 +1,24 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + + <%= yield %> + <% end %> \ No newline at end of file From 4d79e0b99c237344a10ff7759d62f777d560c5bd Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Mon, 17 Apr 2017 10:45:10 -0700 Subject: [PATCH 2/2] website: documentation tweaks for the local_file resource and its provider --- .../docs/providers/local/index.html.markdown | 15 ++++++------- .../docs/providers/local/r/file.html.md | 21 ++++++++++++------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/website/source/docs/providers/local/index.html.markdown b/website/source/docs/providers/local/index.html.markdown index cc83fd241..8c007c15a 100644 --- a/website/source/docs/providers/local/index.html.markdown +++ b/website/source/docs/providers/local/index.html.markdown @@ -3,17 +3,18 @@ layout: "local" page_title: "Provider: Local" sidebar_current: "docs-local-index" description: |- - The Local provider is used to manage local resources (i.e. files). + The Local provider is used to manage local resources, such as files. --- # Local Provider -The Local provider is used to manage local resources (i.e. files). +The Local provider is used to manage local resources, such as files. Use the navigation to the left to read about the available resources. -## Example Usage - -``` -provider "local" {} -``` +~> **Note** Terraform primarily deals with remote resources which are able +to outlive a single Terraform run, and so local resources can sometimes violate +its assumptions. The resources here are best used with care, since depending +on local state can make it hard to apply the same Terraform configuration on +many different local systems where the local resources may not be universally +available. See specific notes in each resource for more information. diff --git a/website/source/docs/providers/local/r/file.html.md b/website/source/docs/providers/local/r/file.html.md index f0e3c8628..83ac2a325 100644 --- a/website/source/docs/providers/local/r/file.html.md +++ b/website/source/docs/providers/local/r/file.html.md @@ -6,14 +6,20 @@ description: |- Generates a local file from content. --- -# local\_file +# local_file -Generates a local file from a given content. +Generates a local file with the given content. + +~> **Note** When working with local files, Terraform will detect the resource +as having been deleted each time a configuration is applied on a new machine +where the file is not present and will generate a diff to re-create it. This +may cause "noise" in diffs in environments where configurations are routinely +applied by many different users or within automation systems. ## Example Usage -``` -data "local_file" "foo" { +```hcl +resource "local_file" "foo" { content = "foo!" filename = "${path.module}/foo.bar" } @@ -23,8 +29,9 @@ data "local_file" "foo" { The following arguments are supported: -* `content` - (required) The content of file to create. +* `content` - (Required) The content of file to create. -* `filename` - (required) The path of the file to create. +* `filename` - (Required) The path of the file to create. -NOTE: Any required parent folders are created automatically. Additionally, any existing file will get overwritten. \ No newline at end of file +Any required parent directories will be created automatically, and any existing +file with the given name will be overwritten.