Merge branch 'master' into b-aws-sqs-defaults

This commit is contained in:
stack72 2016-07-26 21:51:48 +01:00
commit 3a1f2e6fe9
No known key found for this signature in database
GPG Key ID: 8619A619B085CB16
53 changed files with 2956 additions and 148 deletions

View File

@ -8,7 +8,7 @@ BACKWARDS INCOMPATIBILITIES / NOTES:
* Quotation marks may no longer be escaped in HIL expressions [GH-7201]
* `openstack_networking_subnet_v2` now defaults to turning DHCP on.
* `aws_elb` now defaults `cross_zone_load_balancing` to `true`
* `resource_aws_instance`: EC2 Classic users may continue to use
* `aws_instance`: EC2 Classic users may continue to use
`security_groups` to reference Security Groups by their `name`. Users who are
managing Instances inside VPCs will need to use `vpc_security_group_ids` instead,
and reference the security groups by their `id`.
@ -21,6 +21,8 @@ BACKWARDS INCOMPATIBILITIES / NOTES:
* `azurerm_dns_cname_record` now accepts a single record rather than a list of records
* `azurerm_virtual_machine` computer_name now Required
* `aws_db_instance` now defaults `publicly_accessible` to false
* `keep_updated` parameter removed from `docker_image` - This parameter never did what it was supposed to do.
See relevant docs, specifically `pull_trigger` & new `docker_registry_image` data source to understand how to keep your `docker_image` updated.
* `openstack_fw_policy_v1` now correctly applies rules in the order they are specified. Upon the next apply, current rules might be re-ordered.
* `atlas_artifact` resource has be deprecated. Please use the new `atlas_artifact` Data Source
* The `member` attribute of `openstack_lb_pool_v1` has been deprecated. Please ue the new `openstack_lb_member_v1` resource.
@ -41,6 +43,8 @@ FEATURES:
* **New Data Source:** `aws_s3_bucket_object` [GH-6946]
* **New Data Source:** `aws_ecs_container_definition` [GH-7230]
* **New Data Source:** `atlas_artifact` [GH-7419]
* **New Data Source:** `docker_registry_image` [GH-7000]
* **New Data Source:** `consul_keys` [GH-7678]
* **New Interpolation Function:** `sort` [GH-7128]
* **New Interpolation Function:** `distinct` [GH-7174]
* **New Provider:** `grafana` [GH-6206]
@ -79,6 +83,12 @@ FEATURES:
* **New Resource:** `datadog_timeboard` [GH-6900]
* **New Resource:** `digitalocean_tag` [GH-7500]
* **New Resource:** `digitalocean_volume` [GH-7560]
* **New Resource:** `consul_agent_service` [GH-7508]
* **New Resource:** `consul_catalog_entry` [GH-7508]
* **New Resource:** `consul_node` [GH-7508]
* **New Resource:** `consul_service` [GH-7508]
* **New Resource:** `mysql_grant` [GH-7656]
* **New Resource:** `mysql_user` [GH-7656]
* core: Tainted resources now show up in the plan and respect dependency ordering [GH-6600]
* core: The `lookup` interpolation function can now have a default fall-back value specified [GH-6884]
* core: The `terraform plan` command no longer persists state. [GH-6811]
@ -148,6 +158,7 @@ IMPROVEMENTS:
* provider/datadog: Add support for 'require full window' and 'locked' [GH-6738]
* provider/docker: Docker Container DNS Setting Enhancements [GH-7392]
* provider/docker: Add `destroy_grace_seconds` option to stop container before delete [GH-7513]
* provider/docker: Add `pull_trigger` option to `docker_image` to trigger pulling layers of a given image [GH-7000]
* provider/fastly: Add support for Cache Settings [GH-6781]
* provider/fastly: Add support for Service Request Settings on `fastly_service_v1` resources [GH-6622]
* provider/fastly: Add support for custom VCL configuration [GH-6662]
@ -227,6 +238,7 @@ BUG FIXES:
* provider/aws: Fix bug with Updating `aws_autoscaling_group` `enabled_metrics` [GH-7698]
* provider/aws: Ignore IOPS on non io1 AWS root_block_device [GH-7783]
* provider/aws: Ignore missing ENI attachment when trying to detach ENI [GH-7185]
* provider/aws: Fix issue updating ElasticBeanstalk Environment templates [GH-7811]
* provider/azurerm: Fixes terraform crash when using SSH keys with `azurerm_virtual_machine` [GH-6766]
* provider/azurerm: Fix a bug causing 'diffs do not match' on `azurerm_network_interface` resources [GH-6790]
* provider/azurerm: Normalizes `availability_set_id` casing to avoid spurious diffs in `azurerm_virtual_machine` [GH-6768]

View File

