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]
|
||||
* `openstack_networking_subnet_v2` now defaults to turning DHCP on.
|
||||
* `aws_elb` now defaults `cross_zone_load_balancing` to `true`
|
||||
* `resource_aws_instance`: EC2 Classic users may continue to use
|
||||
* `aws_instance`: EC2 Classic users may continue to use
|
||||
`security_groups` to reference Security Groups by their `name`. Users who are
|
||||
managing Instances inside VPCs will need to use `vpc_security_group_ids` instead,
|
||||
and reference the security groups by their `id`.
|
||||
|
@ -21,6 +21,8 @@ BACKWARDS INCOMPATIBILITIES / NOTES:
|
|||
* `azurerm_dns_cname_record` now accepts a single record rather than a list of records
|
||||
* `azurerm_virtual_machine` computer_name now Required
|
||||
* `aws_db_instance` now defaults `publicly_accessible` to false
|
||||
* `keep_updated` parameter removed from `docker_image` - This parameter never did what it was supposed to do.
|
||||
See relevant docs, specifically `pull_trigger` & new `docker_registry_image` data source to understand how to keep your `docker_image` updated.
|
||||
* `openstack_fw_policy_v1` now correctly applies rules in the order they are specified. Upon the next apply, current rules might be re-ordered.
|
||||
* `atlas_artifact` resource has be deprecated. Please use the new `atlas_artifact` Data Source
|
||||
* The `member` attribute of `openstack_lb_pool_v1` has been deprecated. Please ue the new `openstack_lb_member_v1` resource.
|
||||
|
@ -41,6 +43,8 @@ FEATURES:
|
|||
* **New Data Source:** `aws_s3_bucket_object` [GH-6946]
|
||||
* **New Data Source:** `aws_ecs_container_definition` [GH-7230]
|
||||
* **New Data Source:** `atlas_artifact` [GH-7419]
|
||||
* **New Data Source:** `docker_registry_image` [GH-7000]
|
||||
* **New Data Source:** `consul_keys` [GH-7678]
|
||||
* **New Interpolation Function:** `sort` [GH-7128]
|
||||
* **New Interpolation Function:** `distinct` [GH-7174]
|
||||
* **New Provider:** `grafana` [GH-6206]
|
||||
|
@ -79,6 +83,12 @@ FEATURES:
|
|||
* **New Resource:** `datadog_timeboard` [GH-6900]
|
||||
* **New Resource:** `digitalocean_tag` [GH-7500]
|
||||
* **New Resource:** `digitalocean_volume` [GH-7560]
|
||||
* **New Resource:** `consul_agent_service` [GH-7508]
|
||||
* **New Resource:** `consul_catalog_entry` [GH-7508]
|
||||
* **New Resource:** `consul_node` [GH-7508]
|
||||
* **New Resource:** `consul_service` [GH-7508]
|
||||
* **New Resource:** `mysql_grant` [GH-7656]
|
||||
* **New Resource:** `mysql_user` [GH-7656]
|
||||
* core: Tainted resources now show up in the plan and respect dependency ordering [GH-6600]
|
||||
* core: The `lookup` interpolation function can now have a default fall-back value specified [GH-6884]
|
||||
* core: The `terraform plan` command no longer persists state. [GH-6811]
|
||||
|
@ -148,6 +158,7 @@ IMPROVEMENTS:
|
|||
* provider/datadog: Add support for 'require full window' and 'locked' [GH-6738]
|
||||
* provider/docker: Docker Container DNS Setting Enhancements [GH-7392]
|
||||
* provider/docker: Add `destroy_grace_seconds` option to stop container before delete [GH-7513]
|
||||
* provider/docker: Add `pull_trigger` option to `docker_image` to trigger pulling layers of a given image [GH-7000]
|
||||
* provider/fastly: Add support for Cache Settings [GH-6781]
|
||||
* provider/fastly: Add support for Service Request Settings on `fastly_service_v1` resources [GH-6622]
|
||||
* provider/fastly: Add support for custom VCL configuration [GH-6662]
|
||||
|
@ -227,6 +238,7 @@ BUG FIXES:
|
|||
* provider/aws: Fix bug with Updating `aws_autoscaling_group` `enabled_metrics` [GH-7698]
|
||||
* provider/aws: Ignore IOPS on non io1 AWS root_block_device [GH-7783]
|
||||
* provider/aws: Ignore missing ENI attachment when trying to detach ENI [GH-7185]
|
||||
* provider/aws: Fix issue updating ElasticBeanstalk Environment templates [GH-7811]
|
||||
* provider/azurerm: Fixes terraform crash when using SSH keys with `azurerm_virtual_machine` [GH-6766]
|
||||
* provider/azurerm: Fix a bug causing 'diffs do not match' on `azurerm_network_interface` resources [GH-6790]
|
||||
* provider/azurerm: Normalizes `availability_set_id` casing to avoid spurious diffs in `azurerm_virtual_machine` [GH-6768]
|
||||
|
|
|
@ -311,7 +311,9 @@ func resourceAwsElasticBeanstalkEnvironmentUpdate(d *schema.ResourceData, meta i
|
|||
|
||||
if d.HasChange("solution_stack_name") {
|
||||
hasChange = true
|
||||
updateOpts.SolutionStackName = aws.String(d.Get("solution_stack_name").(string))
|
||||
if v, ok := d.GetOk("solution_stack_name"); ok {
|
||||
updateOpts.SolutionStackName = aws.String(v.(string))
|
||||
}
|
||||
}
|
||||
|
||||
if d.HasChange("setting") {
|
||||
|
@ -332,7 +334,9 @@ func resourceAwsElasticBeanstalkEnvironmentUpdate(d *schema.ResourceData, meta i
|
|||
|
||||
if d.HasChange("template_name") {
|
||||
hasChange = true
|
||||
updateOpts.TemplateName = aws.String(d.Get("template_name").(string))
|
||||
if v, ok := d.GetOk("template_name"); ok {
|
||||
updateOpts.TemplateName = aws.String(v.(string))
|
||||
}
|
||||
}
|
||||
|
||||
if hasChange {
|
||||
|
|
|
@ -178,6 +178,40 @@ func TestAccAWSBeanstalkEnv_vpc(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestAccAWSBeanstalkEnv_template_change(t *testing.T) {
|
||||
var app elasticbeanstalk.EnvironmentDescription
|
||||
|
||||
rInt := acctest.RandInt()
|
||||
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() {
|
||||
testAccPreCheck(t)
|
||||
},
|
||||
Providers: testAccProviders,
|
||||
CheckDestroy: testAccCheckBeanstalkEnvDestroy,
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAccBeanstalkEnv_TemplateChange_stack(rInt),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccCheckBeanstalkEnvExists("aws_elastic_beanstalk_environment.environment", &app),
|
||||
),
|
||||
},
|
||||
resource.TestStep{
|
||||
Config: testAccBeanstalkEnv_TemplateChange_temp(rInt),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccCheckBeanstalkEnvExists("aws_elastic_beanstalk_environment.environment", &app),
|
||||
),
|
||||
},
|
||||
resource.TestStep{
|
||||
Config: testAccBeanstalkEnv_TemplateChange_stack(rInt),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccCheckBeanstalkEnvExists("aws_elastic_beanstalk_environment.environment", &app),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccCheckBeanstalkEnvDestroy(s *terraform.State) error {
|
||||
conn := testAccProvider.Meta().(*AWSClient).elasticbeanstalkconn
|
||||
|
||||
|
@ -571,3 +605,63 @@ resource "aws_elastic_beanstalk_environment" "default" {
|
|||
}
|
||||
`, name)
|
||||
}
|
||||
|
||||
func testAccBeanstalkEnv_TemplateChange_stack(r int) string {
|
||||
return fmt.Sprintf(`
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
}
|
||||
|
||||
resource "aws_elastic_beanstalk_application" "app" {
|
||||
name = "beanstalk-app-%d"
|
||||
description = ""
|
||||
}
|
||||
|
||||
resource "aws_elastic_beanstalk_environment" "environment" {
|
||||
name = "beanstalk-env-%d"
|
||||
application = "${aws_elastic_beanstalk_application.app.name}"
|
||||
|
||||
# Go 1.4
|
||||
|
||||
solution_stack_name = "64bit Amazon Linux 2016.03 v2.1.0 running Go 1.4"
|
||||
}
|
||||
|
||||
resource "aws_elastic_beanstalk_configuration_template" "template" {
|
||||
name = "beanstalk-config-%d"
|
||||
application = "${aws_elastic_beanstalk_application.app.name}"
|
||||
|
||||
# Go 1.5
|
||||
solution_stack_name = "64bit Amazon Linux 2016.03 v2.1.3 running Go 1.5"
|
||||
}
|
||||
`, r, r, r)
|
||||
}
|
||||
|
||||
func testAccBeanstalkEnv_TemplateChange_temp(r int) string {
|
||||
return fmt.Sprintf(`
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
}
|
||||
|
||||
resource "aws_elastic_beanstalk_application" "app" {
|
||||
name = "beanstalk-app-%d"
|
||||
description = ""
|
||||
}
|
||||
|
||||
resource "aws_elastic_beanstalk_environment" "environment" {
|
||||
name = "beanstalk-env-%d"
|
||||
application = "${aws_elastic_beanstalk_application.app.name}"
|
||||
|
||||
# Go 1.4
|
||||
|
||||
template_name = "${aws_elastic_beanstalk_configuration_template.template.name}"
|
||||
}
|
||||
|
||||
resource "aws_elastic_beanstalk_configuration_template" "template" {
|
||||
name = "beanstalk-config-%d"
|
||||
application = "${aws_elastic_beanstalk_application.app.name}"
|
||||
|
||||
# Go 1.5
|
||||
solution_stack_name = "64bit Amazon Linux 2016.03 v2.1.3 running Go 1.5"
|
||||
}
|
||||
`, r, r, r)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
type Config struct {
|
||||
Datacenter string `mapstructure:"datacenter"`
|
||||
Address string `mapstructure:"address"`
|
||||
Token string `mapstructure:"token"`
|
||||
Scheme string `mapstructure:"scheme"`
|
||||
}
|
||||
|
||||
|
@ -25,6 +26,9 @@ func (c *Config) Client() (*consulapi.Client, error) {
|
|||
if c.Scheme != "" {
|
||||
config.Scheme = c.Scheme
|
||||
}
|
||||
if c.Token != "" {
|
||||
config.Token = c.Token
|
||||
}
|
||||
client, err := consulapi.NewClient(config)
|
||||
|
||||
log.Printf("[INFO] Consul Client configured with address: '%s', scheme: '%s', datacenter: '%s'",
|
||||
|
|
|
@ -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{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"name": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Deprecated: "Using consul_keys resource to *read* is deprecated; please use consul_keys data source instead",
|
||||
},
|
||||
|
||||
"path": &schema.Schema{
|
||||
|
@ -220,7 +221,12 @@ func resourceConsulKeysRead(d *schema.ResourceData, meta interface{}) error {
|
|||
}
|
||||
|
||||
value = attributeValue(sub, value)
|
||||
vars[key] = value
|
||||
if key != "" {
|
||||
// If key is set then we'll update vars, for backward-compatibilty
|
||||
// with the pre-0.7 capability to read from Consul with this
|
||||
// resource.
|
||||
vars[key] = value
|
||||
}
|
||||
|
||||
// If there is already a "value" attribute present for this key
|
||||
// then it was created as a "write" block. We need to update the
|
||||
|
@ -290,10 +296,7 @@ func parseKey(raw interface{}) (string, string, map[string]interface{}, error) {
|
|||
return "", "", nil, fmt.Errorf("Failed to unroll: %#v", raw)
|
||||
}
|
||||
|
||||
key, ok := sub["name"].(string)
|
||||
if !ok {
|
||||
return "", "", nil, fmt.Errorf("Failed to expand key '%#v'", sub)
|
||||
}
|
||||
key := sub["name"].(string)
|
||||
|
||||
path, ok := sub["path"].(string)
|
||||
if !ok {
|
||||
|
|
|
@ -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,
|
||||
Optional: true,
|
||||
},
|
||||
|
||||
"token": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
|
||||
DataSourcesMap: map[string]*schema.Resource{
|
||||
"consul_keys": dataSourceConsulKeys(),
|
||||
},
|
||||
|
||||
ResourcesMap: map[string]*schema.Resource{
|
||||
"consul_keys": resourceConsulKeys(),
|
||||
"consul_key_prefix": resourceConsulKeyPrefix(),
|
||||
"consul_agent_service": resourceConsulAgentService(),
|
||||
"consul_catalog_entry": resourceConsulCatalogEntry(),
|
||||
"consul_keys": resourceConsulKeys(),
|
||||
"consul_key_prefix": resourceConsulKeyPrefix(),
|
||||
"consul_node": resourceConsulNode(),
|
||||
"consul_service": resourceConsulService(),
|
||||
},
|
||||
|
||||
ConfigureFunc: providerConfigure,
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
|
||||
DataSourcesMap: map[string]*schema.Resource{
|
||||
"docker_registry_image": dataSourceDockerRegistryImage(),
|
||||
},
|
||||
|
||||
ConfigureFunc: providerConfigure,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,11 +17,6 @@ func resourceDockerImage() *schema.Resource {
|
|||
Required: true,
|
||||
},
|
||||
|
||||
"keep_updated": &schema.Schema{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
},
|
||||
|
||||
"latest": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
|
@ -31,6 +26,12 @@ func resourceDockerImage() *schema.Resource {
|
|||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
},
|
||||
|
||||
"pull_trigger": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,25 @@ func resourceDockerImageCreate(d *schema.ResourceData, meta interface{}) error {
|
|||
}
|
||||
|
||||
func resourceDockerImageRead(d *schema.ResourceData, meta interface{}) error {
|
||||
client := meta.(*dc.Client)
|
||||
var data Data
|
||||
if err := fetchLocalImages(&data, client); err != nil {
|
||||
return fmt.Errorf("Error reading docker image list: %s", err)
|
||||
}
|
||||
foundImage := searchLocalImages(data, d.Get("name").(string))
|
||||
|
||||
if foundImage != nil {
|
||||
d.Set("latest", foundImage.ID)
|
||||
} else {
|
||||
d.SetId("")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceDockerImageUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||
// We need to re-read in case switching parameters affects
|
||||
// the value of "latest" or others
|
||||
client := meta.(*dc.Client)
|
||||
apiImage, err := findImage(d, client)
|
||||
if err != nil {
|
||||
|
@ -33,13 +52,6 @@ func resourceDockerImageRead(d *schema.ResourceData, meta interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func resourceDockerImageUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||
// We need to re-read in case switching parameters affects
|
||||
// the value of "latest" or others
|
||||
|
||||
return resourceDockerImageRead(d, meta)
|
||||
}
|
||||
|
||||
func resourceDockerImageDelete(d *schema.ResourceData, meta interface{}) error {
|
||||
client := meta.(*dc.Client)
|
||||
err := removeImage(d, client)
|
||||
|
@ -117,6 +129,17 @@ func pullImage(data *Data, client *dc.Client, image string) error {
|
|||
// TODO: Test local registry handling. It should be working
|
||||
// based on the code that was ported over
|
||||
|
||||
pullOpts := parseImageOptions(image)
|
||||
auth := dc.AuthConfiguration{}
|
||||
|
||||
if err := client.PullImage(pullOpts, auth); err != nil {
|
||||
return fmt.Errorf("Error pulling image %s: %s\n", image, err)
|
||||
}
|
||||
|
||||
return fetchLocalImages(data, client)
|
||||
}
|
||||
|
||||
func parseImageOptions(image string) dc.PullImageOptions {
|
||||
pullOpts := dc.PullImageOptions{}
|
||||
|
||||
splitImageName := strings.Split(image, ":")
|
||||
|
@ -151,32 +174,7 @@ func pullImage(data *Data, client *dc.Client, image string) error {
|
|||
pullOpts.Repository = image
|
||||
}
|
||||
|
||||
if err := client.PullImage(pullOpts, dc.AuthConfiguration{}); err != nil {
|
||||
return fmt.Errorf("Error pulling image %s: %s\n", image, err)
|
||||
}
|
||||
|
||||
return fetchLocalImages(data, client)
|
||||
}
|
||||
|
||||
func getImageTag(image string) string {
|
||||
splitImageName := strings.Split(image, ":")
|
||||
switch {
|
||||
|
||||
// It's in registry:port/repo:tag format
|
||||
case len(splitImageName) == 3:
|
||||
return splitImageName[2]
|
||||
|
||||
// It's either registry:port/repo or repo:tag with default registry
|
||||
case len(splitImageName) == 2:
|
||||
splitPortRepo := strings.Split(splitImageName[1], "/")
|
||||
if len(splitPortRepo) == 2 {
|
||||
return ""
|
||||
} else {
|
||||
return splitImageName[1]
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
return pullOpts
|
||||
}
|
||||
|
||||
func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error) {
|
||||
|
@ -192,7 +190,7 @@ func findImage(d *schema.ResourceData, client *dc.Client) (*dc.APIImages, error)
|
|||
|
||||
foundImage := searchLocalImages(data, imageName)
|
||||
|
||||
if d.Get("keep_updated").(bool) || foundImage == nil {
|
||||
if foundImage == nil {
|
||||
if err := pullImage(&data, client, imageName); err != nil {
|
||||
return nil, fmt.Errorf("Unable to pull image %s: %s", imageName, err)
|
||||
}
|
||||
|
|
|
@ -73,6 +73,22 @@ func TestAccDockerImage_destroy(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestAccDockerImage_data(t *testing.T) {
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
PreventPostDestroyRefresh: true,
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAccDockerImageFromDataConfig,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestMatchResourceAttr("docker_image.foobarbaz", "latest", contentDigestRegexp),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccDockerImageDestroy(s *terraform.State) error {
|
||||
for _, rs := range s.RootModule().Resources {
|
||||
if rs.Type != "docker_image" {
|
||||
|
@ -93,14 +109,12 @@ func testAccDockerImageDestroy(s *terraform.State) error {
|
|||
const testAccDockerImageConfig = `
|
||||
resource "docker_image" "foo" {
|
||||
name = "alpine:3.1"
|
||||
keep_updated = false
|
||||
}
|
||||
`
|
||||
|
||||
const testAddDockerPrivateImageConfig = `
|
||||
resource "docker_image" "foobar" {
|
||||
name = "gcr.io:443/google_containers/pause:0.8.0"
|
||||
keep_updated = true
|
||||
}
|
||||
`
|
||||
|
||||
|
@ -110,3 +124,13 @@ resource "docker_image" "foobarzoo" {
|
|||
keep_locally = true
|
||||
}
|
||||
`
|
||||
|
||||
const testAccDockerImageFromDataConfig = `
|
||||
data "docker_registry_image" "foobarbaz" {
|
||||
name = "alpine:3.1"
|
||||
}
|
||||
resource "docker_image" "foobarbaz" {
|
||||
name = "${data.docker_registry_image.foobarbaz.name}"
|
||||
pull_trigger = "${data.docker_registry_image.foobarbaz.sha256_digest}"
|
||||
}
|
||||
`
|
||||
|
|
|
@ -50,6 +50,8 @@ func Provider() terraform.ResourceProvider {
|
|||
|
||||
ResourcesMap: map[string]*schema.Resource{
|
||||
"mysql_database": resourceDatabase(),
|
||||
"mysql_user": resourceUser(),
|
||||
"mysql_grant": resourceGrant(),
|
||||
},
|
||||
|
||||
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 (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
// FlagKV is a flag.Value implementation for parsing user variables
|
||||
// from the command-line in the format of '-var key=value'.
|
||||
type FlagKV map[string]string
|
||||
// FlagTypedKVis a flag.Value implementation for parsing user variables
|
||||
// from the command-line in the format of '-var key=value', where value is
|
||||
// a type intended for use as a Terraform variable
|
||||
type FlagTypedKV map[string]interface{}
|
||||
|
||||
func (v *FlagKV) String() string {
|
||||
func (v *FlagTypedKV) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (v *FlagKV) Set(raw string) error {
|
||||
func (v *FlagTypedKV) Set(raw string) error {
|
||||
key, value, err := parseVarFlagAsHCL(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if *v == nil {
|
||||
*v = make(map[string]interface{})
|
||||
}
|
||||
|
||||
(*v)[key] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
// FlagStringKV is a flag.Value implementation for parsing user variables
|
||||
// from the command-line in the format of '-var key=value', where value is
|
||||
// only ever a primitive.
|
||||
type FlagStringKV map[string]string
|
||||
|
||||
func (v *FlagStringKV) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (v *FlagStringKV) Set(raw string) error {
|
||||
idx := strings.Index(raw, "=")
|
||||
if idx == -1 {
|
||||
return fmt.Errorf("No '=' value in arg: %s", raw)
|
||||
|
@ -34,7 +59,7 @@ func (v *FlagKV) Set(raw string) error {
|
|||
|
||||
// FlagKVFile is a flag.Value implementation for parsing user variables
|
||||
// from the command line in the form of files. i.e. '-var-file=foo'
|
||||
type FlagKVFile map[string]string
|
||||
type FlagKVFile map[string]interface{}
|
||||
|
||||
func (v *FlagKVFile) String() string {
|
||||
return ""
|
||||
|
@ -47,7 +72,7 @@ func (v *FlagKVFile) Set(raw string) error {
|
|||
}
|
||||
|
||||
if *v == nil {
|
||||
*v = make(map[string]string)
|
||||
*v = make(map[string]interface{})
|
||||
}
|
||||
|
||||
for key, value := range vs {
|
||||
|
@ -57,7 +82,7 @@ func (v *FlagKVFile) Set(raw string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func loadKVFile(rawPath string) (map[string]string, error) {
|
||||
func loadKVFile(rawPath string) (map[string]interface{}, error) {
|
||||
path, err := homedir.Expand(rawPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
|
@ -78,7 +103,7 @@ func loadKVFile(rawPath string) (map[string]string, error) {
|
|||
"Error parsing %s: %s", path, err)
|
||||
}
|
||||
|
||||
var result map[string]string
|
||||
var result map[string]interface{}
|
||||
if err := hcl.DecodeObject(&result, obj); err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"Error decoding Terraform vars file: %s\n\n"+
|
||||
|
@ -103,3 +128,49 @@ func (v *FlagStringSlice) Set(raw string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseVarFlagAsHCL parses the value of a single variable as would have been specified
|
||||
// on the command line via -var or in an environment variable named TF_VAR_x, where x is
|
||||
// the name of the variable. In order to get around the restriction of HCL requiring a
|
||||
// top level object, we prepend a sentinel key, decode the user-specified value as its
|
||||
// value and pull the value back out of the resulting map.
|
||||
func parseVarFlagAsHCL(input string) (string, interface{}, error) {
|
||||
idx := strings.Index(input, "=")
|
||||
if idx == -1 {
|
||||
return "", nil, fmt.Errorf("No '=' value in variable: %s", input)
|
||||
}
|
||||
probablyName := input[0:idx]
|
||||
|
||||
parsed, err := hcl.Parse(input)
|
||||
if err != nil {
|
||||
// This covers flags of the form `foo=bar` which is not valid HCL
|
||||
// At this point, probablyName is actually the name, and the remainder
|
||||
// of the expression after the equals sign is the value.
|
||||
if regexp.MustCompile(`Unknown token: \d+:\d+ IDENT`).Match([]byte(err.Error())) {
|
||||
value := input[idx+1:]
|
||||
return probablyName, value, nil
|
||||
}
|
||||
return "", nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL: %s", probablyName, input, err)
|
||||
}
|
||||
|
||||
var decoded map[string]interface{}
|
||||
if hcl.DecodeObject(&decoded, parsed); err != nil {
|
||||
return "", nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL: %s", probablyName, input, err)
|
||||
}
|
||||
|
||||
// Cover cases such as key=
|
||||
if len(decoded) == 0 {
|
||||
return probablyName, "", nil
|
||||
}
|
||||
|
||||
if len(decoded) > 1 {
|
||||
return "", nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL. Only one value may be specified.", probablyName, input)
|
||||
}
|
||||
|
||||
for k, v := range decoded {
|
||||
return k, v, nil
|
||||
}
|
||||
|
||||
// Should be unreachable
|
||||
return "", nil, fmt.Errorf("No value for variable: %s", input)
|
||||
}
|
||||
|
|
|
@ -2,16 +2,17 @@ package command
|
|||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFlagKV_impl(t *testing.T) {
|
||||
var _ flag.Value = new(FlagKV)
|
||||
func TestFlagStringKV_impl(t *testing.T) {
|
||||
var _ flag.Value = new(FlagStringKV)
|
||||
}
|
||||
|
||||
func TestFlagKV(t *testing.T) {
|
||||
func TestFlagStringKV(t *testing.T) {
|
||||
cases := []struct {
|
||||
Input string
|
||||
Output map[string]string
|
||||
|
@ -49,10 +50,10 @@ func TestFlagKV(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
f := new(FlagKV)
|
||||
f := new(FlagStringKV)
|
||||
err := f.Set(tc.Input)
|
||||
if err != nil != tc.Error {
|
||||
t.Fatalf("bad error. Input: %#v", tc.Input)
|
||||
t.Fatalf("bad error. Input: %#v\n\nError: %s", tc.Input, err)
|
||||
}
|
||||
|
||||
actual := map[string]string(*f)
|
||||
|
@ -62,6 +63,86 @@ func TestFlagKV(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFlagTypedKV_impl(t *testing.T) {
|
||||
var _ flag.Value = new(FlagTypedKV)
|
||||
}
|
||||
|
||||
func TestFlagTypedKV(t *testing.T) {
|
||||
cases := []struct {
|
||||
Input string
|
||||
Output map[string]interface{}
|
||||
Error bool
|
||||
}{
|
||||
{
|
||||
"key=value",
|
||||
map[string]interface{}{"key": "value"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"key=",
|
||||
map[string]interface{}{"key": ""},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"key=foo=bar",
|
||||
map[string]interface{}{"key": "foo=bar"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"map.key=foo",
|
||||
map[string]interface{}{"map.key": "foo"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"key",
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
`key=["hello", "world"]`,
|
||||
map[string]interface{}{"key": []interface{}{"hello", "world"}},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
`key={"hello" = "world", "foo" = "bar"}`,
|
||||
map[string]interface{}{
|
||||
"key": []map[string]interface{}{
|
||||
map[string]interface{}{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
`key={"hello" = "world", "foo" = "bar"}\nkey2="invalid"`,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
f := new(FlagTypedKV)
|
||||
err := f.Set(tc.Input)
|
||||
if err != nil != tc.Error {
|
||||
t.Fatalf("bad error. Input: %#v\n\nError: %s", tc.Input, err)
|
||||
}
|
||||
|
||||
actual := map[string]interface{}(*f)
|
||||
if !reflect.DeepEqual(actual, tc.Output) {
|
||||
t.Fatalf("bad:\nexpected: %s\n\n got: %s\n", spew.Sdump(tc.Output), spew.Sdump(actual))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagKVFile_impl(t *testing.T) {
|
||||
var _ flag.Value = new(FlagKVFile)
|
||||
}
|
||||
|
@ -76,24 +157,24 @@ foo = "bar"
|
|||
|
||||
cases := []struct {
|
||||
Input string
|
||||
Output map[string]string
|
||||
Output map[string]interface{}
|
||||
Error bool
|
||||
}{
|
||||
{
|
||||
inputLibucl,
|
||||
map[string]string{"foo": "bar"},
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
inputJson,
|
||||
map[string]string{"foo": "bar"},
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
`map.key = "foo"`,
|
||||
map[string]string{"map.key": "foo"},
|
||||
map[string]interface{}{"map.key": "foo"},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
@ -111,7 +192,7 @@ foo = "bar"
|
|||
t.Fatalf("bad error. Input: %#v, err: %s", tc.Input, err)
|
||||
}
|
||||
|
||||
actual := map[string]string(*f)
|
||||
actual := map[string]interface{}(*f)
|
||||
if !reflect.DeepEqual(actual, tc.Output) {
|
||||
t.Fatalf("bad: %#v", actual)
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ func (c *InitCommand) Run(args []string) int {
|
|||
remoteConfig := make(map[string]string)
|
||||
cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError)
|
||||
cmdFlags.StringVar(&remoteBackend, "backend", "", "")
|
||||
cmdFlags.Var((*FlagKV)(&remoteConfig), "backend-config", "config")
|
||||
cmdFlags.Var((*FlagStringKV)(&remoteConfig), "backend-config", "config")
|
||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
return 1
|
||||
|
|
|
@ -36,9 +36,9 @@ type Meta struct {
|
|||
|
||||
// Variables for the context (private)
|
||||
autoKey string
|
||||
autoVariables map[string]string
|
||||
autoVariables map[string]interface{}
|
||||
input bool
|
||||
variables map[string]string
|
||||
variables map[string]interface{}
|
||||
|
||||
// Targets for this context (private)
|
||||
targets []string
|
||||
|
@ -315,7 +315,7 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
|
|||
func (m *Meta) flagSet(n string) *flag.FlagSet {
|
||||
f := flag.NewFlagSet(n, flag.ContinueOnError)
|
||||
f.BoolVar(&m.input, "input", true, "input")
|
||||
f.Var((*FlagKV)(&m.variables), "var", "variables")
|
||||
f.Var((*FlagTypedKV)(&m.variables), "var", "variables")
|
||||
f.Var((*FlagKVFile)(&m.variables), "var-file", "variable file")
|
||||
f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target")
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ func (c *RemoteConfigCommand) Run(args []string) int {
|
|||
cmdFlags.StringVar(&c.conf.statePath, "state", DefaultStateFilename, "path")
|
||||
cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path")
|
||||
cmdFlags.StringVar(&c.remoteConf.Type, "backend", "atlas", "")
|
||||
cmdFlags.Var((*FlagKV)(&config), "backend-config", "config")
|
||||
cmdFlags.Var((*FlagStringKV)(&config), "backend-config", "config")
|
||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("\nError parsing CLI flags: %s", err))
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"sync"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/hcl"
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
)
|
||||
|
@ -119,24 +120,109 @@ func NewContext(opts *ContextOpts) (*Context, error) {
|
|||
par = 10
|
||||
}
|
||||
|
||||
// Setup the variables. We first take the variables given to us.
|
||||
// We then merge in the variables set in the environment.
|
||||
// Set up the variables in the following sequence:
|
||||
// 0 - Take default values from the configuration
|
||||
// 1 - Take values from TF_VAR_x environment variables
|
||||
// 2 - Take values specified in -var flags, overriding values
|
||||
// set by environment variables if necessary. This includes
|
||||
// values taken from -var-file in addition.
|
||||
variables := make(map[string]interface{})
|
||||
for _, v := range os.Environ() {
|
||||
if !strings.HasPrefix(v, VarEnvPrefix) {
|
||||
continue
|
||||
|
||||
if opts.Module != nil {
|
||||
for _, v := range opts.Module.Config().Variables {
|
||||
if v.Default != nil {
|
||||
if v.Type() == config.VariableTypeString {
|
||||
// v.Default has already been parsed as HCL so there may be
|
||||
// some stray ints in there
|
||||
switch typedDefault := v.Default.(type) {
|
||||
case string:
|
||||
if typedDefault == "" {
|
||||
continue
|
||||
}
|
||||
variables[v.Name] = typedDefault
|
||||
case int, int64:
|
||||
variables[v.Name] = fmt.Sprintf("%d", typedDefault)
|
||||
case float32, float64:
|
||||
variables[v.Name] = fmt.Sprintf("%f", typedDefault)
|
||||
case bool:
|
||||
variables[v.Name] = fmt.Sprintf("%t", typedDefault)
|
||||
}
|
||||
} else {
|
||||
variables[v.Name] = v.Default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strip off the prefix and get the value after the first "="
|
||||
idx := strings.Index(v, "=")
|
||||
k := v[len(VarEnvPrefix):idx]
|
||||
v = v[idx+1:]
|
||||
for _, v := range os.Environ() {
|
||||
if !strings.HasPrefix(v, VarEnvPrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Override the command-line set variable
|
||||
variables[k] = v
|
||||
}
|
||||
for k, v := range opts.Variables {
|
||||
variables[k] = v
|
||||
// Strip off the prefix and get the value after the first "="
|
||||
idx := strings.Index(v, "=")
|
||||
k := v[len(VarEnvPrefix):idx]
|
||||
v = v[idx+1:]
|
||||
|
||||
// Override the configuration-default values. Note that *not* finding the variable
|
||||
// in configuration is OK, as we don't want to preclude people from having multiple
|
||||
// sets of TF_VAR_whatever in their environment even if it is a little weird.
|
||||
for _, schema := range opts.Module.Config().Variables {
|
||||
if schema.Name == k {
|
||||
varType := schema.Type()
|
||||
varVal, err := parseVariableAsHCL(k, v, varType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch varType {
|
||||
case config.VariableTypeMap:
|
||||
if existing, hasMap := variables[k]; !hasMap {
|
||||
variables[k] = varVal
|
||||
} else {
|
||||
if existingMap, ok := existing.(map[string]interface{}); !ok {
|
||||
panic(fmt.Sprintf("%s is not a map, this is a bug in Terraform.", k))
|
||||
} else {
|
||||
if newMap, ok := varVal.(map[string]interface{}); ok {
|
||||
for newKey, newVal := range newMap {
|
||||
existingMap[newKey] = newVal
|
||||
}
|
||||
} else {
|
||||
panic(fmt.Sprintf("%s is not a map, this is a bug in Terraform.", k))
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
variables[k] = varVal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range opts.Variables {
|
||||
for _, schema := range opts.Module.Config().Variables {
|
||||
if schema.Name == k {
|
||||
switch schema.Type() {
|
||||
case config.VariableTypeMap:
|
||||
if existing, hasMap := variables[k]; !hasMap {
|
||||
variables[k] = v
|
||||
} else {
|
||||
if existingMap, ok := existing.(map[string]interface{}); !ok {
|
||||
panic(fmt.Sprintf("%s is not a map, this is a bug in Terraform.", k))
|
||||
} else {
|
||||
if newMap, ok := v.([]map[string]interface{}); ok {
|
||||
for newKey, newVal := range newMap[0] {
|
||||
existingMap[newKey] = newVal
|
||||
}
|
||||
} else {
|
||||
panic(fmt.Sprintf("%s is not a map, this is a bug in Terraform.", k))
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
variables[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Context{
|
||||
|
@ -548,3 +634,45 @@ func (c *Context) walk(
|
|||
walker := &ContextGraphWalker{Context: c, Operation: operation}
|
||||
return walker, graph.Walk(walker)
|
||||
}
|
||||
|
||||
// parseVariableAsHCL parses the value of a single variable as would have been specified
|
||||
// on the command line via -var or in an environment variable named TF_VAR_x, where x is
|
||||
// the name of the variable. In order to get around the restriction of HCL requiring a
|
||||
// top level object, we prepend a sentinel key, decode the user-specified value as its
|
||||
// value and pull the value back out of the resulting map.
|
||||
func parseVariableAsHCL(name string, input interface{}, targetType config.VariableType) (interface{}, error) {
|
||||
if targetType == config.VariableTypeString {
|
||||
return input, nil
|
||||
}
|
||||
|
||||
const sentinelValue = "SENTINEL_TERRAFORM_VAR_OVERRIDE_KEY"
|
||||
inputWithSentinal := fmt.Sprintf("%s = %s", sentinelValue, input)
|
||||
|
||||
var decoded map[string]interface{}
|
||||
err := hcl.Decode(&decoded, inputWithSentinal)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL: %s", name, input, err)
|
||||
}
|
||||
|
||||
if len(decoded) != 1 {
|
||||
return nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL. Only one value may be specified.", name, input)
|
||||
}
|
||||
|
||||
parsedValue, ok := decoded[sentinelValue]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL. One value must be specified.", name, input)
|
||||
}
|
||||
|
||||
switch targetType {
|
||||
case config.VariableTypeList:
|
||||
return parsedValue, nil
|
||||
case config.VariableTypeMap:
|
||||
if list, ok := parsedValue.([]map[string]interface{}); ok {
|
||||
return list[0], nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL. One value must be specified.", name, input)
|
||||
default:
|
||||
panic(fmt.Errorf("unknown type %s", targetType))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1135,7 +1135,11 @@ func TestContext2Apply_mapVariableOverride(t *testing.T) {
|
|||
"aws": testProviderFuncFixed(p),
|
||||
},
|
||||
Variables: map[string]interface{}{
|
||||
"images.us-west-2": "overridden",
|
||||
"images": []map[string]interface{}{
|
||||
map[string]interface{}{
|
||||
"us-west-2": "overridden",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -4269,8 +4273,18 @@ func TestContext2Apply_vars(t *testing.T) {
|
|||
"aws": testProviderFuncFixed(p),
|
||||
},
|
||||
Variables: map[string]interface{}{
|
||||
"foo": "us-west-2",
|
||||
"amis.us-east-1": "override",
|
||||
"foo": "us-west-2",
|
||||
"test_list": []interface{}{"Hello", "World"},
|
||||
"test_map": map[string]interface{}{
|
||||
"Hello": "World",
|
||||
"Foo": "Bar",
|
||||
"Baz": "Foo",
|
||||
},
|
||||
"amis": []map[string]interface{}{
|
||||
map[string]interface{}{
|
||||
"us-east-1": "override",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -4300,8 +4314,13 @@ func TestContext2Apply_vars(t *testing.T) {
|
|||
|
||||
func TestContext2Apply_varsEnv(t *testing.T) {
|
||||
// Set the env var
|
||||
old := tempEnv(t, "TF_VAR_ami", "baz")
|
||||
defer os.Setenv("TF_VAR_ami", old)
|
||||
old_ami := tempEnv(t, "TF_VAR_ami", "baz")
|
||||
old_list := tempEnv(t, "TF_VAR_list", `["Hello", "World"]`)
|
||||
old_map := tempEnv(t, "TF_VAR_map", `{"Hello" = "World", "Foo" = "Bar", "Baz" = "Foo"}`)
|
||||
|
||||
defer os.Setenv("TF_VAR_ami", old_ami)
|
||||
defer os.Setenv("TF_VAR_list", old_list)
|
||||
defer os.Setenv("TF_VAR_list", old_map)
|
||||
|
||||
m := testModule(t, "apply-vars-env")
|
||||
p := testProvider("aws")
|
||||
|
|
|
@ -19,8 +19,12 @@ func TestContext2Input(t *testing.T) {
|
|||
"aws": testProviderFuncFixed(p),
|
||||
},
|
||||
Variables: map[string]interface{}{
|
||||
"foo": "us-west-2",
|
||||
"amis.us-east-1": "override",
|
||||
"foo": "us-west-2",
|
||||
"amis": []map[string]interface{}{
|
||||
map[string]interface{}{
|
||||
"us-east-1": "override",
|
||||
},
|
||||
},
|
||||
},
|
||||
UIInput: input,
|
||||
})
|
||||
|
|
|
@ -95,16 +95,42 @@ func smcUserVariables(c *config.Config, vs map[string]interface{}) []error {
|
|||
}
|
||||
|
||||
// Check that types match up
|
||||
for k, _ := range vs {
|
||||
v, ok := cvs[k]
|
||||
for name, proposedValue := range vs {
|
||||
schema, ok := cvs[name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if v.Type() != config.VariableTypeString {
|
||||
errs = append(errs, fmt.Errorf(
|
||||
"%s: cannot assign string value to map type",
|
||||
k))
|
||||
declaredType := schema.Type()
|
||||
|
||||
switch declaredType {
|
||||
case config.VariableTypeString:
|
||||
switch proposedValue.(type) {
|
||||
case string:
|
||||
continue
|
||||
default:
|
||||
errs = append(errs, fmt.Errorf("variable %s should be type %s, got %s",
|
||||
name, declaredType.Printable(), hclTypeName(proposedValue)))
|
||||
}
|
||||
case config.VariableTypeMap:
|
||||
switch proposedValue.(type) {
|
||||
case map[string]interface{}:
|
||||
continue
|
||||
default:
|
||||
errs = append(errs, fmt.Errorf("variable %s should be type %s, got %s",
|
||||
name, declaredType.Printable(), hclTypeName(proposedValue)))
|
||||
}
|
||||
case config.VariableTypeList:
|
||||
switch proposedValue.(type) {
|
||||
case []interface{}:
|
||||
continue
|
||||
default:
|
||||
errs = append(errs, fmt.Errorf("variable %s should be type %s, got %s",
|
||||
name, declaredType.Printable(), hclTypeName(proposedValue)))
|
||||
}
|
||||
default:
|
||||
errs = append(errs, fmt.Errorf("variable %s should be type %s, got %s",
|
||||
name, declaredType.Printable(), hclTypeName(proposedValue)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -705,6 +705,8 @@ aws_instance.bar:
|
|||
aws_instance.foo:
|
||||
ID = foo
|
||||
bar = baz
|
||||
list = Hello,World
|
||||
map = Baz,Foo,Hello
|
||||
num = 2
|
||||
type = aws_instance
|
||||
`
|
||||
|
@ -712,6 +714,8 @@ aws_instance.foo:
|
|||
const testTerraformApplyVarsEnvStr = `
|
||||
aws_instance.bar:
|
||||
ID = foo
|
||||
bar = Hello,World
|
||||
baz = Baz,Foo,Hello
|
||||
foo = baz
|
||||
type = aws_instance
|
||||
`
|
||||
|
|
|
@ -1,7 +1,20 @@
|
|||
variable "ami" {
|
||||
default = "foo"
|
||||
default = "foo"
|
||||
type = "string"
|
||||
}
|
||||
|
||||
variable "list" {
|
||||
default = []
|
||||
type = "list"
|
||||
}
|
||||
|
||||
variable "map" {
|
||||
default = {}
|
||||
type = "map"
|
||||
}
|
||||
|
||||
resource "aws_instance" "bar" {
|
||||
foo = "${var.ami}"
|
||||
foo = "${var.ami}"
|
||||
bar = "${join(",", var.list)}"
|
||||
baz = "${join(",", keys(var.map))}"
|
||||
}
|
||||
|
|
|
@ -5,6 +5,14 @@ variable "amis" {
|
|||
}
|
||||
}
|
||||
|
||||
variable "test_list" {
|
||||
type = "list"
|
||||
}
|
||||
|
||||
variable "test_map" {
|
||||
type = "map"
|
||||
}
|
||||
|
||||
variable "bar" {
|
||||
default = "baz"
|
||||
}
|
||||
|
@ -14,6 +22,8 @@ variable "foo" {}
|
|||
resource "aws_instance" "foo" {
|
||||
num = "2"
|
||||
bar = "${var.bar}"
|
||||
list = "${join(",", var.test_list)}"
|
||||
map = "${join(",", keys(var.test_map))}"
|
||||
}
|
||||
|
||||
resource "aws_instance" "bar" {
|
||||
|
|
|
@ -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"
|
||||
sidebar_current: "docs-consul-resource-keys"
|
||||
description: |-
|
||||
Provides access to Key/Value data in Consul. This can be used to both read keys from Consul, but also to set the value of keys in Consul. This is a powerful way dynamically set values in templates, and to expose infrastructure details to clients.
|
||||
Writes values into the Consul key/value store.
|
||||
---
|
||||
|
||||
# consul\_keys
|
||||
|
||||
Provides access to Key/Value data in Consul. This can be used
|
||||
to both read keys from Consul, but also to set the value of keys
|
||||
in Consul. This is a powerful way dynamically set values in templates,
|
||||
and to expose infrastructure details to clients.
|
||||
`consul_keys` writes sets of individual values into Consul.
|
||||
This is a powerful way to expose infrastructure details to clients.
|
||||
|
||||
This resource manages individual keys, and thus it can create, update and
|
||||
delete the keys explicitly given. Howver, It is not able to detect and remove
|
||||
|
@ -27,13 +25,6 @@ resource "consul_keys" "app" {
|
|||
datacenter = "nyc1"
|
||||
token = "abcd"
|
||||
|
||||
# Read the launch AMI from Consul
|
||||
key {
|
||||
name = "ami"
|
||||
path = "service/app/launch_ami"
|
||||
default = "ami-1234"
|
||||
}
|
||||
|
||||
# Set the CNAME of our load balancer as a key
|
||||
key {
|
||||
name = "elb_cname"
|
||||
|
@ -41,12 +32,6 @@ resource "consul_keys" "app" {
|
|||
value = "${aws_elb.app.dns_name}"
|
||||
}
|
||||
}
|
||||
|
||||
# Start our instance with the dynamic ami value
|
||||
resource "aws_instance" "app" {
|
||||
ami = "${consul_keys.app.var.ami}"
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Argument Reference
|
||||
|
@ -59,33 +44,31 @@ The following arguments are supported:
|
|||
* `token` - (Optional) The ACL token to use. This overrides the
|
||||
token that the agent provides by default.
|
||||
|
||||
* `key` - (Required) Specifies a key in Consul to be read or written.
|
||||
* `key` - (Required) Specifies a key in Consul to be written.
|
||||
Supported values documented below.
|
||||
|
||||
The `key` block supports the following:
|
||||
|
||||
* `name` - (Required) This is the name of the key. This value of the
|
||||
key is exposed as `var.<name>`. This is not the path of the key
|
||||
in Consul.
|
||||
* `path` - (Required) This is the path in Consul that should be written to.
|
||||
|
||||
* `path` - (Required) This is the path in Consul that should be read
|
||||
or written to.
|
||||
|
||||
* `default` - (Optional) This is the default value to set for `var.<name>`
|
||||
if the key does not exist in Consul. Defaults to the empty string.
|
||||
|
||||
* `value` - (Optional) If set, the key will be set to this value.
|
||||
This allows a key to be written to.
|
||||
* `value` - (Required) The value to write to the given path.
|
||||
|
||||
* `delete` - (Optional) If true, then the key will be deleted when
|
||||
either its configuration block is removed from the configuration or
|
||||
the entire resource is destroyed. Otherwise, it will be left in Consul.
|
||||
Defaults to false.
|
||||
|
||||
### Deprecated `key` arguments
|
||||
|
||||
Prior to Terraform 0.7 this resource was used both to read *and* write the
|
||||
Consul key/value store. The read functionality has moved to the `consul_keys`
|
||||
*data source*, whose documentation can be found via the navigation.
|
||||
|
||||
The pre-0.7 interface for reading is still supported for backward compatibilty,
|
||||
but will be removed in a future version of Terraform.
|
||||
|
||||
## Attributes Reference
|
||||
|
||||
The following attributes are exported:
|
||||
|
||||
* `datacenter` - The datacenter the keys are being read/written to.
|
||||
* `var.<name>` - For each name given, the corresponding attribute
|
||||
has the value of the key.
|
||||
* `datacenter` - The datacenter the keys are being written to.
|
||||
|
|
|
@ -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
|
||||
|
||||
The following arguments are supported:
|
||||
|
|
|
@ -3,15 +3,18 @@ layout: "docker"
|
|||
page_title: "Docker: docker_image"
|
||||
sidebar_current: "docs-docker-resource-image"
|
||||
description: |-
|
||||
Downloads and exports the ID of a Docker image.
|
||||
Pulls a Docker image to a given Docker host.
|
||||
---
|
||||
|
||||
# docker\_image
|
||||
|
||||
Downloads and exports the ID of a Docker image. This can be used alongside
|
||||
[docker\_container](/docs/providers/docker/r/container.html)
|
||||
to programmatically get the latest image IDs without having to hardcode
|
||||
them.
|
||||
-> **Note:** The initial (current) version of this resource can only pull **public** images **from the official Docker Hub Registry**.
|
||||
|
||||
Pulls a Docker image to a given Docker host from a Docker Registry.
|
||||
|
||||
This resource will *not* pull new layers of the image automatically unless used in
|
||||
conjuction with [`docker_registry_image`](/docs/providers/docker/d/registry_image.html)
|
||||
data source to update the `pull_trigger` field.
|
||||
|
||||
## Example Usage
|
||||
|
||||
|
@ -24,18 +27,31 @@ resource "docker_image" "ubuntu" {
|
|||
# Access it somewhere else with ${docker_image.ubuntu.latest}
|
||||
```
|
||||
|
||||
### Dynamic image
|
||||
|
||||
```
|
||||
data "docker_registry_image" "ubuntu" {
|
||||
name = "ubuntu:precise"
|
||||
}
|
||||
|
||||
resource "docker_image" "ubuntu" {
|
||||
name = "${data.docker_registry_image.ubuntu.name}"
|
||||
pull_trigger = "${data.docker_registry_image.ubuntu.sha256_digest}"
|
||||
}
|
||||
```
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
* `name` - (Required, string) The name of the Docker image, including any tags.
|
||||
* `keep_updated` - (Optional, boolean) If true, then the Docker image will
|
||||
always be updated on the host to the latest. If this is false, as long as an
|
||||
image is downloaded with the correct tag, it won't be redownloaded if
|
||||
there is a newer image.
|
||||
* `keep_locally` - (Optional, boolean) If true, then the Docker image won't be
|
||||
deleted on destroy operation. If this is false, it will delete the image from
|
||||
the docker local storage on destroy operation.
|
||||
* `pull_trigger` - (Optional, string) Used to store the image digest from the
|
||||
registry and will cause an image pull when changed. Needed when using
|
||||
the `docker_registry_image` [data source](/docs/providers/docker/d/registry_image.html)
|
||||
to trigger an update of the image.
|
||||
|
||||
## Attributes Reference
|
||||
|
||||
|
|
|
@ -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>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current(/^docs-consul-data-source/) %>>
|
||||
<a href="#">Data Sources</a>
|
||||
<ul class="nav nav-visible">
|
||||
<li<%= sidebar_current("docs-consul-data-source-keys") %>>
|
||||
<a href="/docs/providers/consul/d/keys.html">consul_keys</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current(/^docs-consul-resource/) %>>
|
||||
<a href="#">Resources</a>
|
||||
<ul class="nav nav-visible">
|
||||
<li<%= sidebar_current("docs-consul-resource-agent-service") %>>
|
||||
<a href="/docs/providers/consul/r/agent_service.html">consul_agent_service</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-consul-resource-catalog-entry") %>>
|
||||
<a href="/docs/providers/consul/r/catalog_entry.html">consul_catalog_entry</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-consul-resource-keys") %>>
|
||||
<a href="/docs/providers/consul/r/keys.html">consul_keys</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-consul-resource-key-prefix") %>>
|
||||
<a href="/docs/providers/consul/r/key_prefix.html">consul_key_prefix</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-consul-resource-node") %>>
|
||||
<a href="/docs/providers/consul/r/node.html">consul_node</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-consul-resource-service") %>>
|
||||
<a href="/docs/providers/consul/r/service.html">consul_service</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -10,6 +10,15 @@
|
|||
<a href="/docs/providers/docker/index.html">Docker Provider</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current(/^docs-docker-datasource/) %>>
|
||||
<a href="#">Data Sources</a>
|
||||
<ul class="nav nav-visible">
|
||||
<li<%= sidebar_current("docs-docker-datasource-registry-image") %>>
|
||||
<a href="/docs/providers/docker/d/registry_image.html">docker_registry_image</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current(/^docs-docker-resource/) %>>
|
||||
<a href="#">Resources</a>
|
||||
<ul class="nav nav-visible">
|
||||
|
|
|
@ -16,6 +16,12 @@
|
|||
<li<%= sidebar_current("docs-mysql-resource-database") %>>
|
||||
<a href="/docs/providers/mysql/r/database.html">mysql_database</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-mysql-resource-grant") %>>
|
||||
<a href="/docs/providers/mysql/r/grant.html">mysql_grant</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-mysql-resource-user") %>>
|
||||
<a href="/docs/providers/mysql/r/user.html">mysql_user</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
Loading…
Reference in New Issue