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.
This commit is contained in:
Quentin Machu 2017-03-16 11:51:51 +01:00
parent 0bd8c7acb2
commit bf8d932d23
No known key found for this signature in database
GPG Key ID: EC27C261A31A007F
10 changed files with 264 additions and 0 deletions

View File

@ -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,
})
}

View File

@ -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(),
},
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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")
},
})
}
}

View File

@ -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,

View File

@ -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" {}
```

View File

@ -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.

View File

@ -304,6 +304,10 @@
<a href="/docs/providers/librato/index.html">Librato</a>
</li>
<li<%= sidebar_current("docs-providers-local") %>>
<a href="/docs/providers/local/index.html">Local</a>
</li>
<li<%= sidebar_current("docs-providers-logentries") %>>
<a href="/docs/providers/logentries/index.html">Logentries</a>
</li>

View File

@ -0,0 +1,24 @@
<% wrap_layout :inner do %>
<% content_for :sidebar do %>
<ul class="nav docs-sidenav">
<li<%= sidebar_current("docs-home") %>>
<a href="/docs/providers/index.html">All Providers</a>
</li>
<li<%= sidebar_current("docs-local-index") %>>
<a href="/docs/providers/local/index.html">Local Provider</a>
</li>
<li<%= sidebar_current("docs-local-resource") %>>
<a href="#">Resources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-local-resource-file") %>>
<a href="/docs/providers/local/r/file.html">file</a>
</li>
</ul>
</li>
</ul>
<% end %>
<%= yield %>
<% end %>