@ -311,7 +311,9 @@ func resourceAwsElasticBeanstalkEnvironmentUpdate(d *schema.ResourceData, meta i
if d.HasChange("solution_stack_name") {
hasChange = true
updateOpts.SolutionStackName = aws.String(d.Get("solution_stack_name").(string))
if v, ok := d.GetOk("solution_stack_name"); ok {
updateOpts.SolutionStackName = aws.String(v.(string))
}
}
if d.HasChange("setting") {
@ -332,7 +334,9 @@ func resourceAwsElasticBeanstalkEnvironmentUpdate(d *schema.ResourceData, meta i
if d.HasChange("template_name") {
hasChange = true
updateOpts.TemplateName = aws.String(d.Get("template_name").(string))
if v, ok := d.GetOk("template_name"); ok {
updateOpts.TemplateName = aws.String(v.(string))
}
}
if hasChange {

View File

@ -178,6 +178,40 @@ func TestAccAWSBeanstalkEnv_vpc(t *testing.T) {
})
}
func TestAccAWSBeanstalkEnv_template_change(t *testing.T) {
var app elasticbeanstalk.EnvironmentDescription
rInt := acctest.RandInt()
resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
},
Providers: testAccProviders,
CheckDestroy: testAccCheckBeanstalkEnvDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccBeanstalkEnv_TemplateChange_stack(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckBeanstalkEnvExists("aws_elastic_beanstalk_environment.environment", &app),
),
},
resource.TestStep{
Config: testAccBeanstalkEnv_TemplateChange_temp(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckBeanstalkEnvExists("aws_elastic_beanstalk_environment.environment", &app),
),
},
resource.TestStep{
Config: testAccBeanstalkEnv_TemplateChange_stack(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckBeanstalkEnvExists("aws_elastic_beanstalk_environment.environment", &app),
),
},
},
})
}
func testAccCheckBeanstalkEnvDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).elasticbeanstalkconn
@ -571,3 +605,63 @@ resource "aws_elastic_beanstalk_environment" "default" {
}
`, name)
}
func testAccBeanstalkEnv_TemplateChange_stack(r int) string {
return fmt.Sprintf(`
provider "aws" {
region = "us-east-1"
}
resource "aws_elastic_beanstalk_application" "app" {
name = "beanstalk-app-%d"
description = ""
}
resource "aws_elastic_beanstalk_environment" "environment" {
name = "beanstalk-env-%d"
application = "${aws_elastic_beanstalk_application.app.name}"
# Go 1.4
solution_stack_name = "64bit Amazon Linux 2016.03 v2.1.0 running Go 1.4"
}
resource "aws_elastic_beanstalk_configuration_template" "template" {
name = "beanstalk-config-%d"
application = "${aws_elastic_beanstalk_application.app.name}"
# Go 1.5
solution_stack_name = "64bit Amazon Linux 2016.03 v2.1.3 running Go 1.5"
}
`, r, r, r)
}
func testAccBeanstalkEnv_TemplateChange_temp(r int) string {
return fmt.Sprintf(`
provider "aws" {
region = "us-east-1"
}
resource "aws_elastic_beanstalk_application" "app" {
name = "beanstalk-app-%d"
description = ""
}
resource "aws_elastic_beanstalk_environment" "environment" {
name = "beanstalk-env-%d"
application = "${aws_elastic_beanstalk_application.app.name}"
# Go 1.4
template_name = "${aws_elastic_beanstalk_configuration_template.template.name}"
}
resource "aws_elastic_beanstalk_configuration_template" "template" {
name = "beanstalk-config-%d"
application = "${aws_elastic_beanstalk_application.app.name}"
# Go 1.5
solution_stack_name = "64bit Amazon Linux 2016.03 v2.1.3 running Go 1.5"
}
`, r, r, r)
}

View File

@ -9,6 +9,7 @@ import (
type Config struct {
Datacenter string `mapstructure:"datacenter"`
Address string `mapstructure:"address"`
Token string `mapstructure:"token"`
Scheme string `mapstructure:"scheme"`
}
@ -25,6 +26,9 @@ func (c *Config) Client() (*consulapi.Client, error) {
if c.Scheme != "" {
config.Scheme = c.Scheme
}
if c.Token != "" {
config.Token = c.Token
}
client, err := consulapi.NewClient(config)
log.Printf("[INFO] Consul Client configured with address: '%s', scheme: '%s', datacenter: '%s'",

View File

@ -0,0 +1,96 @@
package consul
import (
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/helper/schema"
)
func dataSourceConsulKeys() *schema.Resource {
return &schema.Resource{
Read: dataSourceConsulKeysRead,
Schema: map[string]*schema.Schema{
"datacenter": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"key": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"path": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"default": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
},
},
"var": &schema.Schema{
Type: schema.TypeMap,
Computed: true,
},
},
}
}
func dataSourceConsulKeysRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
kv := client.KV()
token := d.Get("token").(string)
dc, err := getDC(d, client)
if err != nil {
return err
}
keyClient := newKeyClient(kv, dc, token)
vars := make(map[string]string)
keys := d.Get("key").(*schema.Set).List()
for _, raw := range keys {
key, path, sub, err := parseKey(raw)
if err != nil {
return err
}
value, err := keyClient.Get(path)
if err != nil {
return err
}
value = attributeValue(sub, value)
vars[key] = value
}
if err := d.Set("var", vars); err != nil {
return err
}
// Store the datacenter on this resource, which can be helpful for reference
// in case it was read from the provider
d.Set("datacenter", dc)
d.SetId("-")
return nil
}

View File

@ -0,0 +1,44 @@
package consul
import (
"testing"
"github.com/hashicorp/terraform/helper/resource"
)
func TestAccDataConsulKeys_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDataConsulKeysConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckConsulKeysValue("data.consul_keys.read", "read", "written"),
),
},
},
})
}
const testAccDataConsulKeysConfig = `
resource "consul_keys" "write" {
datacenter = "dc1"
key {
path = "test/data_source"
value = "written"
}
}
data "consul_keys" "read" {
# Create a dependency on the resource so we're sure to
# have the value in place before we try to read it.
datacenter = "${consul_keys.write.datacenter}"
key {
path = "test/data_source"
name = "read"
}
}
`

View File

@ -0,0 +1,139 @@
package consul
import (
"fmt"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceConsulAgentService() *schema.Resource {
return &schema.Resource{
Create: resourceConsulAgentServiceCreate,
Update: resourceConsulAgentServiceCreate,
Read: resourceConsulAgentServiceRead,
Delete: resourceConsulAgentServiceDelete,
Schema: map[string]*schema.Schema{
"address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"port": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"tags": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
ForceNew: true,
},
},
}
}
func resourceConsulAgentServiceCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
agent := client.Agent()
name := d.Get("name").(string)
registration := consulapi.AgentServiceRegistration{Name: name}
if address, ok := d.GetOk("address"); ok {
registration.Address = address.(string)
}
if port, ok := d.GetOk("port"); ok {
registration.Port = port.(int)
}
if v, ok := d.GetOk("tags"); ok {
vs := v.([]interface{})
s := make([]string, len(vs))
for i, raw := range vs {
s[i] = raw.(string)
}
registration.Tags = s
}
if err := agent.ServiceRegister(&registration); err != nil {
return fmt.Errorf("Failed to register service '%s' with Consul agent: %v", name, err)
}
// Update the resource
if serviceMap, err := agent.Services(); err != nil {
return fmt.Errorf("Failed to read services from Consul agent: %v", err)
} else if service, ok := serviceMap[name]; !ok {
return fmt.Errorf("Failed to read service '%s' from Consul agent: %v", name, err)
} else {
d.Set("address", service.Address)
d.Set("id", service.ID)
d.SetId(service.ID)
d.Set("name", service.Service)
d.Set("port", service.Port)
tags := make([]string, 0, len(service.Tags))
for _, tag := range service.Tags {
tags = append(tags, tag)
}
d.Set("tags", tags)
}
return nil
}
func resourceConsulAgentServiceRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
agent := client.Agent()
name := d.Get("name").(string)
if services, err := agent.Services(); err != nil {
return fmt.Errorf("Failed to get services from Consul agent: %v", err)
} else if service, ok := services[name]; !ok {
d.Set("id", "")
} else {
d.Set("address", service.Address)
d.Set("id", service.ID)
d.SetId(service.ID)
d.Set("name", service.Service)
d.Set("port", service.Port)
tags := make([]string, 0, len(service.Tags))
for _, tag := range service.Tags {
tags = append(tags, tag)
}
d.Set("tags", tags)
}
return nil
}
func resourceConsulAgentServiceDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
catalog := client.Agent()
id := d.Get("id").(string)
if err := catalog.ServiceDeregister(id); err != nil {
return fmt.Errorf("Failed to deregister service '%s' from Consul agent: %v", id, err)
}
// Clear the ID
d.SetId("")
return nil
}

View File

@ -0,0 +1,90 @@
package consul
import (
"fmt"
"testing"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccConsulAgentService_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() {},
Providers: testAccProviders,
CheckDestroy: testAccCheckConsulAgentServiceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccConsulAgentServiceConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckConsulAgentServiceExists(),
testAccCheckConsulAgentServiceValue("consul_agent_service.app", "address", "www.google.com"),
testAccCheckConsulAgentServiceValue("consul_agent_service.app", "id", "google"),
testAccCheckConsulAgentServiceValue("consul_agent_service.app", "name", "google"),
testAccCheckConsulAgentServiceValue("consul_agent_service.app", "port", "80"),
testAccCheckConsulAgentServiceValue("consul_agent_service.app", "tags.#", "2"),
testAccCheckConsulAgentServiceValue("consul_agent_service.app", "tags.0", "tag0"),
testAccCheckConsulAgentServiceValue("consul_agent_service.app", "tags.1", "tag1"),
),
},
},
})
}
func testAccCheckConsulAgentServiceDestroy(s *terraform.State) error {
agent := testAccProvider.Meta().(*consulapi.Client).Agent()
services, err := agent.Services()
if err != nil {
return fmt.Errorf("Could not retrieve services: %#v", err)
}
_, ok := services["google"]
if ok {
return fmt.Errorf("Service still exists: %#v", "google")
}
return nil
}
func testAccCheckConsulAgentServiceExists() resource.TestCheckFunc {
return func(s *terraform.State) error {
agent := testAccProvider.Meta().(*consulapi.Client).Agent()
services, err := agent.Services()
if err != nil {
return err
}
_, ok := services["google"]
if !ok {
return fmt.Errorf("Service does not exist: %#v", "google")
}
return nil
}
}
func testAccCheckConsulAgentServiceValue(n, attr, val string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rn, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Resource not found")
}
out, ok := rn.Primary.Attributes[attr]
if !ok {
return fmt.Errorf("Attribute '%s' not found: %#v", attr, rn.Primary.Attributes)
}
if val != "<any>" && out != val {
return fmt.Errorf("Attribute '%s' value '%s' != '%s'", attr, out, val)
}
if val == "<any>" && out == "" {
return fmt.Errorf("Attribute '%s' value '%s'", attr, out)
}
return nil
}
}
const testAccConsulAgentServiceConfig = `
resource "consul_agent_service" "app" {
address = "www.google.com"
name = "google"
port = 80
tags = ["tag0", "tag1"]
}
`

View File

@ -0,0 +1,270 @@
package consul
import (
"bytes"
"fmt"
"sort"
"strings"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceConsulCatalogEntry() *schema.Resource {
return &schema.Resource{
Create: resourceConsulCatalogEntryCreate,
Update: resourceConsulCatalogEntryCreate,
Read: resourceConsulCatalogEntryRead,
Delete: resourceConsulCatalogEntryDelete,
Schema: map[string]*schema.Schema{
"address": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"datacenter": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"node": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"service": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"port": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"tags": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: resourceConsulCatalogEntryServiceTagsHash,
},
},
},
Set: resourceConsulCatalogEntryServicesHash,
},
"token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
}
}
func resourceConsulCatalogEntryServiceTagsHash(v interface{}) int {
return hashcode.String(v.(string))
}
func resourceConsulCatalogEntryServicesHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["id"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["address"].(string)))
buf.WriteString(fmt.Sprintf("%d-", m["port"].(int)))
if v, ok := m["tags"]; ok {
vs := v.(*schema.Set).List()
s := make([]string, len(vs))
for i, raw := range vs {
s[i] = raw.(string)
}
sort.Strings(s)
for _, v := range s {
buf.WriteString(fmt.Sprintf("%s-", v))
}
}
return hashcode.String(buf.String())
}
func resourceConsulCatalogEntryCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
catalog := client.Catalog()
var dc string
if v, ok := d.GetOk("datacenter"); ok {
dc = v.(string)
} else {
var err error
if dc, err = getDC(d, client); err != nil {
return err
}
}
var token string
if v, ok := d.GetOk("token"); ok {
token = v.(string)
}
// Setup the operations using the datacenter
wOpts := consulapi.WriteOptions{Datacenter: dc, Token: token}
address := d.Get("address").(string)
node := d.Get("node").(string)
var serviceIDs []string
if service, ok := d.GetOk("service"); ok {
serviceList := service.(*schema.Set).List()
serviceIDs = make([]string, len(serviceList))
for i, rawService := range serviceList {
serviceData := rawService.(map[string]interface{})
serviceID := serviceData["id"].(string)
serviceIDs[i] = serviceID
var tags []string
if v := serviceData["tags"].(*schema.Set).List(); len(v) > 0 {
tags = make([]string, len(v))
for i, raw := range v {
tags[i] = raw.(string)
}
}
registration := &consulapi.CatalogRegistration{
Address: address,
Datacenter: dc,
Node: node,
Service: &consulapi.AgentService{
Address: serviceData["address"].(string),
ID: serviceID,
Service: serviceData["name"].(string),
Port: serviceData["port"].(int),
Tags: tags,
},
}
if _, err := catalog.Register(registration, &wOpts); err != nil {
return fmt.Errorf("Failed to register Consul catalog entry with node '%s' at address '%s' in %s: %v",
node, address, dc, err)
}
}
} else {
registration := &consulapi.CatalogRegistration{
Address: address,
Datacenter: dc,
Node: node,
}
if _, err := catalog.Register(registration, &wOpts); err != nil {
return fmt.Errorf("Failed to register Consul catalog entry with node '%s' at address '%s' in %s: %v",
node, address, dc, err)
}
}
// Update the resource
qOpts := consulapi.QueryOptions{Datacenter: dc}
if _, _, err := catalog.Node(node, &qOpts); err != nil {
return fmt.Errorf("Failed to read Consul catalog entry for node '%s' at address '%s' in %s: %v",
node, address, dc, err)
} else {
d.Set("datacenter", dc)
}
sort.Strings(serviceIDs)
serviceIDsJoined := strings.Join(serviceIDs, ",")
d.SetId(fmt.Sprintf("%s-%s-[%s]", node, address, serviceIDsJoined))
return nil
}
func resourceConsulCatalogEntryRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
catalog := client.Catalog()
// Get the DC, error if not available.
var dc string
if v, ok := d.GetOk("datacenter"); ok {
dc = v.(string)
}
node := d.Get("node").(string)
// Setup the operations using the datacenter
qOpts := consulapi.QueryOptions{Datacenter: dc}
if _, _, err := catalog.Node(node, &qOpts); err != nil {
return fmt.Errorf("Failed to get node '%s' from Consul catalog: %v", node, err)
}
return nil
}
func resourceConsulCatalogEntryDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
catalog := client.Catalog()
var dc string
if v, ok := d.GetOk("datacenter"); ok {
dc = v.(string)
} else {
var err error
if dc, err = getDC(d, client); err != nil {
return err
}
}
var token string
if v, ok := d.GetOk("token"); ok {
token = v.(string)
}
// Setup the operations using the datacenter
wOpts := consulapi.WriteOptions{Datacenter: dc, Token: token}
address := d.Get("address").(string)
node := d.Get("node").(string)
deregistration := consulapi.CatalogDeregistration{
Address: address,
Datacenter: dc,
Node: node,
}
if _, err := catalog.Deregister(&deregistration, &wOpts); err != nil {
return fmt.Errorf("Failed to deregister Consul catalog entry with node '%s' at address '%s' in %s: %v",
node, address, dc, err)
}
// Clear the ID
d.SetId("")
return nil
}

View File

@ -0,0 +1,100 @@
package consul
import (
"fmt"
"testing"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccConsulCatalogEntry_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() {},
Providers: testAccProviders,
CheckDestroy: testAccCheckConsulCatalogEntryDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccConsulCatalogEntryConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckConsulCatalogEntryExists(),
testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "address", "127.0.0.1"),
testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "node", "bastion"),
testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "service.#", "1"),
testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "service.3112399829.address", "www.google.com"),
testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "service.3112399829.id", "google1"),
testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "service.3112399829.name", "google"),
testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "service.3112399829.port", "80"),
testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "service.3112399829.tags.#", "2"),
testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "service.3112399829.tags.2154398732", "tag0"),
testAccCheckConsulCatalogEntryValue("consul_catalog_entry.app", "service.3112399829.tags.4151227546", "tag1"),
),
},
},
})
}
func testAccCheckConsulCatalogEntryDestroy(s *terraform.State) error {
catalog := testAccProvider.Meta().(*consulapi.Client).Catalog()
qOpts := consulapi.QueryOptions{}
services, _, err := catalog.Services(&qOpts)
if err != nil {
return fmt.Errorf("Could not retrieve services: %#v", err)
}
_, ok := services["google"]
if ok {
return fmt.Errorf("Service still exists: %#v", "google")
}
return nil
}
func testAccCheckConsulCatalogEntryExists() resource.TestCheckFunc {
return func(s *terraform.State) error {
catalog := testAccProvider.Meta().(*consulapi.Client).Catalog()
qOpts := consulapi.QueryOptions{}
services, _, err := catalog.Services(&qOpts)
if err != nil {
return err
}
_, ok := services["google"]
if !ok {
return fmt.Errorf("Service does not exist: %#v", "google")
}
return nil
}
}
func testAccCheckConsulCatalogEntryValue(n, attr, val string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rn, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Resource not found")
}
out, ok := rn.Primary.Attributes[attr]
if !ok {
return fmt.Errorf("Attribute '%s' not found: %#v", attr, rn.Primary.Attributes)
}
if val != "<any>" && out != val {
return fmt.Errorf("Attribute '%s' value '%s' != '%s'", attr, out, val)
}
if val == "<any>" && out == "" {
return fmt.Errorf("Attribute '%s' value '%s'", attr, out)
}
return nil
}
}
const testAccConsulCatalogEntryConfig = `
resource "consul_catalog_entry" "app" {
address = "127.0.0.1"
node = "bastion"
service = {
address = "www.google.com"
id = "google1"
name = "google"
port = 80
tags = ["tag0", "tag1"]
}
}
`

View File

@ -37,8 +37,9 @@ func resourceConsulKeys() *schema.Resource {
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
Type: schema.TypeString,
Optional: true,
Deprecated: "Using consul_keys resource to *read* is deprecated; please use consul_keys data source instead",
},
"path": &schema.Schema{
@ -220,7 +221,12 @@ func resourceConsulKeysRead(d *schema.ResourceData, meta interface{}) error {
}
value = attributeValue(sub, value)
vars[key] = value
if key != "" {
// If key is set then we'll update vars, for backward-compatibilty
// with the pre-0.7 capability to read from Consul with this
// resource.
vars[key] = value
}
// If there is already a "value" attribute present for this key
// then it was created as a "write" block. We need to update the
@ -290,10 +296,7 @@ func parseKey(raw interface{}) (string, string, map[string]interface{}, error) {
return "", "", nil, fmt.Errorf("Failed to unroll: %#v", raw)
}
key, ok := sub["name"].(string)
if !ok {
return "", "", nil, fmt.Errorf("Failed to expand key '%#v'", sub)
}
key := sub["name"].(string)
path, ok := sub["path"].(string)
if !ok {

View File

@ -0,0 +1,156 @@
package consul
import (
"fmt"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceConsulNode() *schema.Resource {
return &schema.Resource{
Create: resourceConsulNodeCreate,
Update: resourceConsulNodeCreate,
Read: resourceConsulNodeRead,
Delete: resourceConsulNodeDelete,
Schema: map[string]*schema.Schema{
"address": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"datacenter": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
}
}
func resourceConsulNodeCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
catalog := client.Catalog()
var dc string
if v, ok := d.GetOk("datacenter"); ok {
dc = v.(string)
} else {
var err error
if dc, err = getDC(d, client); err != nil {
return err
}
}
var token string
if v, ok := d.GetOk("token"); ok {
token = v.(string)
}
// Setup the operations using the datacenter
wOpts := consulapi.WriteOptions{Datacenter: dc, Token: token}
address := d.Get("address").(string)
name := d.Get("name").(string)
registration := &consulapi.CatalogRegistration{
Address: address,
Datacenter: dc,
Node: name,
}
if _, err := catalog.Register(registration, &wOpts); err != nil {
return fmt.Errorf("Failed to register Consul catalog node with name '%s' at address '%s' in %s: %v",
name, address, dc, err)
}
// Update the resource
qOpts := consulapi.QueryOptions{Datacenter: dc}
if _, _, err := catalog.Node(name, &qOpts); err != nil {
return fmt.Errorf("Failed to read Consul catalog node with name '%s' at address '%s' in %s: %v",
name, address, dc, err)
} else {
d.Set("datacenter", dc)
}
d.SetId(fmt.Sprintf("%s-%s", name, address))
return nil
}
func resourceConsulNodeRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
catalog := client.Catalog()
// Get the DC, error if not available.
var dc string
if v, ok := d.GetOk("datacenter"); ok {
dc = v.(string)
}
name := d.Get("name").(string)
// Setup the operations using the datacenter
qOpts := consulapi.QueryOptions{Datacenter: dc}
if _, _, err := catalog.Node(name, &qOpts); err != nil {
return fmt.Errorf("Failed to get name '%s' from Consul catalog: %v", name, err)
}
return nil
}
func resourceConsulNodeDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
catalog := client.Catalog()
var dc string
if v, ok := d.GetOk("datacenter"); ok {
dc = v.(string)
} else {
var err error
if dc, err = getDC(d, client); err != nil {
return err
}
}
var token string
if v, ok := d.GetOk("token"); ok {
token = v.(string)
}
// Setup the operations using the datacenter
wOpts := consulapi.WriteOptions{Datacenter: dc, Token: token}
address := d.Get("address").(string)
name := d.Get("name").(string)
deregistration := consulapi.CatalogDeregistration{
Address: address,
Datacenter: dc,
Node: name,
}
if _, err := catalog.Deregister(&deregistration, &wOpts); err != nil {
return fmt.Errorf("Failed to deregister Consul catalog node with name '%s' at address '%s' in %s: %v",
name, address, dc, err)
}
// Clear the ID
d.SetId("")
return nil
}

View File

@ -0,0 +1,87 @@
package consul
import (
"fmt"
"testing"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccConsulNode_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() {},
Providers: testAccProviders,
CheckDestroy: testAccCheckConsulNodeDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccConsulNodeConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckConsulNodeExists(),
testAccCheckConsulNodeValue("consul_catalog_entry.foo", "address", "127.0.0.1"),
testAccCheckConsulNodeValue("consul_catalog_entry.foo", "node", "foo"),
),
},
},
})
}
func testAccCheckConsulNodeDestroy(s *terraform.State) error {
catalog := testAccProvider.Meta().(*consulapi.Client).Catalog()
qOpts := consulapi.QueryOptions{}
nodes, _, err := catalog.Nodes(&qOpts)
if err != nil {
return fmt.Errorf("Could not retrieve services: %#v", err)
}
for i := range nodes {
if nodes[i].Node == "foo" {
return fmt.Errorf("Node still exists: %#v", "foo")
}
}
return nil
}
func testAccCheckConsulNodeExists() resource.TestCheckFunc {
return func(s *terraform.State) error {
catalog := testAccProvider.Meta().(*consulapi.Client).Catalog()
qOpts := consulapi.QueryOptions{}
nodes, _, err := catalog.Nodes(&qOpts)
if err != nil {
return err
}
for i := range nodes {
if nodes[i].Node == "foo" {
return nil
}
}
return fmt.Errorf("Service does not exist: %#v", "google")
}
}
func testAccCheckConsulNodeValue(n, attr, val string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rn, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Resource not found")
}
out, ok := rn.Primary.Attributes[attr]
if !ok {
return fmt.Errorf("Attribute '%s' not found: %#v", attr, rn.Primary.Attributes)
}
if val != "<any>" && out != val {
return fmt.Errorf("Attribute '%s' value '%s' != '%s'", attr, out, val)
}
if val == "<any>" && out == "" {
return fmt.Errorf("Attribute '%s' value '%s'", attr, out)
}
return nil
}
}
const testAccConsulNodeConfig = `
resource "consul_catalog_entry" "foo" {
address = "127.0.0.1"
node = "foo"
}
`

View File

@ -0,0 +1,139 @@
package consul
import (
"fmt"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceConsulService() *schema.Resource {
return &schema.Resource{
Create: resourceConsulServiceCreate,
Update: resourceConsulServiceCreate,
Read: resourceConsulServiceRead,
Delete: resourceConsulServiceDelete,
Schema: map[string]*schema.Schema{
"address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"port": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"tags": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
ForceNew: true,
},
},
}
}
func resourceConsulServiceCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
agent := client.Agent()
name := d.Get("name").(string)
registration := consulapi.AgentServiceRegistration{Name: name}
if address, ok := d.GetOk("address"); ok {
registration.Address = address.(string)
}
if port, ok := d.GetOk("port"); ok {
registration.Port = port.(int)
}
if v, ok := d.GetOk("tags"); ok {
vs := v.([]interface{})
s := make([]string, len(vs))
for i, raw := range vs {
s[i] = raw.(string)
}
registration.Tags = s
}
if err := agent.ServiceRegister(&registration); err != nil {
return fmt.Errorf("Failed to register service '%s' with Consul agent: %v", name, err)
}
// Update the resource
if serviceMap, err := agent.Services(); err != nil {
return fmt.Errorf("Failed to read services from Consul agent: %v", err)
} else if service, ok := serviceMap[name]; !ok {
return fmt.Errorf("Failed to read service '%s' from Consul agent: %v", name, err)
} else {
d.Set("address", service.Address)
d.Set("id", service.ID)
d.SetId(service.ID)
d.Set("name", service.Service)
d.Set("port", service.Port)
tags := make([]string, 0, len(service.Tags))
for _, tag := range service.Tags {
tags = append(tags, tag)
}
d.Set("tags", tags)
}
return nil
}
func resourceConsulServiceRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
agent := client.Agent()
name := d.Get("name").(string)
if services, err := agent.Services(); err != nil {
return fmt.Errorf("Failed to get services from Consul agent: %v", err)
} else if service, ok := services[name]; !ok {
return fmt.Errorf("Failed to get service '%s' from Consul agent", name)
} else {
d.Set("address", service.Address)
d.Set("id", service.ID)
d.SetId(service.ID)
d.Set("name", service.Service)
d.Set("port", service.Port)
tags := make([]string, 0, len(service.Tags))
for _, tag := range service.Tags {
tags = append(tags, tag)
}
d.Set("tags", tags)
}
return nil
}
func resourceConsulServiceDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*consulapi.Client)
catalog := client.Agent()
id := d.Get("id").(string)
if err := catalog.ServiceDeregister(id); err != nil {
return fmt.Errorf("Failed to deregister service '%s' from Consul agent: %v", id, err)
}
// Clear the ID
d.SetId("")
return nil
}

View File

@ -0,0 +1,90 @@
package consul
import (
"fmt"
"testing"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccConsulService_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() {},
Providers: testAccProviders,
CheckDestroy: testAccCheckConsulServiceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccConsulServiceConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckConsulServiceExists(),
testAccCheckConsulServiceValue("consul_service.app", "address", "www.google.com"),
testAccCheckConsulServiceValue("consul_service.app", "id", "google"),
testAccCheckConsulServiceValue("consul_service.app", "name", "google"),
testAccCheckConsulServiceValue("consul_service.app", "port", "80"),
testAccCheckConsulServiceValue("consul_service.app", "tags.#", "2"),
testAccCheckConsulServiceValue("consul_service.app", "tags.0", "tag0"),
testAccCheckConsulServiceValue("consul_service.app", "tags.1", "tag1"),
),
},
},
})
}
func testAccCheckConsulServiceDestroy(s *terraform.State) error {
agent := testAccProvider.Meta().(*consulapi.Client).Agent()
services, err := agent.Services()
if err != nil {
return fmt.Errorf("Could not retrieve services: %#v", err)
}
_, ok := services["google"]
if ok {
return fmt.Errorf("Service still exists: %#v", "google")
}
return nil
}
func testAccCheckConsulServiceExists() resource.TestCheckFunc {
return func(s *terraform.State) error {
agent := testAccProvider.Meta().(*consulapi.Client).Agent()
services, err := agent.Services()
if err != nil {
return err
}
_, ok := services["google"]
if !ok {
return fmt.Errorf("Service does not exist: %#v", "google")
}
return nil
}
}
func testAccCheckConsulServiceValue(n, attr, val string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rn, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Resource not found")
}
out, ok := rn.Primary.Attributes[attr]
if !ok {
return fmt.Errorf("Attribute '%s' not found: %#v", attr, rn.Primary.Attributes)
}
if val != "<any>" && out != val {
return fmt.Errorf("Attribute '%s' value '%s' != '%s'", attr, out, val)
}
if val == "<any>" && out == "" {
return fmt.Errorf("Attribute '%s' value '%s'", attr, out)
}
return nil
}
}
const testAccConsulServiceConfig = `
resource "consul_service" "app" {
address = "www.google.com"
name = "google"
port = 80
tags = ["tag0", "tag1"]
}
`

View File

@ -26,11 +26,24 @@ func Provider() terraform.ResourceProvider {
Type: schema.TypeString,
Optional: true,
},
"token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
DataSourcesMap: map[string]*schema.Resource{
"consul_keys": dataSourceConsulKeys(),
},
ResourcesMap: map[string]*schema.Resource{
"consul_keys": resourceConsulKeys(),
"consul_key_prefix": resourceConsulKeyPrefix(),
"consul_agent_service": resourceConsulAgentService(),
"consul_catalog_entry": resourceConsulCatalogEntry(),
"consul_keys": resourceConsulKeys(),
"consul_key_prefix": resourceConsulKeyPrefix(),
"consul_node": resourceConsulNode(),
"consul_service": resourceConsulService(),
},
ConfigureFunc: providerConfigure,

View File

@ -0,0 +1,166 @@
package docker
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/hashicorp/terraform/helper/schema"
)
func dataSourceDockerRegistryImage() *schema.Resource {
return &schema.Resource{
Read: dataSourceDockerRegistryImageRead,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"sha256_digest": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}
func dataSourceDockerRegistryImageRead(d *schema.ResourceData, meta interface{}) error {
pullOpts := parseImageOptions(d.Get("name").(string))
// Use the official Docker Hub if a registry isn't specified
if pullOpts.Registry == "" {
pullOpts.Registry = "registry.hub.docker.com"
} else {
// Otherwise, filter the registry name out of the repo name
pullOpts.Repository = strings.Replace(pullOpts.Repository, pullOpts.Registry+"/", "", 1)
}
// Docker prefixes 'library' to official images in the path; 'consul' becomes 'library/consul'
if !strings.Contains(pullOpts.Repository, "/") {
pullOpts.Repository = "library/" + pullOpts.Repository
}
if pullOpts.Tag == "" {
pullOpts.Tag = "latest"
}
digest, err := getImageDigest(pullOpts.Registry, pullOpts.Repository, pullOpts.Tag, "", "")
if err != nil {
return fmt.Errorf("Got error when attempting to fetch image version from registry: %s", err)
}
d.SetId(digest)
d.Set("sha256_digest", digest)
return nil
}
func getImageDigest(registry, image, tag, username, password string) (string, error) {
client := http.DefaultClient
req, err := http.NewRequest("GET", "https://"+registry+"/v2/"+image+"/manifests/"+tag, nil)
if err != nil {
return "", fmt.Errorf("Error creating registry request: %s", err)
}
if username != "" {
req.SetBasicAuth(username, password)
}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("Error during registry request: %s", err)
}
switch resp.StatusCode {
// Basic auth was valid or not needed
case http.StatusOK:
return resp.Header.Get("Docker-Content-Digest"), nil
// Either OAuth is required or the basic auth creds were invalid
case http.StatusUnauthorized:
if strings.HasPrefix(resp.Header.Get("www-authenticate"), "Bearer") {
auth := parseAuthHeader(resp.Header.Get("www-authenticate"))
params := url.Values{}
params.Set("service", auth["service"])
params.Set("scope", auth["scope"])
tokenRequest, err := http.NewRequest("GET", auth["realm"]+"?"+params.Encode(), nil)
if err != nil {
return "", fmt.Errorf("Error creating registry request: %s", err)
}
if username != "" {
tokenRequest.SetBasicAuth(username, password)
}
tokenResponse, err := client.Do(tokenRequest)
if err != nil {
return "", fmt.Errorf("Error during registry request: %s", err)
}
if tokenResponse.StatusCode != http.StatusOK {
return "", fmt.Errorf("Got bad response from registry: " + tokenResponse.Status)
}
body, err := ioutil.ReadAll(tokenResponse.Body)
if err != nil {
return "", fmt.Errorf("Error reading response body: %s", err)
}
token := &TokenResponse{}
err = json.Unmarshal(body, token)
if err != nil {
return "", fmt.Errorf("Error parsing OAuth token response: %s", err)
}
req.Header.Set("Authorization", "Bearer "+token.Token)
digestResponse, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("Error during registry request: %s", err)
}
if digestResponse.StatusCode != http.StatusOK {
return "", fmt.Errorf("Got bad response from registry: " + digestResponse.Status)
}
return digestResponse.Header.Get("Docker-Content-Digest"), nil
} else {
return "", fmt.Errorf("Bad credentials: " + resp.Status)
}
// Some unexpected status was given, return an error
default:
return "", fmt.Errorf("Got bad response from registry: " + resp.Status)
}
}
type TokenResponse struct {
Token string
}
// Parses key/value pairs from a WWW-Authenticate header
func parseAuthHeader(header string) map[string]string {
parts := strings.SplitN(header, " ", 2)
parts = strings.Split(parts[1], ",")
opts := make(map[string]string)
for _, part := range parts {
vals := strings.SplitN(part, "=", 2)
key := vals[0]
val := strings.Trim(vals[1], "\", ")
opts[key] = val
}
return opts
}

View File

@ -0,0 +1,52 @@
package docker
import (
"regexp"
"testing"
"github.com/hashicorp/terraform/helper/resource"
)
var registryDigestRegexp = regexp.MustCompile(`\A[A-Za-z0-9_\+\.-]+:[A-Fa-f0-9]+\z`)
func TestAccDockerRegistryImage_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDockerImageDataSourceConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr("data.docker_registry_image.foo", "sha256_digest", registryDigestRegexp),
),
},
},
})
}
func TestAccDockerRegistryImage_private(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDockerImageDataSourcePrivateConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr("data.docker_registry_image.bar", "sha256_digest", registryDigestRegexp),
),
},
},
})
}
const testAccDockerImageDataSourceConfig = `
data "docker_registry_image" "foo" {
name = "alpine:latest"
}
`
const testAccDockerImageDataSourcePrivateConfig = `
data "docker_registry_image" "bar" {
name = "gcr.io:443/google_containers/pause:0.8.0"
}
`

View File

@ -32,6 +32,10 @@ func Provider() terraform.ResourceProvider {
"docker_volume": resourceDockerVolume(),
},
DataSourcesMap: map[string]*schema.Resource{
"docker_registry_image": dataSourceDockerRegistryImage(),
},
ConfigureFunc: providerConfigure,
}
}

View File

@ -17,11 +17,6 @@ func resourceDockerImage() *schema.Resource {
Required: true,
},
"keep_updated": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"latest": &schema.Schema{
Type: schema.TypeString,
Computed: true,
@ -31,6 +26,12 @@ func resourceDockerImage() *schema.Resource {
Type: schema.TypeBool,
Optional: true,
},
"pull_trigger": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
},
}
}

View File

@ -22,6 +22,25 @@ func resourceDockerImageCreate(d *schema.ResourceData, meta interface{}) error {
}
func resourceDockerImageRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
var data Data
if err := fetchLocalImages(&data, client); err != nil {
return fmt.Errorf("Error reading docker image list: %s", err)
}
foundImage := searchLocalImages(data, d.Get("name").(string))
if foundImage != nil {
d.Set("latest", foundImage.ID)
} else {
d.SetId("")
}
return nil
}
func resourceDockerImageUpdate(d *schema.ResourceData, meta interface{}) error {
// We need to re-read in case switching parameters affects
// the value of "latest" or others
client := meta.(*dc.Client)
apiImage, err := findImage(d, client)
if err != nil {
@ -33,13 +52,6 @@ func resourceDockerImageRead(d *schema.ResourceData, meta interface{}) error {
return nil
}
func resourceDockerImageUpdate(d *schema.ResourceData, meta interface{}) error {
// We need to re-read in case switching parameters affects
// the value of "latest" or others
return resourceDockerImageRead(d, meta)
}
func resourceDockerImageDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*dc.Client)
err := removeImage(d, client)
@ -117,6 +129,17 @@ func pullImage(data *Data, client *dc.Client, image string) error {
// TODO: Test local registry handling. It should be working
// based on the code that was ported over
pullOpts := parseImageOptions(image)
auth := dc.AuthConfiguration{}
if err := client.PullImage(pullOpts, auth); err != nil {
return fmt.Errorf("Error pulling image %s: %s\n", image, err)
}
return fetchLocalImages(data, client)
}
func parseImageOptions(image string) dc.PullImageOptions {
pullOpts := dc.PullImageOptions{}
splitImageName := strings.Split(image, ":")
@ -151,32 +174,7 @@ func pullImage(data *Data, client *dc.Client, image string) error {
pullOpts.Repository = image
}
if err := client.PullImage(pullOpts, dc.AuthConfiguration{}); err != nil {
return fmt.Errorf("Error pulling image %s: %s\n", image, err)
}
return fetchLocalImages(data, client)
}
func getImageTag(image string) string {
splitImageName := strings.Split(image, ":")
switch {
// It's in registry:port/repo:tag format
case len(splitImageName) == 3:
return splitImageName[2]
// It's either registry:port/repo or repo:tag with default registry
case len(splitImageName) == 2:
splitPortRepo := strings.Split(splitImageName[1], "/")
if len(splitPortRepo) == 2 {
return ""
} else {
return splitImageName[1]
}
}
return ""
return pullOpts
}
func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error) {
@ -192,7 +190,7 @@ func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error)
foundImage := searchLocalImages(data, imageName)
if d.Get("keep_updated").(bool) || foundImage == nil {
if foundImage == nil {
if err := pullImage(&data, client, imageName); err != nil {
return nil, fmt.Errorf("Unable to pull image %s: %s", imageName, err)
}

View File

@ -73,6 +73,22 @@ func TestAccDockerImage_destroy(t *testing.T) {
})
}
func TestAccDockerImage_data(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
PreventPostDestroyRefresh: true,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccDockerImageFromDataConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr("docker_image.foobarbaz", "latest", contentDigestRegexp),
),
},
},
})
}
func testAccDockerImageDestroy(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "docker_image" {
@ -93,14 +109,12 @@ func testAccDockerImageDestroy(s *terraform.State) error {
const testAccDockerImageConfig = `
resource "docker_image" "foo" {
name = "alpine:3.1"
keep_updated = false
}
`
const testAddDockerPrivateImageConfig = `
resource "docker_image" "foobar" {
name = "gcr.io:443/google_containers/pause:0.8.0"
keep_updated = true
}
`
@ -110,3 +124,13 @@ resource "docker_image" "foobarzoo" {
keep_locally = true
}
`
const testAccDockerImageFromDataConfig = `
data "docker_registry_image" "foobarbaz" {
name = "alpine:3.1"
}
resource "docker_image" "foobarbaz" {
name = "${data.docker_registry_image.foobarbaz.name}"
pull_trigger = "${data.docker_registry_image.foobarbaz.sha256_digest}"
}
`

