Merge branch 'master' into b-aws-sqs-defaults
This commit is contained in:
commit
3a1f2e6fe9
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -8,7 +8,7 @@ BACKWARDS INCOMPATIBILITIES / NOTES:
|
||||||
* Quotation marks may no longer be escaped in HIL expressions [GH-7201]
|
* Quotation marks may no longer be escaped in HIL expressions [GH-7201]
|
||||||
* `openstack_networking_subnet_v2` now defaults to turning DHCP on.
|
* `openstack_networking_subnet_v2` now defaults to turning DHCP on.
|
||||||
* `aws_elb` now defaults `cross_zone_load_balancing` to `true`
|
* `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
|
`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,
|
managing Instances inside VPCs will need to use `vpc_security_group_ids` instead,
|
||||||
and reference the security groups by their `id`.
|
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_dns_cname_record` now accepts a single record rather than a list of records
|
||||||
* `azurerm_virtual_machine` computer_name now Required
|
* `azurerm_virtual_machine` computer_name now Required
|
||||||
* `aws_db_instance` now defaults `publicly_accessible` to false
|
* `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.
|
* `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
|
* `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.
|
* 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_s3_bucket_object` [GH-6946]
|
||||||
* **New Data Source:** `aws_ecs_container_definition` [GH-7230]
|
* **New Data Source:** `aws_ecs_container_definition` [GH-7230]
|
||||||
* **New Data Source:** `atlas_artifact` [GH-7419]
|
* **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:** `sort` [GH-7128]
|
||||||
* **New Interpolation Function:** `distinct` [GH-7174]
|
* **New Interpolation Function:** `distinct` [GH-7174]
|
||||||
* **New Provider:** `grafana` [GH-6206]
|
* **New Provider:** `grafana` [GH-6206]
|
||||||
|
@ -79,6 +83,12 @@ FEATURES:
|
||||||
* **New Resource:** `datadog_timeboard` [GH-6900]
|
* **New Resource:** `datadog_timeboard` [GH-6900]
|
||||||
* **New Resource:** `digitalocean_tag` [GH-7500]
|
* **New Resource:** `digitalocean_tag` [GH-7500]
|
||||||
* **New Resource:** `digitalocean_volume` [GH-7560]
|
* **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: 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 `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]
|
* 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/datadog: Add support for 'require full window' and 'locked' [GH-6738]
|
||||||
* provider/docker: Docker Container DNS Setting Enhancements [GH-7392]
|
* 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 `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 Cache Settings [GH-6781]
|
||||||
* provider/fastly: Add support for Service Request Settings on `fastly_service_v1` resources [GH-6622]
|
* 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]
|
* 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: 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 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: 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: 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: 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]
|
* provider/azurerm: Normalizes `availability_set_id` casing to avoid spurious diffs in `azurerm_virtual_machine` [GH-6768]
|
||||||
|
|
|
@ -311,7 +311,9 @@ func resourceAwsElasticBeanstalkEnvironmentUpdate(d *schema.ResourceData, meta i
|
||||||
|
|
||||||
if d.HasChange("solution_stack_name") {
|
if d.HasChange("solution_stack_name") {
|
||||||
hasChange = true
|
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") {
|
if d.HasChange("setting") {
|
||||||
|
@ -332,7 +334,9 @@ func resourceAwsElasticBeanstalkEnvironmentUpdate(d *schema.ResourceData, meta i
|
||||||
|
|
||||||
if d.HasChange("template_name") {
|
if d.HasChange("template_name") {
|
||||||
hasChange = true
|
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 {
|
if hasChange {
|
||||||
|
|
|
@ -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 {
|
func testAccCheckBeanstalkEnvDestroy(s *terraform.State) error {
|
||||||
conn := testAccProvider.Meta().(*AWSClient).elasticbeanstalkconn
|
conn := testAccProvider.Meta().(*AWSClient).elasticbeanstalkconn
|
||||||
|
|
||||||
|
@ -571,3 +605,63 @@ resource "aws_elastic_beanstalk_environment" "default" {
|
||||||
}
|
}
|
||||||
`, name)
|
`, 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)
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Datacenter string `mapstructure:"datacenter"`
|
Datacenter string `mapstructure:"datacenter"`
|
||||||
Address string `mapstructure:"address"`
|
Address string `mapstructure:"address"`
|
||||||
|
Token string `mapstructure:"token"`
|
||||||
Scheme string `mapstructure:"scheme"`
|
Scheme string `mapstructure:"scheme"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +26,9 @@ func (c *Config) Client() (*consulapi.Client, error) {
|
||||||
if c.Scheme != "" {
|
if c.Scheme != "" {
|
||||||
config.Scheme = c.Scheme
|
config.Scheme = c.Scheme
|
||||||
}
|
}
|
||||||
|
if c.Token != "" {
|
||||||
|
config.Token = c.Token
|
||||||
|
}
|
||||||
client, err := consulapi.NewClient(config)
|
client, err := consulapi.NewClient(config)
|
||||||
|
|
||||||
log.Printf("[INFO] Consul Client configured with address: '%s', scheme: '%s', datacenter: '%s'",
|
log.Printf("[INFO] Consul Client configured with address: '%s', scheme: '%s', datacenter: '%s'",
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -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(®istration); 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
|
||||||
|
}
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
`
|
|
@ -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
|
||||||
|
}
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -37,8 +37,9 @@ func resourceConsulKeys() *schema.Resource {
|
||||||
Elem: &schema.Resource{
|
Elem: &schema.Resource{
|
||||||
Schema: map[string]*schema.Schema{
|
Schema: map[string]*schema.Schema{
|
||||||
"name": &schema.Schema{
|
"name": &schema.Schema{
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Required: true,
|
Optional: true,
|
||||||
|
Deprecated: "Using consul_keys resource to *read* is deprecated; please use consul_keys data source instead",
|
||||||
},
|
},
|
||||||
|
|
||||||
"path": &schema.Schema{
|
"path": &schema.Schema{
|
||||||
|
@ -220,7 +221,12 @@ func resourceConsulKeysRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
value = attributeValue(sub, value)
|
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
|
// If there is already a "value" attribute present for this key
|
||||||
// then it was created as a "write" block. We need to update the
|
// 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)
|
return "", "", nil, fmt.Errorf("Failed to unroll: %#v", raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
key, ok := sub["name"].(string)
|
key := sub["name"].(string)
|
||||||
if !ok {
|
|
||||||
return "", "", nil, fmt.Errorf("Failed to expand key '%#v'", sub)
|
|
||||||
}
|
|
||||||
|
|
||||||
path, ok := sub["path"].(string)
|
path, ok := sub["path"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
`
|
|
@ -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(®istration); 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
|
||||||
|
}
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
`
|
|
@ -26,11 +26,24 @@ func Provider() terraform.ResourceProvider {
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"token": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
DataSourcesMap: map[string]*schema.Resource{
|
||||||
|
"consul_keys": dataSourceConsulKeys(),
|
||||||
},
|
},
|
||||||
|
|
||||||
ResourcesMap: map[string]*schema.Resource{
|
ResourcesMap: map[string]*schema.Resource{
|
||||||
"consul_keys": resourceConsulKeys(),
|
"consul_agent_service": resourceConsulAgentService(),
|
||||||
"consul_key_prefix": resourceConsulKeyPrefix(),
|
"consul_catalog_entry": resourceConsulCatalogEntry(),
|
||||||
|
"consul_keys": resourceConsulKeys(),
|
||||||
|
"consul_key_prefix": resourceConsulKeyPrefix(),
|
||||||
|
"consul_node": resourceConsulNode(),
|
||||||
|
"consul_service": resourceConsulService(),
|
||||||
},
|
},
|
||||||
|
|
||||||
ConfigureFunc: providerConfigure,
|
ConfigureFunc: providerConfigure,
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
`
|
|
@ -32,6 +32,10 @@ func Provider() terraform.ResourceProvider {
|
||||||
"docker_volume": resourceDockerVolume(),
|
"docker_volume": resourceDockerVolume(),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
DataSourcesMap: map[string]*schema.Resource{
|
||||||
|
"docker_registry_image": dataSourceDockerRegistryImage(),
|
||||||
|
},
|
||||||
|
|
||||||
ConfigureFunc: providerConfigure,
|
ConfigureFunc: providerConfigure,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,6 @@ func resourceDockerImage() *schema.Resource {
|
||||||
Required: true,
|
Required: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
"keep_updated": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
"latest": &schema.Schema{
|
"latest": &schema.Schema{
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
Computed: true,
|
Computed: true,
|
||||||
|
@ -31,6 +26,12 @@ func resourceDockerImage() *schema.Resource {
|
||||||
Type: schema.TypeBool,
|
Type: schema.TypeBool,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"pull_trigger": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,25 @@ func resourceDockerImageCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func resourceDockerImageRead(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)
|
client := meta.(*dc.Client)
|
||||||
apiImage, err := findImage(d, client)
|
apiImage, err := findImage(d, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -33,13 +52,6 @@ func resourceDockerImageRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
return nil
|
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 {
|
func resourceDockerImageDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
client := meta.(*dc.Client)
|
client := meta.(*dc.Client)
|
||||||
err := removeImage(d, 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
|
// TODO: Test local registry handling. It should be working
|
||||||
// based on the code that was ported over
|
// 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{}
|
pullOpts := dc.PullImageOptions{}
|
||||||
|
|
||||||
splitImageName := strings.Split(image, ":")
|
splitImageName := strings.Split(image, ":")
|
||||||
|
@ -151,32 +174,7 @@ func pullImage(data *Data, client *dc.Client, image string) error {
|
||||||
pullOpts.Repository = image
|
pullOpts.Repository = image
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := client.PullImage(pullOpts, dc.AuthConfiguration{}); err != nil {
|
return pullOpts
|
||||||
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 ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error) {
|
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)
|
foundImage := searchLocalImages(data, imageName)
|
||||||
|
|
||||||
if d.Get("keep_updated").(bool) || foundImage == nil {
|
if foundImage == nil {
|
||||||
if err := pullImage(&data, client, imageName); err != nil {
|
if err := pullImage(&data, client, imageName); err != nil {
|
||||||
return nil, fmt.Errorf("Unable to pull image %s: %s", imageName, err)
|
return nil, fmt.Errorf("Unable to pull image %s: %s", imageName, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
func testAccDockerImageDestroy(s *terraform.State) error {
|
||||||
for _, rs := range s.RootModule().Resources {
|
for _, rs := range s.RootModule().Resources {
|
||||||
if rs.Type != "docker_image" {
|
if rs.Type != "docker_image" {
|
||||||
|
@ -93,14 +109,12 @@ func testAccDockerImageDestroy(s *terraform.State) error {
|
||||||
const testAccDockerImageConfig = `
|
const testAccDockerImageConfig = `
|
||||||
resource "docker_image" "foo" {
|
resource "docker_image" "foo" {
|
||||||
name = "alpine:3.1"
|
name = "alpine:3.1"
|
||||||
keep_updated = false
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const testAddDockerPrivateImageConfig = `
|
const testAddDockerPrivateImageConfig = `
|
||||||
resource "docker_image" "foobar" {
|
resource "docker_image" "foobar" {
|
||||||
name = "gcr.io:443/google_containers/pause:0.8.0"
|
name = "gcr.io:443/google_containers/pause:0.8.0"
|
||||||
keep_updated = true
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -110,3 +124,13 @@ resource "docker_image" "foobarzoo" {
|
||||||
keep_locally = true
|
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}"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
|
@ -50,6 +50,8 @@ func Provider() terraform.ResourceProvider {
|
||||||
|
|
||||||
ResourcesMap: map[string]*schema.Resource{
|
ResourcesMap: map[string]*schema.Resource{
|
||||||
"mysql_database": resourceDatabase(),
|
"mysql_database": resourceDatabase(),
|
||||||
|
"mysql_user": resourceUser(),
|
||||||
|
"mysql_grant": resourceGrant(),
|
||||||
},
|
},
|
||||||
|
|
||||||
ConfigureFunc: providerConfigure,
|
ConfigureFunc: providerConfigure,
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
`
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
`
|
|
@ -3,21 +3,46 @@ package command
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl"
|
"github.com/hashicorp/hcl"
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FlagKV is a flag.Value implementation for parsing user variables
|
// FlagTypedKVis a flag.Value implementation for parsing user variables
|
||||||
// from the command-line in the format of '-var key=value'.
|
// from the command-line in the format of '-var key=value', where value is
|
||||||
type FlagKV map[string]string
|
// 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 ""
|
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, "=")
|
idx := strings.Index(raw, "=")
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
return fmt.Errorf("No '=' value in arg: %s", raw)
|
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
|
// FlagKVFile is a flag.Value implementation for parsing user variables
|
||||||
// from the command line in the form of files. i.e. '-var-file=foo'
|
// 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 {
|
func (v *FlagKVFile) String() string {
|
||||||
return ""
|
return ""
|
||||||
|
@ -47,7 +72,7 @@ func (v *FlagKVFile) Set(raw string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if *v == nil {
|
if *v == nil {
|
||||||
*v = make(map[string]string)
|
*v = make(map[string]interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value := range vs {
|
for key, value := range vs {
|
||||||
|
@ -57,7 +82,7 @@ func (v *FlagKVFile) Set(raw string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadKVFile(rawPath string) (map[string]string, error) {
|
func loadKVFile(rawPath string) (map[string]interface{}, error) {
|
||||||
path, err := homedir.Expand(rawPath)
|
path, err := homedir.Expand(rawPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
|
@ -78,7 +103,7 @@ func loadKVFile(rawPath string) (map[string]string, error) {
|
||||||
"Error parsing %s: %s", path, err)
|
"Error parsing %s: %s", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var result map[string]string
|
var result map[string]interface{}
|
||||||
if err := hcl.DecodeObject(&result, obj); err != nil {
|
if err := hcl.DecodeObject(&result, obj); err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"Error decoding Terraform vars file: %s\n\n"+
|
"Error decoding Terraform vars file: %s\n\n"+
|
||||||
|
@ -103,3 +128,49 @@ func (v *FlagStringSlice) Set(raw string) error {
|
||||||
|
|
||||||
return nil
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -2,16 +2,17 @@ package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFlagKV_impl(t *testing.T) {
|
func TestFlagStringKV_impl(t *testing.T) {
|
||||||
var _ flag.Value = new(FlagKV)
|
var _ flag.Value = new(FlagStringKV)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFlagKV(t *testing.T) {
|
func TestFlagStringKV(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
Input string
|
Input string
|
||||||
Output map[string]string
|
Output map[string]string
|
||||||
|
@ -49,10 +50,10 @@ func TestFlagKV(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
f := new(FlagKV)
|
f := new(FlagStringKV)
|
||||||
err := f.Set(tc.Input)
|
err := f.Set(tc.Input)
|
||||||
if err != nil != tc.Error {
|
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)
|
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) {
|
func TestFlagKVFile_impl(t *testing.T) {
|
||||||
var _ flag.Value = new(FlagKVFile)
|
var _ flag.Value = new(FlagKVFile)
|
||||||
}
|
}
|
||||||
|
@ -76,24 +157,24 @@ foo = "bar"
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
Input string
|
Input string
|
||||||
Output map[string]string
|
Output map[string]interface{}
|
||||||
Error bool
|
Error bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
inputLibucl,
|
inputLibucl,
|
||||||
map[string]string{"foo": "bar"},
|
map[string]interface{}{"foo": "bar"},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
inputJson,
|
inputJson,
|
||||||
map[string]string{"foo": "bar"},
|
map[string]interface{}{"foo": "bar"},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
`map.key = "foo"`,
|
`map.key = "foo"`,
|
||||||
map[string]string{"map.key": "foo"},
|
map[string]interface{}{"map.key": "foo"},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -111,7 +192,7 @@ foo = "bar"
|
||||||
t.Fatalf("bad error. Input: %#v, err: %s", tc.Input, err)
|
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) {
|
if !reflect.DeepEqual(actual, tc.Output) {
|
||||||
t.Fatalf("bad: %#v", actual)
|
t.Fatalf("bad: %#v", actual)
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ func (c *InitCommand) Run(args []string) int {
|
||||||
remoteConfig := make(map[string]string)
|
remoteConfig := make(map[string]string)
|
||||||
cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError)
|
cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError)
|
||||||
cmdFlags.StringVar(&remoteBackend, "backend", "", "")
|
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()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
|
|
|
@ -36,9 +36,9 @@ type Meta struct {
|
||||||
|
|
||||||
// Variables for the context (private)
|
// Variables for the context (private)
|
||||||
autoKey string
|
autoKey string
|
||||||
autoVariables map[string]string
|
autoVariables map[string]interface{}
|
||||||
input bool
|
input bool
|
||||||
variables map[string]string
|
variables map[string]interface{}
|
||||||
|
|
||||||
// Targets for this context (private)
|
// Targets for this context (private)
|
||||||
targets []string
|
targets []string
|
||||||
|
@ -315,7 +315,7 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
|
||||||
func (m *Meta) flagSet(n string) *flag.FlagSet {
|
func (m *Meta) flagSet(n string) *flag.FlagSet {
|
||||||
f := flag.NewFlagSet(n, flag.ContinueOnError)
|
f := flag.NewFlagSet(n, flag.ContinueOnError)
|
||||||
f.BoolVar(&m.input, "input", true, "input")
|
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((*FlagKVFile)(&m.variables), "var-file", "variable file")
|
||||||
f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target")
|
f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target")
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ func (c *RemoteConfigCommand) Run(args []string) int {
|
||||||
cmdFlags.StringVar(&c.conf.statePath, "state", DefaultStateFilename, "path")
|
cmdFlags.StringVar(&c.conf.statePath, "state", DefaultStateFilename, "path")
|
||||||
cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path")
|
cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path")
|
||||||
cmdFlags.StringVar(&c.remoteConf.Type, "backend", "atlas", "")
|
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()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("\nError parsing CLI flags: %s", err))
|
c.Ui.Error(fmt.Sprintf("\nError parsing CLI flags: %s", err))
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/hashicorp/hcl"
|
||||||
"github.com/hashicorp/terraform/config"
|
"github.com/hashicorp/terraform/config"
|
||||||
"github.com/hashicorp/terraform/config/module"
|
"github.com/hashicorp/terraform/config/module"
|
||||||
)
|
)
|
||||||
|
@ -119,24 +120,109 @@ func NewContext(opts *ContextOpts) (*Context, error) {
|
||||||
par = 10
|
par = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup the variables. We first take the variables given to us.
|
// Set up the variables in the following sequence:
|
||||||
// We then merge in the variables set in the environment.
|
// 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{})
|
variables := make(map[string]interface{})
|
||||||
for _, v := range os.Environ() {
|
|
||||||
if !strings.HasPrefix(v, VarEnvPrefix) {
|
if opts.Module != nil {
|
||||||
continue
|
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 "="
|
for _, v := range os.Environ() {
|
||||||
idx := strings.Index(v, "=")
|
if !strings.HasPrefix(v, VarEnvPrefix) {
|
||||||
k := v[len(VarEnvPrefix):idx]
|
continue
|
||||||
v = v[idx+1:]
|
}
|
||||||
|
|
||||||
// Override the command-line set variable
|
// Strip off the prefix and get the value after the first "="
|
||||||
variables[k] = v
|
idx := strings.Index(v, "=")
|
||||||
}
|
k := v[len(VarEnvPrefix):idx]
|
||||||
for k, v := range opts.Variables {
|
v = v[idx+1:]
|
||||||
variables[k] = v
|
|
||||||
|
// 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{
|
return &Context{
|
||||||
|
@ -548,3 +634,45 @@ func (c *Context) walk(
|
||||||
walker := &ContextGraphWalker{Context: c, Operation: operation}
|
walker := &ContextGraphWalker{Context: c, Operation: operation}
|
||||||
return walker, graph.Walk(walker)
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1135,7 +1135,11 @@ func TestContext2Apply_mapVariableOverride(t *testing.T) {
|
||||||
"aws": testProviderFuncFixed(p),
|
"aws": testProviderFuncFixed(p),
|
||||||
},
|
},
|
||||||
Variables: map[string]interface{}{
|
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),
|
"aws": testProviderFuncFixed(p),
|
||||||
},
|
},
|
||||||
Variables: map[string]interface{}{
|
Variables: map[string]interface{}{
|
||||||
"foo": "us-west-2",
|
"foo": "us-west-2",
|
||||||
"amis.us-east-1": "override",
|
"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) {
|
func TestContext2Apply_varsEnv(t *testing.T) {
|
||||||
// Set the env var
|
// Set the env var
|
||||||
old := tempEnv(t, "TF_VAR_ami", "baz")
|
old_ami := tempEnv(t, "TF_VAR_ami", "baz")
|
||||||
defer os.Setenv("TF_VAR_ami", old)
|
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")
|
m := testModule(t, "apply-vars-env")
|
||||||
p := testProvider("aws")
|
p := testProvider("aws")
|
||||||
|
|
|
@ -19,8 +19,12 @@ func TestContext2Input(t *testing.T) {
|
||||||
"aws": testProviderFuncFixed(p),
|
"aws": testProviderFuncFixed(p),
|
||||||
},
|
},
|
||||||
Variables: map[string]interface{}{
|
Variables: map[string]interface{}{
|
||||||
"foo": "us-west-2",
|
"foo": "us-west-2",
|
||||||
"amis.us-east-1": "override",
|
"amis": []map[string]interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"us-east-1": "override",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
UIInput: input,
|
UIInput: input,
|
||||||
})
|
})
|
||||||
|
|
|
@ -95,16 +95,42 @@ func smcUserVariables(c *config.Config, vs map[string]interface{}) []error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that types match up
|
// Check that types match up
|
||||||
for k, _ := range vs {
|
for name, proposedValue := range vs {
|
||||||
v, ok := cvs[k]
|
schema, ok := cvs[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.Type() != config.VariableTypeString {
|
declaredType := schema.Type()
|
||||||
errs = append(errs, fmt.Errorf(
|
|
||||||
"%s: cannot assign string value to map type",
|
switch declaredType {
|
||||||
k))
|
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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -705,6 +705,8 @@ aws_instance.bar:
|
||||||
aws_instance.foo:
|
aws_instance.foo:
|
||||||
ID = foo
|
ID = foo
|
||||||
bar = baz
|
bar = baz
|
||||||
|
list = Hello,World
|
||||||
|
map = Baz,Foo,Hello
|
||||||
num = 2
|
num = 2
|
||||||
type = aws_instance
|
type = aws_instance
|
||||||
`
|
`
|
||||||
|
@ -712,6 +714,8 @@ aws_instance.foo:
|
||||||
const testTerraformApplyVarsEnvStr = `
|
const testTerraformApplyVarsEnvStr = `
|
||||||
aws_instance.bar:
|
aws_instance.bar:
|
||||||
ID = foo
|
ID = foo
|
||||||
|
bar = Hello,World
|
||||||
|
baz = Baz,Foo,Hello
|
||||||
foo = baz
|
foo = baz
|
||||||
type = aws_instance
|
type = aws_instance
|
||||||
`
|
`
|
||||||
|
|
|
@ -1,7 +1,20 @@
|
||||||
variable "ami" {
|
variable "ami" {
|
||||||
default = "foo"
|
default = "foo"
|
||||||
|
type = "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "list" {
|
||||||
|
default = []
|
||||||
|
type = "list"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "map" {
|
||||||
|
default = {}
|
||||||
|
type = "map"
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_instance" "bar" {
|
resource "aws_instance" "bar" {
|
||||||
foo = "${var.ami}"
|
foo = "${var.ami}"
|
||||||
|
bar = "${join(",", var.list)}"
|
||||||
|
baz = "${join(",", keys(var.map))}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,14 @@ variable "amis" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "test_list" {
|
||||||
|
type = "list"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "test_map" {
|
||||||
|
type = "map"
|
||||||
|
}
|
||||||
|
|
||||||
variable "bar" {
|
variable "bar" {
|
||||||
default = "baz"
|
default = "baz"
|
||||||
}
|
}
|
||||||
|
@ -14,6 +22,8 @@ variable "foo" {}
|
||||||
resource "aws_instance" "foo" {
|
resource "aws_instance" "foo" {
|
||||||
num = "2"
|
num = "2"
|
||||||
bar = "${var.bar}"
|
bar = "${var.bar}"
|
||||||
|
list = "${join(",", var.test_list)}"
|
||||||
|
map = "${join(",", keys(var.test_map))}"
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_instance" "bar" {
|
resource "aws_instance" "bar" {
|
||||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -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`.
|
|
@ -3,15 +3,13 @@ layout: "consul"
|
||||||
page_title: "Consul: consul_keys"
|
page_title: "Consul: consul_keys"
|
||||||
sidebar_current: "docs-consul-resource-keys"
|
sidebar_current: "docs-consul-resource-keys"
|
||||||
description: |-
|
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
|
# consul\_keys
|
||||||
|
|
||||||
Provides access to Key/Value data in Consul. This can be used
|
`consul_keys` writes sets of individual values into Consul.
|
||||||
to both read keys from Consul, but also to set the value of keys
|
This is a powerful way to expose infrastructure details to clients.
|
||||||
in Consul. This is a powerful way dynamically set values in templates,
|
|
||||||
and to expose infrastructure details to clients.
|
|
||||||
|
|
||||||
This resource manages individual keys, and thus it can create, update and
|
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
|
delete the keys explicitly given. Howver, It is not able to detect and remove
|
||||||
|
@ -27,13 +25,6 @@ resource "consul_keys" "app" {
|
||||||
datacenter = "nyc1"
|
datacenter = "nyc1"
|
||||||
token = "abcd"
|
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
|
# Set the CNAME of our load balancer as a key
|
||||||
key {
|
key {
|
||||||
name = "elb_cname"
|
name = "elb_cname"
|
||||||
|
@ -41,12 +32,6 @@ resource "consul_keys" "app" {
|
||||||
value = "${aws_elb.app.dns_name}"
|
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
|
## Argument Reference
|
||||||
|
@ -59,33 +44,31 @@ The following arguments are supported:
|
||||||
* `token` - (Optional) The ACL token to use. This overrides the
|
* `token` - (Optional) The ACL token to use. This overrides the
|
||||||
token that the agent provides by default.
|
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.
|
Supported values documented below.
|
||||||
|
|
||||||
The `key` block supports the following:
|
The `key` block supports the following:
|
||||||
|
|
||||||
* `name` - (Required) This is the name of the key. This value of the
|
* `path` - (Required) This is the path in Consul that should be written to.
|
||||||
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
|
* `value` - (Required) The value to write to the given path.
|
||||||
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.
|
|
||||||
|
|
||||||
* `delete` - (Optional) If true, then the key will be deleted when
|
* `delete` - (Optional) If true, then the key will be deleted when
|
||||||
either its configuration block is removed from the configuration or
|
either its configuration block is removed from the configuration or
|
||||||
the entire resource is destroyed. Otherwise, it will be left in Consul.
|
the entire resource is destroyed. Otherwise, it will be left in Consul.
|
||||||
Defaults to false.
|
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
|
## Attributes Reference
|
||||||
|
|
||||||
The following attributes are exported:
|
The following attributes are exported:
|
||||||
|
|
||||||
* `datacenter` - The datacenter the keys are being read/written to.
|
* `datacenter` - The datacenter the keys are being written to.
|
||||||
* `var.<name>` - For each name given, the corresponding attribute
|
|
||||||
has the value of the key.
|
|
||||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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
|
## Argument Reference
|
||||||
|
|
||||||
The following arguments are supported:
|
The following arguments are supported:
|
||||||
|
|
|
@ -3,15 +3,18 @@ layout: "docker"
|
||||||
page_title: "Docker: docker_image"
|
page_title: "Docker: docker_image"
|
||||||
sidebar_current: "docs-docker-resource-image"
|
sidebar_current: "docs-docker-resource-image"
|
||||||
description: |-
|
description: |-
|
||||||
Downloads and exports the ID of a Docker image.
|
Pulls a Docker image to a given Docker host.
|
||||||
---
|
---
|
||||||
|
|
||||||
# docker\_image
|
# docker\_image
|
||||||
|
|
||||||
Downloads and exports the ID of a Docker image. This can be used alongside
|
-> **Note:** The initial (current) version of this resource can only pull **public** images **from the official Docker Hub Registry**.
|
||||||
[docker\_container](/docs/providers/docker/r/container.html)
|
|
||||||
to programmatically get the latest image IDs without having to hardcode
|
Pulls a Docker image to a given Docker host from a Docker Registry.
|
||||||
them.
|
|
||||||
|
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
|
## Example Usage
|
||||||
|
|
||||||
|
@ -24,18 +27,31 @@ resource "docker_image" "ubuntu" {
|
||||||
# Access it somewhere else with ${docker_image.ubuntu.latest}
|
# 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
|
## Argument Reference
|
||||||
|
|
||||||
The following arguments are supported:
|
The following arguments are supported:
|
||||||
|
|
||||||
* `name` - (Required, string) The name of the Docker image, including any tags.
|
* `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
|
* `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
|
deleted on destroy operation. If this is false, it will delete the image from
|
||||||
the docker local storage on destroy operation.
|
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
|
## Attributes Reference
|
||||||
|
|
||||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -10,15 +10,36 @@
|
||||||
<a href="/docs/providers/consul/index.html">Consul Provider</a>
|
<a href="/docs/providers/consul/index.html">Consul Provider</a>
|
||||||
</li>
|
</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/) %>>
|
<li<%= sidebar_current(/^docs-consul-resource/) %>>
|
||||||
<a href="#">Resources</a>
|
<a href="#">Resources</a>
|
||||||
<ul class="nav nav-visible">
|
<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") %>>
|
<li<%= sidebar_current("docs-consul-resource-keys") %>>
|
||||||
<a href="/docs/providers/consul/r/keys.html">consul_keys</a>
|
<a href="/docs/providers/consul/r/keys.html">consul_keys</a>
|
||||||
</li>
|
</li>
|
||||||
<li<%= sidebar_current("docs-consul-resource-key-prefix") %>>
|
<li<%= sidebar_current("docs-consul-resource-key-prefix") %>>
|
||||||
<a href="/docs/providers/consul/r/key_prefix.html">consul_key_prefix</a>
|
<a href="/docs/providers/consul/r/key_prefix.html">consul_key_prefix</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -10,6 +10,15 @@
|
||||||
<a href="/docs/providers/docker/index.html">Docker Provider</a>
|
<a href="/docs/providers/docker/index.html">Docker Provider</a>
|
||||||
</li>
|
</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/) %>>
|
<li<%= sidebar_current(/^docs-docker-resource/) %>>
|
||||||
<a href="#">Resources</a>
|
<a href="#">Resources</a>
|
||||||
<ul class="nav nav-visible">
|
<ul class="nav nav-visible">
|
||||||
|
|
|
@ -16,6 +16,12 @@
|
||||||
<li<%= sidebar_current("docs-mysql-resource-database") %>>
|
<li<%= sidebar_current("docs-mysql-resource-database") %>>
|
||||||
<a href="/docs/providers/mysql/r/database.html">mysql_database</a>
|
<a href="/docs/providers/mysql/r/database.html">mysql_database</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
Loading…
Reference in New Issue