From bf8d932d233fbc764c5cc9660c1be7310853e3ae Mon Sep 17 00:00:00 2001 From: Quentin Machu Date: Thu, 16 Mar 2017 11:51:51 +0100 Subject: [PATCH] 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