View File

@ -50,6 +50,8 @@ func Provider() terraform.ResourceProvider {
ResourcesMap: map[string]*schema.Resource{
"mysql_database": resourceDatabase(),
"mysql_user": resourceUser(),
"mysql_grant": resourceGrant(),
},
ConfigureFunc: providerConfigure,

View File

@ -0,0 +1,123 @@
package mysql
import (
"fmt"
"log"
"strings"
mysqlc "github.com/ziutek/mymysql/mysql"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceGrant() *schema.Resource {
return &schema.Resource{
Create: CreateGrant,
Update: nil,
Read: ReadGrant,
Delete: DeleteGrant,
Schema: map[string]*schema.Schema{
"user": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"host": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: "localhost",
},
"database": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"privileges": &schema.Schema{
Type: schema.TypeSet,
Required: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"grant": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
Default: false,
},
},
}
}
func CreateGrant(d *schema.ResourceData, meta interface{}) error {
conn := meta.(mysqlc.Conn)
// create a comma-delimited string of privileges
var privileges string
var privilegesList []string
vL := d.Get("privileges").(*schema.Set).List()
for _, v := range vL {
privilegesList = append(privilegesList, v.(string))
}
privileges = strings.Join(privilegesList, ",")
stmtSQL := fmt.Sprintf("GRANT %s on %s.* TO '%s'@'%s'",
privileges,
d.Get("database").(string),
d.Get("user").(string),
d.Get("host").(string))
if d.Get("grant").(bool) {
stmtSQL = " WITH GRANT OPTION"
}
log.Println("Executing statement:", stmtSQL)
_, _, err := conn.Query(stmtSQL)
if err != nil {
return err
}
user := fmt.Sprintf("%s@%s:%s", d.Get("user").(string), d.Get("host").(string), d.Get("database"))
d.SetId(user)
return ReadGrant(d, meta)
}
func ReadGrant(d *schema.ResourceData, meta interface{}) error {
// At this time, all attributes are supplied by the user
return nil
}
func DeleteGrant(d *schema.ResourceData, meta interface{}) error {
conn := meta.(mysqlc.Conn)
stmtSQL := fmt.Sprintf("REVOKE GRANT OPTION ON %s.* FROM '%s'@'%s'",
d.Get("database").(string),
d.Get("user").(string),
d.Get("host").(string))
log.Println("Executing statement:", stmtSQL)
_, _, err := conn.Query(stmtSQL)
if err != nil {
return err
}
stmtSQL = fmt.Sprintf("REVOKE ALL ON %s.* FROM '%s'@'%s'",
d.Get("database").(string),
d.Get("user").(string),
d.Get("host").(string))
log.Println("Executing statement:", stmtSQL)
_, _, err = conn.Query(stmtSQL)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,125 @@
package mysql
import (
"fmt"
"log"
"strings"
"testing"
mysqlc "github.com/ziutek/mymysql/mysql"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccGrant(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccGrantCheckDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccGrantConfig_basic,
Check: resource.ComposeTestCheckFunc(
testAccPrivilegeExists("mysql_grant.test", "SELECT"),
resource.TestCheckResourceAttr("mysql_grant.test", "user", "jdoe"),
resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"),
resource.TestCheckResourceAttr("mysql_grant.test", "database", "foo"),
),
},
},
})
}
func testAccPrivilegeExists(rn string, privilege string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[rn]
if !ok {
return fmt.Errorf("resource not found: %s", rn)
}
if rs.Primary.ID == "" {
return fmt.Errorf("grant id not set")
}
id := strings.Split(rs.Primary.ID, ":")
userhost := strings.Split(id[0], "@")
user := userhost[0]
host := userhost[1]
conn := testAccProvider.Meta().(mysqlc.Conn)
stmtSQL := fmt.Sprintf("SHOW GRANTS for '%s'@'%s'", user, host)
log.Println("Executing statement:", stmtSQL)
rows, _, err := conn.Query(stmtSQL)
if err != nil {
return fmt.Errorf("error reading grant: %s", err)
}
if len(rows) == 0 {
return fmt.Errorf("grant not found for '%s'@'%s'", user, host)
}
privilegeFound := false
for _, row := range rows {
log.Printf("Result Row: %s", row[0])
privIndex := strings.Index(string(row[0].([]byte)), privilege)
if privIndex != -1 {
privilegeFound = true
}
}
if !privilegeFound {
return fmt.Errorf("grant no found for '%s'@'%s'", user, host)
}
return nil
}
}
func testAccGrantCheckDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(mysqlc.Conn)
for _, rs := range s.RootModule().Resources {
if rs.Type != "mysql_grant" {
continue
}
id := strings.Split(rs.Primary.ID, ":")
userhost := strings.Split(id[0], "@")
user := userhost[0]
host := userhost[1]
stmtSQL := fmt.Sprintf("SHOW GRANTS for '%s'@'%s'", user, host)
log.Println("Executing statement:", stmtSQL)
rows, _, err := conn.Query(stmtSQL)
if err != nil {
if mysqlErr, ok := err.(*mysqlc.Error); ok {
if mysqlErr.Code == mysqlc.ER_NONEXISTING_GRANT {
return nil
}
}
return fmt.Errorf("error reading grant: %s", err)
}
if len(rows) != 0 {
return fmt.Errorf("grant still exists for'%s'@'%s'", user, host)
}
}
return nil
}
const testAccGrantConfig_basic = `
resource "mysql_user" "test" {
user = "jdoe"
host = "example.com"
password = "password"
}
resource "mysql_grant" "test" {
user = "${mysql_user.test.user}"
host = "${mysql_user.test.host}"
database = "foo"
privileges = ["UPDATE", "SELECT"]
}
`

