diff --git a/CHANGELOG.md b/CHANGELOG.md index 00f2bd953..826b6ccf1 100644 --- a/CHANGELOG.md +++ b/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] diff --git a/builtin/providers/aws/resource_aws_elastic_beanstalk_environment.go b/builtin/providers/aws/resource_aws_elastic_beanstalk_environment.go index b94494778..cf1748733 100644 --- a/builtin/providers/aws/resource_aws_elastic_beanstalk_environment.go +++ b/builtin/providers/aws/resource_aws_elastic_beanstalk_environment.go @@ -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 { diff --git a/builtin/providers/aws/resource_aws_elastic_beanstalk_environment_test.go b/builtin/providers/aws/resource_aws_elastic_beanstalk_environment_test.go index 4d9481791..ab4744351 100644 --- a/builtin/providers/aws/resource_aws_elastic_beanstalk_environment_test.go +++ b/builtin/providers/aws/resource_aws_elastic_beanstalk_environment_test.go @@ -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) +} diff --git a/builtin/providers/consul/config.go b/builtin/providers/consul/config.go index 7983018c6..cb6d7af79 100644 --- a/builtin/providers/consul/config.go +++ b/builtin/providers/consul/config.go @@ -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'", diff --git a/builtin/providers/consul/data_source_consul_keys.go b/builtin/providers/consul/data_source_consul_keys.go new file mode 100644 index 000000000..af3e62771 --- /dev/null +++ b/builtin/providers/consul/data_source_consul_keys.go @@ -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 +} diff --git a/builtin/providers/consul/data_source_consul_keys_test.go b/builtin/providers/consul/data_source_consul_keys_test.go new file mode 100644 index 000000000..09f62a927 --- /dev/null +++ b/builtin/providers/consul/data_source_consul_keys_test.go @@ -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" + } +} +` diff --git a/builtin/providers/consul/resource_consul_agent_service.go b/builtin/providers/consul/resource_consul_agent_service.go new file mode 100644 index 000000000..6636060a8 --- /dev/null +++ b/builtin/providers/consul/resource_consul_agent_service.go @@ -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 +} diff --git a/builtin/providers/consul/resource_consul_agent_service_test.go b/builtin/providers/consul/resource_consul_agent_service_test.go new file mode 100644 index 000000000..5150c4e85 --- /dev/null +++ b/builtin/providers/consul/resource_consul_agent_service_test.go @@ -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 != "" && out != val { + return fmt.Errorf("Attribute '%s' value '%s' != '%s'", attr, out, val) + } + if val == "" && 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"] +} +` diff --git a/builtin/providers/consul/resource_consul_catalog_entry.go b/builtin/providers/consul/resource_consul_catalog_entry.go new file mode 100644 index 000000000..cf7d9acd9 --- /dev/null +++ b/builtin/providers/consul/resource_consul_catalog_entry.go @@ -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 +} diff --git a/builtin/providers/consul/resource_consul_catalog_entry_test.go b/builtin/providers/consul/resource_consul_catalog_entry_test.go new file mode 100644 index 000000000..0a28b675c --- /dev/null +++ b/builtin/providers/consul/resource_consul_catalog_entry_test.go @@ -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 != "" && out != val { + return fmt.Errorf("Attribute '%s' value '%s' != '%s'", attr, out, val) + } + if val == "" && 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"] + } +} +` diff --git a/builtin/providers/consul/resource_consul_keys.go b/builtin/providers/consul/resource_consul_keys.go index fe1db4ec1..bc91e6e7f 100644 --- a/builtin/providers/consul/resource_consul_keys.go +++ b/builtin/providers/consul/resource_consul_keys.go @@ -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 { diff --git a/builtin/providers/consul/resource_consul_node.go b/builtin/providers/consul/resource_consul_node.go new file mode 100644 index 000000000..c81544ccb --- /dev/null +++ b/builtin/providers/consul/resource_consul_node.go @@ -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 +} diff --git a/builtin/providers/consul/resource_consul_node_test.go b/builtin/providers/consul/resource_consul_node_test.go new file mode 100644 index 000000000..9cb62a4f6 --- /dev/null +++ b/builtin/providers/consul/resource_consul_node_test.go @@ -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 != "" && out != val { + return fmt.Errorf("Attribute '%s' value '%s' != '%s'", attr, out, val) + } + if val == "" && 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" +} +` diff --git a/builtin/providers/consul/resource_consul_service.go b/builtin/providers/consul/resource_consul_service.go new file mode 100644 index 000000000..57f95a856 --- /dev/null +++ b/builtin/providers/consul/resource_consul_service.go @@ -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 +} diff --git a/builtin/providers/consul/resource_consul_service_test.go b/builtin/providers/consul/resource_consul_service_test.go new file mode 100644 index 000000000..f4df71542 --- /dev/null +++ b/builtin/providers/consul/resource_consul_service_test.go @@ -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 != "" && out != val { + return fmt.Errorf("Attribute '%s' value '%s' != '%s'", attr, out, val) + } + if val == "" && 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"] +} +` diff --git a/builtin/providers/consul/resource_provider.go b/builtin/providers/consul/resource_provider.go index d2f8d269a..9b15ecdab 100644 --- a/builtin/providers/consul/resource_provider.go +++ b/builtin/providers/consul/resource_provider.go @@ -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, diff --git a/builtin/providers/docker/data_source_docker_registry_image.go b/builtin/providers/docker/data_source_docker_registry_image.go new file mode 100644 index 000000000..9898c8ac8 --- /dev/null +++ b/builtin/providers/docker/data_source_docker_registry_image.go @@ -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 +} diff --git a/builtin/providers/docker/data_source_docker_registry_image_test.go b/builtin/providers/docker/data_source_docker_registry_image_test.go new file mode 100644 index 000000000..aa34b004b --- /dev/null +++ b/builtin/providers/docker/data_source_docker_registry_image_test.go @@ -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" +} +` diff --git a/builtin/providers/docker/provider.go b/builtin/providers/docker/provider.go index cc25e1430..cee438ae3 100644 --- a/builtin/providers/docker/provider.go +++ b/builtin/providers/docker/provider.go @@ -32,6 +32,10 @@ func Provider() terraform.ResourceProvider { "docker_volume": resourceDockerVolume(), }, + DataSourcesMap: map[string]*schema.Resource{ + "docker_registry_image": dataSourceDockerRegistryImage(), + }, + ConfigureFunc: providerConfigure, } } diff --git a/builtin/providers/docker/resource_docker_image.go b/builtin/providers/docker/resource_docker_image.go index 09b6d32b8..9c2f84d48 100644 --- a/builtin/providers/docker/resource_docker_image.go +++ b/builtin/providers/docker/resource_docker_image.go @@ -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, + }, }, } } diff --git a/builtin/providers/docker/resource_docker_image_funcs.go b/builtin/providers/docker/resource_docker_image_funcs.go index 03d287c0b..72cbc8ea2 100644 --- a/builtin/providers/docker/resource_docker_image_funcs.go +++ b/builtin/providers/docker/resource_docker_image_funcs.go @@ -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) } diff --git a/builtin/providers/docker/resource_docker_image_test.go b/builtin/providers/docker/resource_docker_image_test.go index adf35fd9f..484c45e83 100644 --- a/builtin/providers/docker/resource_docker_image_test.go +++ b/builtin/providers/docker/resource_docker_image_test.go @@ -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}" +} +` diff --git a/builtin/providers/mysql/provider.go b/builtin/providers/mysql/provider.go index bd3401979..714e98d9f 100644 --- a/builtin/providers/mysql/provider.go +++ b/builtin/providers/mysql/provider.go @@ -50,6 +50,8 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "mysql_database": resourceDatabase(), + "mysql_user": resourceUser(), + "mysql_grant": resourceGrant(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/mysql/resource_grant.go b/builtin/providers/mysql/resource_grant.go new file mode 100644 index 000000000..332585011 --- /dev/null +++ b/builtin/providers/mysql/resource_grant.go @@ -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 +} diff --git a/builtin/providers/mysql/resource_grant_test.go b/builtin/providers/mysql/resource_grant_test.go new file mode 100644 index 000000000..d7541aee0 --- /dev/null +++ b/builtin/providers/mysql/resource_grant_test.go @@ -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"] +} +` diff --git a/builtin/providers/mysql/resource_user.go b/builtin/providers/mysql/resource_user.go new file mode 100644 index 000000000..c0969b7f3 --- /dev/null +++ b/builtin/providers/mysql/resource_user.go @@ -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 +} diff --git a/builtin/providers/mysql/resource_user_test.go b/builtin/providers/mysql/resource_user_test.go new file mode 100644 index 000000000..0d5b47cfb --- /dev/null +++ b/builtin/providers/mysql/resource_user_test.go @@ -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" +} +` diff --git a/command/flag_kv.go b/command/flag_kv.go index 492cc66be..b39aa1cd8 100644 --- a/command/flag_kv.go +++ b/command/flag_kv.go @@ -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) +} diff --git a/command/flag_kv_test.go b/command/flag_kv_test.go index 07b70d022..000915e61 100644 --- a/command/flag_kv_test.go +++ b/command/flag_kv_test.go @@ -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) } diff --git a/command/init.go b/command/init.go index bcc339bee..c4026d48d 100644 --- a/command/init.go +++ b/command/init.go @@ -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 diff --git a/command/meta.go b/command/meta.go index 69f3e3e5c..5adf1b667 100644 --- a/command/meta.go +++ b/command/meta.go @@ -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") diff --git a/command/remote_config.go b/command/remote_config.go index 6f53d0dbb..7f4a00b81 100644 --- a/command/remote_config.go +++ b/command/remote_config.go @@ -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)) diff --git a/terraform/context.go b/terraform/context.go index f653aba5e..86d7e58ce 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -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)) + } +} diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index cc499a482..ce64ddfca 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -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") diff --git a/terraform/context_input_test.go b/terraform/context_input_test.go index 9791b06fb..13e372469 100644 --- a/terraform/context_input_test.go +++ b/terraform/context_input_test.go @@ -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, }) diff --git a/terraform/semantics.go b/terraform/semantics.go index 6d001226a..e8e52b7aa 100644 --- a/terraform/semantics.go +++ b/terraform/semantics.go @@ -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))) } } diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 6be32ea03..fbcf6c61e 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -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 ` diff --git a/terraform/test-fixtures/apply-vars-env/main.tf b/terraform/test-fixtures/apply-vars-env/main.tf index b686da065..245564323 100644 --- a/terraform/test-fixtures/apply-vars-env/main.tf +++ b/terraform/test-fixtures/apply-vars-env/main.tf @@ -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))}" } diff --git a/terraform/test-fixtures/apply-vars/main.tf b/terraform/test-fixtures/apply-vars/main.tf index 7cd4b5316..7c426b227 100644 --- a/terraform/test-fixtures/apply-vars/main.tf +++ b/terraform/test-fixtures/apply-vars/main.tf @@ -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" { diff --git a/website/source/docs/providers/consul/d/keys.html.markdown b/website/source/docs/providers/consul/d/keys.html.markdown new file mode 100644 index 000000000..1e8439375 --- /dev/null +++ b/website/source/docs/providers/consul/d/keys.html.markdown @@ -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.`. 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.` + 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.` - For each name given, the corresponding attribute + has the value of the key. diff --git a/website/source/docs/providers/consul/r/agent_service.html.markdown b/website/source/docs/providers/consul/r/agent_service.html.markdown new file mode 100644 index 000000000..edf7524dd --- /dev/null +++ b/website/source/docs/providers/consul/r/agent_service.html.markdown @@ -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. diff --git a/website/source/docs/providers/consul/r/catalog_entry.html.markdown b/website/source/docs/providers/consul/r/catalog_entry.html.markdown new file mode 100644 index 000000000..354301a33 --- /dev/null +++ b/website/source/docs/providers/consul/r/catalog_entry.html.markdown @@ -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`. diff --git a/website/source/docs/providers/consul/r/keys.html.markdown b/website/source/docs/providers/consul/r/keys.html.markdown index 3fcd66ac5..42a74ddaf 100644 --- a/website/source/docs/providers/consul/r/keys.html.markdown +++ b/website/source/docs/providers/consul/r/keys.html.markdown @@ -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.`. 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.` - 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.` - For each name given, the corresponding attribute - has the value of the key. +* `datacenter` - The datacenter the keys are being written to. diff --git a/website/source/docs/providers/consul/r/node.html.markdown b/website/source/docs/providers/consul/r/node.html.markdown new file mode 100644 index 000000000..d8cc322bb --- /dev/null +++ b/website/source/docs/providers/consul/r/node.html.markdown @@ -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. diff --git a/website/source/docs/providers/consul/r/service.html.markdown b/website/source/docs/providers/consul/r/service.html.markdown new file mode 100644 index 000000000..a91370c2c --- /dev/null +++ b/website/source/docs/providers/consul/r/service.html.markdown @@ -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. diff --git a/website/source/docs/providers/docker/d/registry_image.html.markdown b/website/source/docs/providers/docker/d/registry_image.html.markdown new file mode 100644 index 000000000..8032e8b8b --- /dev/null +++ b/website/source/docs/providers/docker/d/registry_image.html.markdown @@ -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. diff --git a/website/source/docs/providers/docker/index.html.markdown b/website/source/docs/providers/docker/index.html.markdown index 9d057fd34..7af82efe3 100644 --- a/website/source/docs/providers/docker/index.html.markdown +++ b/website/source/docs/providers/docker/index.html.markdown @@ -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: diff --git a/website/source/docs/providers/docker/r/image.html.markdown b/website/source/docs/providers/docker/r/image.html.markdown index d6c0dcf56..b2c44369b 100644 --- a/website/source/docs/providers/docker/r/image.html.markdown +++ b/website/source/docs/providers/docker/r/image.html.markdown @@ -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 diff --git a/website/source/docs/providers/mysql/r/grant.html.markdown b/website/source/docs/providers/mysql/r/grant.html.markdown new file mode 100644 index 000000000..43e2fa07c --- /dev/null +++ b/website/source/docs/providers/mysql/r/grant.html.markdown @@ -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. diff --git a/website/source/docs/providers/mysql/r/user.html.markdown b/website/source/docs/providers/mysql/r/user.html.markdown new file mode 100644 index 000000000..f70e274a3 --- /dev/null +++ b/website/source/docs/providers/mysql/r/user.html.markdown @@ -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. diff --git a/website/source/layouts/consul.erb b/website/source/layouts/consul.erb index d4acef8fc..0af3d742a 100644 --- a/website/source/layouts/consul.erb +++ b/website/source/layouts/consul.erb @@ -10,15 +10,36 @@ Consul Provider + > + Data Sources + + + > Resources diff --git a/website/source/layouts/docker.erb b/website/source/layouts/docker.erb index fd2f76d6d..417cdbe62 100644 --- a/website/source/layouts/docker.erb +++ b/website/source/layouts/docker.erb @@ -10,6 +10,15 @@ Docker Provider + > + Data Sources + + + > Resources