View File

@ -0,0 +1,105 @@
package mysql
import (
"fmt"
"log"
mysqlc "github.com/ziutek/mymysql/mysql"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceUser() *schema.Resource {
return &schema.Resource{
Create: CreateUser,
Update: UpdateUser,
Read: ReadUser,
Delete: DeleteUser,
Schema: map[string]*schema.Schema{
"user": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"host": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: "localhost",
},
"password": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Sensitive: true,
},
},
}
}
func CreateUser(d *schema.ResourceData, meta interface{}) error {
conn := meta.(mysqlc.Conn)
stmtSQL := fmt.Sprintf("CREATE USER '%s'@'%s'",
d.Get("user").(string),
d.Get("host").(string))
password := d.Get("password").(string)
if password != "" {
stmtSQL = stmtSQL + fmt.Sprintf(" IDENTIFIED BY '%s'", password)
}
log.Println("Executing statement:", stmtSQL)
_, _, err := conn.Query(stmtSQL)
if err != nil {
return err
}
user := fmt.Sprintf("%s@%s", d.Get("user").(string), d.Get("host").(string))
d.SetId(user)
return nil
}
func UpdateUser(d *schema.ResourceData, meta interface{}) error {
conn := meta.(mysqlc.Conn)
if d.HasChange("password") {
_, newpw := d.GetChange("password")
stmtSQL := fmt.Sprintf("ALTER USER '%s'@'%s' IDENTIFIED BY '%s'",
d.Get("user").(string),
d.Get("host").(string),
newpw.(string))
log.Println("Executing query:", stmtSQL)
_, _, err := conn.Query(stmtSQL)
if err != nil {
return err
}
}
return nil
}
func ReadUser(d *schema.ResourceData, meta interface{}) error {
// At this time, all attributes are supplied by the user
return nil
}
func DeleteUser(d *schema.ResourceData, meta interface{}) error {
conn := meta.(mysqlc.Conn)
stmtSQL := fmt.Sprintf("DROP USER '%s'@'%s'",
d.Get("user").(string),
d.Get("host").(string))
log.Println("Executing statement:", stmtSQL)
_, _, err := conn.Query(stmtSQL)
if err == nil {
d.SetId("")
}
return err
}

View File

@ -0,0 +1,86 @@
package mysql
import (
"fmt"
"log"
"testing"
mysqlc "github.com/ziutek/mymysql/mysql"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccUser(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccUserCheckDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccUserConfig_basic,
Check: resource.ComposeTestCheckFunc(
testAccUserExists("mysql_user.test"),
resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"),
resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"),
resource.TestCheckResourceAttr("mysql_user.test", "password", "password"),
),
},
},
})
}
func testAccUserExists(rn string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[rn]
if !ok {
return fmt.Errorf("resource not found: %s", rn)
}
if rs.Primary.ID == "" {
return fmt.Errorf("user id not set")
}
conn := testAccProvider.Meta().(mysqlc.Conn)
stmtSQL := fmt.Sprintf("SELECT count(*) from mysql.user where CONCAT(user, '@', host) = '%s'", rs.Primary.ID)
log.Println("Executing statement:", stmtSQL)
rows, _, err := conn.Query(stmtSQL)
if err != nil {
return fmt.Errorf("error reading user: %s", err)
}
if len(rows) != 1 {
return fmt.Errorf("expected 1 row reading user but got %d", len(rows))
}
return nil
}
}
func testAccUserCheckDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(mysqlc.Conn)
for _, rs := range s.RootModule().Resources {
if rs.Type != "mysql_user" {
continue
}
stmtSQL := fmt.Sprintf("SELECT user from mysql.user where CONCAT(user, '@', host) = '%s'", rs.Primary.ID)
log.Println("Executing statement:", stmtSQL)
rows, _, err := conn.Query(stmtSQL)
if err != nil {
return fmt.Errorf("error issuing query: %s", err)
}
if len(rows) != 0 {
return fmt.Errorf("user still exists after destroy")
}
}
return nil
}
const testAccUserConfig_basic = `
resource "mysql_user" "test" {
user = "jdoe"
host = "example.com"
password = "password"
}
`

View File

@ -3,21 +3,46 @@ package command
import (
"fmt"
"io/ioutil"
"regexp"
"strings"
"github.com/hashicorp/hcl"
"github.com/mitchellh/go-homedir"
)
// FlagKV is a flag.Value implementation for parsing user variables
// from the command-line in the format of '-var key=value'.
type FlagKV map[string]string
// FlagTypedKVis a flag.Value implementation for parsing user variables
// from the command-line in the format of '-var key=value', where value is
// a type intended for use as a Terraform variable
type FlagTypedKV map[string]interface{}
func (v *FlagKV) String() string {
func (v *FlagTypedKV) String() string {
return ""
}
func (v *FlagKV) Set(raw string) error {
func (v *FlagTypedKV) Set(raw string) error {
key, value, err := parseVarFlagAsHCL(raw)
if err != nil {
return err
}
if *v == nil {
*v = make(map[string]interface{})
}
(*v)[key] = value
return nil
}
// FlagStringKV is a flag.Value implementation for parsing user variables
// from the command-line in the format of '-var key=value', where value is
// only ever a primitive.
type FlagStringKV map[string]string
func (v *FlagStringKV) String() string {
return ""
}
func (v *FlagStringKV) Set(raw string) error {
idx := strings.Index(raw, "=")
if idx == -1 {
return fmt.Errorf("No '=' value in arg: %s", raw)
@ -34,7 +59,7 @@ func (v *FlagKV) Set(raw string) error {
// FlagKVFile is a flag.Value implementation for parsing user variables
// from the command line in the form of files. i.e. '-var-file=foo'
type FlagKVFile map[string]string
type FlagKVFile map[string]interface{}
func (v *FlagKVFile) String() string {
return ""
@ -47,7 +72,7 @@ func (v *FlagKVFile) Set(raw string) error {
}
if *v == nil {
*v = make(map[string]string)
*v = make(map[string]interface{})
}
for key, value := range vs {
@ -57,7 +82,7 @@ func (v *FlagKVFile) Set(raw string) error {
return nil
}
func loadKVFile(rawPath string) (map[string]string, error) {
func loadKVFile(rawPath string) (map[string]interface{}, error) {
path, err := homedir.Expand(rawPath)
if err != nil {
return nil, fmt.Errorf(
@ -78,7 +103,7 @@ func loadKVFile(rawPath string) (map[string]string, error) {
"Error parsing %s: %s", path, err)
}
var result map[string]string
var result map[string]interface{}
if err := hcl.DecodeObject(&result, obj); err != nil {
return nil, fmt.Errorf(
"Error decoding Terraform vars file: %s\n\n"+
@ -103,3 +128,49 @@ func (v *FlagStringSlice) Set(raw string) error {
return nil
}
// parseVarFlagAsHCL parses the value of a single variable as would have been specified
// on the command line via -var or in an environment variable named TF_VAR_x, where x is
// the name of the variable. In order to get around the restriction of HCL requiring a
// top level object, we prepend a sentinel key, decode the user-specified value as its
// value and pull the value back out of the resulting map.
func parseVarFlagAsHCL(input string) (string, interface{}, error) {
idx := strings.Index(input, "=")
if idx == -1 {
return "", nil, fmt.Errorf("No '=' value in variable: %s", input)
}
probablyName := input[0:idx]
parsed, err := hcl.Parse(input)
if err != nil {
// This covers flags of the form `foo=bar` which is not valid HCL
// At this point, probablyName is actually the name, and the remainder
// of the expression after the equals sign is the value.
if regexp.MustCompile(`Unknown token: \d+:\d+ IDENT`).Match([]byte(err.Error())) {
value := input[idx+1:]
return probablyName, value, nil
}
return "", nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL: %s", probablyName, input, err)
}
var decoded map[string]interface{}
if hcl.DecodeObject(&decoded, parsed); err != nil {
return "", nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL: %s", probablyName, input, err)
}
// Cover cases such as key=
if len(decoded) == 0 {
return probablyName, "", nil
}
if len(decoded) > 1 {
return "", nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL. Only one value may be specified.", probablyName, input)
}
for k, v := range decoded {
return k, v, nil
}
// Should be unreachable
return "", nil, fmt.Errorf("No value for variable: %s", input)
}

View File

@ -2,16 +2,17 @@ package command
import (
"flag"
"github.com/davecgh/go-spew/spew"
"io/ioutil"
"reflect"
"testing"
)
func TestFlagKV_impl(t *testing.T) {
var _ flag.Value = new(FlagKV)
func TestFlagStringKV_impl(t *testing.T) {
var _ flag.Value = new(FlagStringKV)
}
func TestFlagKV(t *testing.T) {
func TestFlagStringKV(t *testing.T) {
cases := []struct {
Input string
Output map[string]string
@ -49,10 +50,10 @@ func TestFlagKV(t *testing.T) {
}
for _, tc := range cases {
f := new(FlagKV)
f := new(FlagStringKV)
err := f.Set(tc.Input)
if err != nil != tc.Error {
t.Fatalf("bad error. Input: %#v", tc.Input)
t.Fatalf("bad error. Input: %#v\n\nError: %s", tc.Input, err)
}
actual := map[string]string(*f)
@ -62,6 +63,86 @@ func TestFlagKV(t *testing.T) {
}
}
func TestFlagTypedKV_impl(t *testing.T) {
var _ flag.Value = new(FlagTypedKV)
}
func TestFlagTypedKV(t *testing.T) {
cases := []struct {
Input string
Output map[string]interface{}
Error bool
}{
{
"key=value",
map[string]interface{}{"key": "value"},
false,
},
{
"key=",
map[string]interface{}{"key": ""},
false,
},
{
"key=foo=bar",
map[string]interface{}{"key": "foo=bar"},
false,
},
{
"map.key=foo",
map[string]interface{}{"map.key": "foo"},
false,
},
{
"key",
nil,
true,
},
{
`key=["hello", "world"]`,
map[string]interface{}{"key": []interface{}{"hello", "world"}},
false,
},
{
`key={"hello" = "world", "foo" = "bar"}`,
map[string]interface{}{
"key": []map[string]interface{}{
map[string]interface{}{
"hello": "world",
"foo": "bar",
},
},
},
false,
},
{
`key={"hello" = "world", "foo" = "bar"}\nkey2="invalid"`,
nil,
true,
},
}
for _, tc := range cases {
f := new(FlagTypedKV)
err := f.Set(tc.Input)
if err != nil != tc.Error {
t.Fatalf("bad error. Input: %#v\n\nError: %s", tc.Input, err)
}
actual := map[string]interface{}(*f)
if !reflect.DeepEqual(actual, tc.Output) {
t.Fatalf("bad:\nexpected: %s\n\n got: %s\n", spew.Sdump(tc.Output), spew.Sdump(actual))
}
}
}
func TestFlagKVFile_impl(t *testing.T) {
var _ flag.Value = new(FlagKVFile)
}
@ -76,24 +157,24 @@ foo = "bar"
cases := []struct {
Input string
Output map[string]string
Output map[string]interface{}
Error bool
}{
{
inputLibucl,
map[string]string{"foo": "bar"},
map[string]interface{}{"foo": "bar"},
false,
},
{
inputJson,
map[string]string{"foo": "bar"},
map[string]interface{}{"foo": "bar"},
false,
},
{
`map.key = "foo"`,
map[string]string{"map.key": "foo"},
map[string]interface{}{"map.key": "foo"},
false,
},
}
@ -111,7 +192,7 @@ foo = "bar"
t.Fatalf("bad error. Input: %#v, err: %s", tc.Input, err)
}
actual := map[string]string(*f)
actual := map[string]interface{}(*f)
if !reflect.DeepEqual(actual, tc.Output) {
t.Fatalf("bad: %#v", actual)
}

View File

@ -25,7 +25,7 @@ func (c *InitCommand) Run(args []string) int {
remoteConfig := make(map[string]string)
cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError)
cmdFlags.StringVar(&remoteBackend, "backend", "", "")
cmdFlags.Var((*FlagKV)(&remoteConfig), "backend-config", "config")
cmdFlags.Var((*FlagStringKV)(&remoteConfig), "backend-config", "config")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1

View File

@ -36,9 +36,9 @@ type Meta struct {
// Variables for the context (private)
autoKey string
autoVariables map[string]string
autoVariables map[string]interface{}
input bool
variables map[string]string
variables map[string]interface{}
// Targets for this context (private)
targets []string
@ -315,7 +315,7 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
func (m *Meta) flagSet(n string) *flag.FlagSet {
f := flag.NewFlagSet(n, flag.ContinueOnError)
f.BoolVar(&m.input, "input", true, "input")
f.Var((*FlagKV)(&m.variables), "var", "variables")
f.Var((*FlagTypedKV)(&m.variables), "var", "variables")
f.Var((*FlagKVFile)(&m.variables), "var-file", "variable file")
f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target")

View File

@ -38,7 +38,7 @@ func (c *RemoteConfigCommand) Run(args []string) int {
cmdFlags.StringVar(&c.conf.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path")
cmdFlags.StringVar(&c.remoteConf.Type, "backend", "atlas", "")
cmdFlags.Var((*FlagKV)(&config), "backend-config", "config")
cmdFlags.Var((*FlagStringKV)(&config), "backend-config", "config")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("\nError parsing CLI flags: %s", err))

View File

@ -9,6 +9,7 @@ import (
"sync"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/hcl"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
)
@ -119,24 +120,109 @@ func NewContext(opts *ContextOpts) (*Context, error) {
par = 10
}
// Setup the variables. We first take the variables given to us.
// We then merge in the variables set in the environment.
// Set up the variables in the following sequence:
// 0 - Take default values from the configuration
// 1 - Take values from TF_VAR_x environment variables
// 2 - Take values specified in -var flags, overriding values
// set by environment variables if necessary. This includes
// values taken from -var-file in addition.
variables := make(map[string]interface{})
for _, v := range os.Environ() {
if !strings.HasPrefix(v, VarEnvPrefix) {
continue
if opts.Module != nil {
for _, v := range opts.Module.Config().Variables {
if v.Default != nil {
if v.Type() == config.VariableTypeString {
// v.Default has already been parsed as HCL so there may be
// some stray ints in there
switch typedDefault := v.Default.(type) {
case string:
if typedDefault == "" {
continue
}
variables[v.Name] = typedDefault
case int, int64:
variables[v.Name] = fmt.Sprintf("%d", typedDefault)
case float32, float64:
variables[v.Name] = fmt.Sprintf("%f", typedDefault)
case bool:
variables[v.Name] = fmt.Sprintf("%t", typedDefault)
}
} else {
variables[v.Name] = v.Default
}
}
}
// Strip off the prefix and get the value after the first "="
idx := strings.Index(v, "=")
k := v[len(VarEnvPrefix):idx]
v = v[idx+1:]
for _, v := range os.Environ() {
if !strings.HasPrefix(v, VarEnvPrefix) {
continue
}
// Override the command-line set variable
variables[k] = v
}
for k, v := range opts.Variables {
variables[k] = v
// Strip off the prefix and get the value after the first "="
idx := strings.Index(v, "=")
k := v[len(VarEnvPrefix):idx]
v = v[idx+1:]
// Override the configuration-default values. Note that *not* finding the variable
// in configuration is OK, as we don't want to preclude people from having multiple
// sets of TF_VAR_whatever in their environment even if it is a little weird.
for _, schema := range opts.Module.Config().Variables {
if schema.Name == k {
varType := schema.Type()
varVal, err := parseVariableAsHCL(k, v, varType)
if err != nil {
return nil, err
}
switch varType {
case config.VariableTypeMap:
if existing, hasMap := variables[k]; !hasMap {
variables[k] = varVal
} else {
if existingMap, ok := existing.(map[string]interface{}); !ok {
panic(fmt.Sprintf("%s is not a map, this is a bug in Terraform.", k))
} else {
if newMap, ok := varVal.(map[string]interface{}); ok {
for newKey, newVal := range newMap {
existingMap[newKey] = newVal
}
} else {
panic(fmt.Sprintf("%s is not a map, this is a bug in Terraform.", k))
}
}
}
default:
variables[k] = varVal
}
}
}
}
for k, v := range opts.Variables {
for _, schema := range opts.Module.Config().Variables {
if schema.Name == k {
switch schema.Type() {
case config.VariableTypeMap:
if existing, hasMap := variables[k]; !hasMap {
variables[k] = v
} else {
if existingMap, ok := existing.(map[string]interface{}); !ok {
panic(fmt.Sprintf("%s is not a map, this is a bug in Terraform.", k))
} else {
if newMap, ok := v.([]map[string]interface{}); ok {
for newKey, newVal := range newMap[0] {
existingMap[newKey] = newVal
}
} else {
panic(fmt.Sprintf("%s is not a map, this is a bug in Terraform.", k))
}
}
}
default:
variables[k] = v
}
}
}
}
}
return &Context{
@ -548,3 +634,45 @@ func (c *Context) walk(
walker := &ContextGraphWalker{Context: c, Operation: operation}
return walker, graph.Walk(walker)
}
// parseVariableAsHCL parses the value of a single variable as would have been specified
// on the command line via -var or in an environment variable named TF_VAR_x, where x is
// the name of the variable. In order to get around the restriction of HCL requiring a
// top level object, we prepend a sentinel key, decode the user-specified value as its
// value and pull the value back out of the resulting map.
func parseVariableAsHCL(name string, input interface{}, targetType config.VariableType) (interface{}, error) {
if targetType == config.VariableTypeString {
return input, nil
}
const sentinelValue = "SENTINEL_TERRAFORM_VAR_OVERRIDE_KEY"
inputWithSentinal := fmt.Sprintf("%s = %s", sentinelValue, input)
var decoded map[string]interface{}
err := hcl.Decode(&decoded, inputWithSentinal)
if err != nil {
return nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL: %s", name, input, err)
}
if len(decoded) != 1 {
return nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL. Only one value may be specified.", name, input)
}
parsedValue, ok := decoded[sentinelValue]
if !ok {
return nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL. One value must be specified.", name, input)
}
switch targetType {
case config.VariableTypeList:
return parsedValue, nil
case config.VariableTypeMap:
if list, ok := parsedValue.([]map[string]interface{}); ok {
return list[0], nil
}
return nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL. One value must be specified.", name, input)
default:
panic(fmt.Errorf("unknown type %s", targetType))
}
}

View File

@ -1135,7 +1135,11 @@ func TestContext2Apply_mapVariableOverride(t *testing.T) {
"aws": testProviderFuncFixed(p),
},
Variables: map[string]interface{}{
"images.us-west-2": "overridden",
"images": []map[string]interface{}{
map[string]interface{}{
"us-west-2": "overridden",
},
},
},
})
@ -4269,8 +4273,18 @@ func TestContext2Apply_vars(t *testing.T) {
"aws": testProviderFuncFixed(p),
},
Variables: map[string]interface{}{
"foo": "us-west-2",
"amis.us-east-1": "override",
"foo": "us-west-2",
"test_list": []interface{}{"Hello", "World"},
"test_map": map[string]interface{}{
"Hello": "World",
"Foo": "Bar",
"Baz": "Foo",
},
"amis": []map[string]interface{}{
map[string]interface{}{
"us-east-1": "override",
},
},
},
})
@ -4300,8 +4314,13 @@ func TestContext2Apply_vars(t *testing.T) {
func TestContext2Apply_varsEnv(t *testing.T) {
// Set the env var
old := tempEnv(t, "TF_VAR_ami", "baz")
defer os.Setenv("TF_VAR_ami", old)
old_ami := tempEnv(t, "TF_VAR_ami", "baz")
old_list := tempEnv(t, "TF_VAR_list", `["Hello", "World"]`)
old_map := tempEnv(t, "TF_VAR_map", `{"Hello" = "World", "Foo" = "Bar", "Baz" = "Foo"}`)
defer os.Setenv("TF_VAR_ami", old_ami)
defer os.Setenv("TF_VAR_list", old_list)
defer os.Setenv("TF_VAR_list", old_map)
m := testModule(t, "apply-vars-env")
p := testProvider("aws")

View File

@ -19,8 +19,12 @@ func TestContext2Input(t *testing.T) {
"aws": testProviderFuncFixed(p),
},
Variables: map[string]interface{}{
"foo": "us-west-2",
"amis.us-east-1": "override",
"foo": "us-west-2",
"amis": []map[string]interface{}{
map[string]interface{}{
"us-east-1": "override",
},
},
},
UIInput: input,
})

View File

@ -95,16 +95,42 @@ func smcUserVariables(c *config.Config, vs map[string]interface{}) []error {
}
// Check that types match up
for k, _ := range vs {
v, ok := cvs[k]
for name, proposedValue := range vs {
schema, ok := cvs[name]
if !ok {
continue
}
if v.Type() != config.VariableTypeString {
errs = append(errs, fmt.Errorf(
"%s: cannot assign string value to map type",
k))
declaredType := schema.Type()
switch declaredType {
case config.VariableTypeString:
switch proposedValue.(type) {
case string:
continue
default:
errs = append(errs, fmt.Errorf("variable %s should be type %s, got %s",
name, declaredType.Printable(), hclTypeName(proposedValue)))
}
case config.VariableTypeMap:
switch proposedValue.(type) {
case map[string]interface{}:
continue
default:
errs = append(errs, fmt.Errorf("variable %s should be type %s, got %s",
name, declaredType.Printable(), hclTypeName(proposedValue)))
}
case config.VariableTypeList:
switch proposedValue.(type) {
case []interface{}:
continue
default:
errs = append(errs, fmt.Errorf("variable %s should be type %s, got %s",
name, declaredType.Printable(), hclTypeName(proposedValue)))
}
default:
errs = append(errs, fmt.Errorf("variable %s should be type %s, got %s",
name, declaredType.Printable(), hclTypeName(proposedValue)))
}
}

View File

@ -705,6 +705,8 @@ aws_instance.bar:
aws_instance.foo:
ID = foo
bar = baz
list = Hello,World
map = Baz,Foo,Hello
num = 2
type = aws_instance
`
@ -712,6 +714,8 @@ aws_instance.foo:
const testTerraformApplyVarsEnvStr = `
aws_instance.bar:
ID = foo
bar = Hello,World
baz = Baz,Foo,Hello
foo = baz
type = aws_instance
`

View File

@ -1,7 +1,20 @@
variable "ami" {
default = "foo"
default = "foo"
type = "string"
}
variable "list" {
default = []
type = "list"
}
variable "map" {
default = {}
type = "map"
}
resource "aws_instance" "bar" {
foo = "${var.ami}"
foo = "${var.ami}"
bar = "${join(",", var.list)}"
baz = "${join(",", keys(var.map))}"
}

View File

@ -5,6 +5,14 @@ variable "amis" {
}
}
variable "test_list" {
type = "list"
}
variable "test_map" {
type = "map"
}
variable "bar" {
default = "baz"
}
@ -14,6 +22,8 @@ variable "foo" {}
resource "aws_instance" "foo" {
num = "2"
bar = "${var.bar}"
list = "${join(",", var.test_list)}"
map = "${join(",", keys(var.test_map))}"
}
resource "aws_instance" "bar" {

View File

@ -0,0 +1,67 @@
---
layout: "consul"
page_title: "Consul: consul_keys"
sidebar_current: "docs-consul-data-source-keys"
description: |-
Reads values from the Consul key/value store.
---
# consul\_keys
`consul_keys` reads values from the Consul key/value store.
This is a powerful way dynamically set values in templates.
## Example Usage
```
data "consul_keys" "app" {
datacenter = "nyc1"
token = "abcd"
# Read the launch AMI from Consul
key {
name = "ami"
path = "service/app/launch_ami"
default = "ami-1234"
}
}
# Start our instance with the dynamic ami value
resource "aws_instance" "app" {
ami = "${data.consul_keys.app.var.ami}"
...
}
```
## Argument Reference
The following arguments are supported:
* `datacenter` - (Optional) The datacenter to use. This overrides the
datacenter in the provider setup and the agent's default datacenter.
* `token` - (Optional) The ACL token to use. This overrides the
token that the agent provides by default.
* `key` - (Required) Specifies a key in Consul to be read or written.
Supported values documented below.
The `key` block supports the following:
* `name` - (Required) This is the name of the key. This value of the
key is exposed as `var.<name>`. This is not the path of the key
in Consul.
* `path` - (Required) This is the path in Consul that should be read
or written to.
* `default` - (Optional) This is the default value to set for `var.<name>`
if the key does not exist in Consul. Defaults to the empty string.
## Attributes Reference
The following attributes are exported:
* `datacenter` - The datacenter the keys are being read from to.
* `var.<name>` - For each name given, the corresponding attribute
has the value of the key.

View File

@ -0,0 +1,47 @@
---
layout: "consul"
page_title: "Consul: consul_agent_service"
sidebar_current: "docs-consul-resource-agent-service"
description: |-
Provides access to Agent Service data in Consul. This can be used to define a service associated with a particular agent. Currently, defining health checks for an agent service is not supported.
---
# consul\_agent\_service
Provides access to Agent Service data in Consul. This can be used to define a service associated with a particular agent. Currently, defining health checks for an agent service is not supported.
## Example Usage
```
resource "consul_agent_service" "app" {
address = "www.google.com"
name = "google"
port = 80
tags = ["tag0", "tag1"]
}
```
## Argument Reference
The following arguments are supported:
* `address` - (Optional) The address of the service. Defaults to the
address of the agent.
* `name` - (Required) The name of the service.
* `port` - (Optional) The port of the service.
* `tags` - (Optional) A list of values that are opaque to Consul,
but can be used to distinguish between services or nodes.
## Attributes Reference
The following attributes are exported:
* `address` - The address of the service.
* `id` - The id of the service, defaults to the value of `name`.
* `name` - The name of the service.
* `port` - The port of the service.
* `tags` - The tags of the service.

View File

@ -0,0 +1,58 @@
---
layout: "consul"
page_title: "Consul: consul_catalog_entry"
sidebar_current: "docs-consul-resource-catalog-entry"
description: |-
Provides access to Catalog data in Consul. This can be used to define a node or a service. Currently, defining health checks is not supported.
---
# consul\_catalog\_entry
Provides access to Catalog data in Consul. This can be used to define a node or a service. Currently, defining health checks is not supported.
## Example Usage
```
resource "consul_catalog_entry" "app" {
address = "192.168.10.10"
name = "foobar"
service = {
address = "127.0.0.1"
id = "redis1"
name = "redis"
port = 8000
tags = ["master", "v1"]
}
}
```
## Argument Reference
The following arguments are supported:
* `address` - (Required) The address of the node being added to
or referenced in the catalog.
* `node` - (Required) The name of the node being added to or
referenced in the catalog.
* `service` - (Optional) A service to optionally associated with
the node. Supported values documented below.
The `service` block supports the following:
* `address` - (Optional) The address of the service. Defaults to the
node address.
* `id` - (Optional) The ID of the service. Defaults to the `name`.
* `name` - (Required) The name of the service
* `port` - (Optional) The port of the service.
* `tags` - (Optional) A list of values that are opaque to Consul,
but can be used to distinguish between services or nodes.
## Attributes Reference
The following attributes are exported:
* `address` - The address of the service.
* `node` - The id of the service, defaults to the value of `name`.

View File

@ -3,15 +3,13 @@ layout: "consul"
page_title: "Consul: consul_keys"
sidebar_current: "docs-consul-resource-keys"
description: |-
Provides access to Key/Value data in Consul. This can be used to both read keys from Consul, but also to set the value of keys in Consul. This is a powerful way dynamically set values in templates, and to expose infrastructure details to clients.
Writes values into the Consul key/value store.
---
# consul\_keys
Provides access to Key/Value data in Consul. This can be used
to both read keys from Consul, but also to set the value of keys
in Consul. This is a powerful way dynamically set values in templates,
and to expose infrastructure details to clients.
`consul_keys` writes sets of individual values into Consul.
This is a powerful way to expose infrastructure details to clients.
This resource manages individual keys, and thus it can create, update and
delete the keys explicitly given. Howver, It is not able to detect and remove
@ -27,13 +25,6 @@ resource "consul_keys" "app" {
datacenter = "nyc1"
token = "abcd"
# Read the launch AMI from Consul
key {
name = "ami"
path = "service/app/launch_ami"
default = "ami-1234"
}
# Set the CNAME of our load balancer as a key
key {
name = "elb_cname"
@ -41,12 +32,6 @@ resource "consul_keys" "app" {
value = "${aws_elb.app.dns_name}"
}
}
# Start our instance with the dynamic ami value
resource "aws_instance" "app" {
ami = "${consul_keys.app.var.ami}"
...
}
```
## Argument Reference
@ -59,33 +44,31 @@ The following arguments are supported:
* `token` - (Optional) The ACL token to use. This overrides the
token that the agent provides by default.
* `key` - (Required) Specifies a key in Consul to be read or written.
* `key` - (Required) Specifies a key in Consul to be written.
Supported values documented below.
The `key` block supports the following:
* `name` - (Required) This is the name of the key. This value of the
key is exposed as `var.<name>`. This is not the path of the key
in Consul.
* `path` - (Required) This is the path in Consul that should be written to.
* `path` - (Required) This is the path in Consul that should be read
or written to.
* `default` - (Optional) This is the default value to set for `var.<name>`
if the key does not exist in Consul. Defaults to the empty string.
* `value` - (Optional) If set, the key will be set to this value.
This allows a key to be written to.
* `value` - (Required) The value to write to the given path.
* `delete` - (Optional) If true, then the key will be deleted when
either its configuration block is removed from the configuration or
the entire resource is destroyed. Otherwise, it will be left in Consul.
Defaults to false.
### Deprecated `key` arguments
Prior to Terraform 0.7 this resource was used both to read *and* write the
Consul key/value store. The read functionality has moved to the `consul_keys`
*data source*, whose documentation can be found via the navigation.
The pre-0.7 interface for reading is still supported for backward compatibilty,
but will be removed in a future version of Terraform.
## Attributes Reference
The following attributes are exported:
* `datacenter` - The datacenter the keys are being read/written to.
* `var.<name>` - For each name given, the corresponding attribute
has the value of the key.
* `datacenter` - The datacenter the keys are being written to.

View File

@ -0,0 +1,37 @@
---
layout: "consul"
page_title: "Consul: consul_node"
sidebar_current: "docs-consul-resource-node"
description: |-
Provides access to Node data in Consul. This can be used to define a node.
---
# consul\_node
Provides access to Node data in Consul. This can be used to define a node. Currently, defining health checks is not supported.
## Example Usage
```
resource "consul_node" "foobar" {
address = "192.168.10.10"
name = "foobar"
}
```
## Argument Reference
The following arguments are supported:
* `address` - (Required) The address of the node being added to
or referenced in the catalog.
* `name` - (Required) The name of the node being added to or
referenced in the catalog.
## Attributes Reference
The following attributes are exported:
* `address` - The address of the service.
* `name` - The name of the service.

View File

@ -0,0 +1,47 @@
---
layout: "consul"
page_title: "Consul: consul_service"
sidebar_current: "docs-consul-resource-service"
description: |-
A high-level resource for creating a Service in Consul. Since Consul requires clients to register services with either the catalog or an agent, `consul_service` may register with either the catalog or an agent, depending on the configuration of `consul_service`. For now, `consul_service` always registers services with the agent running at the address defined in the `consul` resource. Health checks are not currently supported.
---
# consul\_service
A high-level resource for creating a Service in Consul. Currently, defining health checks for a service is not supported.
## Example Usage
```
resource "consul_service" "google" {
address = "www.google.com"
name = "google"
port = 80
tags = ["tag0", "tag1"]
}
```
## Argument Reference
The following arguments are supported:
* `address` - (Optional) The address of the service. Defaults to the
address of the agent.
* `name` - (Required) The name of the service.
* `port` - (Optional) The port of the service.
* `tags` - (Optional) A list of values that are opaque to Consul,
but can be used to distinguish between services or nodes.
## Attributes Reference
The following attributes are exported:
* `address` - The address of the service.
* `id` - The id of the service, defaults to the value of `name`.
* `name` - The name of the service.
* `port` - The port of the service.
* `tags` - The tags of the service.

View File

@ -0,0 +1,40 @@
---
layout: "docker"
page_title: "Docker: docker_registry_image"
sidebar_current: "docs-docker-datasource-registry-image"
description: |-
Finds the latest available sha256 digest for a docker image/tag from a registry.
---
# docker\_registry\_image
-> **Note:** The initial (current) version of this data source can reliably read only **public** images **from the official Docker Hub Registry**.
Reads the image metadata from a Docker Registry. Used in conjunction with the
[docker\_image](/docs/providers/docker/r/image.html) resource to keep an image up
to date on the latest available version of the tag.
## Example Usage
```
data "docker_registry_image" "ubuntu" {
name = "ubuntu:precise"
}
resource "docker_image" "ubuntu" {
name = "${data.docker_image.ubuntu.name}"
pull_trigger = "${data.docker_registry_image.ubuntu.sha256_digest}"
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required, string) The name of the Docker image, including any tags. e.g. `alpine:latest`
## Attributes Reference
The following attributes are exported in addition to the above configuration:
* `id` (string) - The ID of the image, as stored on the registry.

View File

@ -41,6 +41,11 @@ resource "docker_image" "ubuntu" {
}
```
## Registry Credentials
The initial (current) version of the Docker provider **doesn't** support registry authentication.
This limits any use cases to public images for now.
## Argument Reference
The following arguments are supported:

View File

@ -3,15 +3,18 @@ layout: "docker"
page_title: "Docker: docker_image"
sidebar_current: "docs-docker-resource-image"
description: |-
Downloads and exports the ID of a Docker image.
Pulls a Docker image to a given Docker host.
---
# docker\_image
Downloads and exports the ID of a Docker image. This can be used alongside
[docker\_container](/docs/providers/docker/r/container.html)
to programmatically get the latest image IDs without having to hardcode
them.
-> **Note:** The initial (current) version of this resource can only pull **public** images **from the official Docker Hub Registry**.
Pulls a Docker image to a given Docker host from a Docker Registry.
This resource will *not* pull new layers of the image automatically unless used in
conjuction with [`docker_registry_image`](/docs/providers/docker/d/registry_image.html)
data source to update the `pull_trigger` field.
## Example Usage
@ -24,18 +27,31 @@ resource "docker_image" "ubuntu" {
# Access it somewhere else with ${docker_image.ubuntu.latest}
```
### Dynamic image
```
data "docker_registry_image" "ubuntu" {
name = "ubuntu:precise"
}
resource "docker_image" "ubuntu" {
name = "${data.docker_registry_image.ubuntu.name}"
pull_trigger = "${data.docker_registry_image.ubuntu.sha256_digest}"
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required, string) The name of the Docker image, including any tags.
* `keep_updated` - (Optional, boolean) If true, then the Docker image will
always be updated on the host to the latest. If this is false, as long as an
image is downloaded with the correct tag, it won't be redownloaded if
there is a newer image.
* `keep_locally` - (Optional, boolean) If true, then the Docker image won't be
deleted on destroy operation. If this is false, it will delete the image from
the docker local storage on destroy operation.
* `pull_trigger` - (Optional, string) Used to store the image digest from the
registry and will cause an image pull when changed. Needed when using
the `docker_registry_image` [data source](/docs/providers/docker/d/registry_image.html)
to trigger an update of the image.
## Attributes Reference

View File

@ -0,0 +1,52 @@
---
layout: "mysql"
page_title: "MySQL: mysql_grant"
sidebar_current: "docs-mysql-resource-grant"
description: |-
Creates and manages privileges given to a user on a MySQL server
---
# mysql\_grant
The ``mysql_grant`` resource creates and manages privileges given to
a user on a MySQL server.
## Example Usage
```
resource "mysql_user" "jdoe" {
user = "jdoe"
host = "example.com"
password = "password"
}
resource "mysql_grant" "jdoe" {
user = "${mysql_user.jdoe.user}"
host = "${mysql_user.jdoe.host}"
database = "app"
privileges = ["SELECT", "UPDATE"]
}
```
## Argument Reference
The following arguments are supported:
* `user` - (Required) The name of the user.
* `host` - (Optional) The source host of the user. Defaults to "localhost".
* `database` - (Required) The database to grant privileges on. At this time,
privileges are given to all tables on the database (`mydb.*`).
* `privileges` - (Required) A list of privileges to grant to the user. Refer
to a list of privileges (such as
[here](https://dev.mysql.com/doc/refman/5.5/en/grant.html)) for applicable
privileges.
* `grant` - (Optional) Whether to also give the user privileges to grant
the same privileges to other users.
## Attributes Reference
No further attributes are exported.

View File

@ -0,0 +1,37 @@
---
layout: "mysql"
page_title: "MySQL: mysql_user"
sidebar_current: "docs-mysql-resource-user"
description: |-
Creates and manages a user on a MySQL server.
---
# mysql\_user
The ``mysql_user`` resource creates and manages a user on a MySQL
server.
## Example Usage
```
resource "mysql_user" "jdoe" {
user = "jdoe"
host = "example.com"
password = "password"
}
```
## Argument Reference
The following arguments are supported:
* `user` - (Required) The name of the user.
* `host` - (Optional) The source host of the user. Defaults to "localhost".
* `password` - (Optional) The password of the user. The value of this
argument is plain-text so make sure to secure where this is defined.
## Attributes Reference
No further attributes are exported.

View File

@ -10,15 +10,36 @@
<a href="/docs/providers/consul/index.html">Consul Provider</a>
</li>
<li<%= sidebar_current(/^docs-consul-data-source/) %>>
<a href="#">Data Sources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-consul-data-source-keys") %>>
<a href="/docs/providers/consul/d/keys.html">consul_keys</a>
</li>
</ul>
</li>
<li<%= sidebar_current(/^docs-consul-resource/) %>>
<a href="#">Resources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-consul-resource-agent-service") %>>
<a href="/docs/providers/consul/r/agent_service.html">consul_agent_service</a>
</li>
<li<%= sidebar_current("docs-consul-resource-catalog-entry") %>>
<a href="/docs/providers/consul/r/catalog_entry.html">consul_catalog_entry</a>
</li>
<li<%= sidebar_current("docs-consul-resource-keys") %>>
<a href="/docs/providers/consul/r/keys.html">consul_keys</a>
</li>
<li<%= sidebar_current("docs-consul-resource-key-prefix") %>>
<a href="/docs/providers/consul/r/key_prefix.html">consul_key_prefix</a>
</li>
<li<%= sidebar_current("docs-consul-resource-node") %>>
<a href="/docs/providers/consul/r/node.html">consul_node</a>
</li>
<li<%= sidebar_current("docs-consul-resource-service") %>>
<a href="/docs/providers/consul/r/service.html">consul_service</a>
</li>
</ul>
</li>
</ul>

View File

@ -10,6 +10,15 @@
<a href="/docs/providers/docker/index.html">Docker Provider</a>
</li>
<li<%= sidebar_current(/^docs-docker-datasource/) %>>
<a href="#">Data Sources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-docker-datasource-registry-image") %>>
<a href="/docs/providers/docker/d/registry_image.html">docker_registry_image</a>
</li>
</ul>
</li>
<li<%= sidebar_current(/^docs-docker-resource/) %>>
<a href="#">Resources</a>
<ul class="nav nav-visible">

View File

@ -16,6 +16,12 @@
<li<%= sidebar_current("docs-mysql-resource-database") %>>
<a href="/docs/providers/mysql/r/database.html">mysql_database</a>
</li>
<li<%= sidebar_current("docs-mysql-resource-grant") %>>
<a href="/docs/providers/mysql/r/grant.html">mysql_grant</a>
</li>
<li<%= sidebar_current("docs-mysql-resource-user") %>>
<a href="/docs/providers/mysql/r/user.html">mysql_user</a>
</li>
</ul>
</li>
</ul>