From 1917646ec352769aca9e1260ccf78e9628aa4290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Pinson?= Date: Tue, 14 Mar 2017 18:17:09 +0100 Subject: [PATCH] Add rancher_host resource type (#11545) * Add rancher_host resource type This adds a rancher_host resource type. For now, the goal is to detect if the host already exists, so that it can be purged cleanly when the host is deprovisioned. The typical use is to create both an instance (e.g. aws_instance) and a rancher_host resources with the same hostname. The rancher_host resource will detect when the agent has registered itself. When removing the host, both the instance and the rancher_host resources can be removed, ensuring the host is purged from Rancher. In future versions, this could support creating hosts as well. * Use ro_labels to avoid removing internal Rancher labels As reported in https://github.com/rancher/rancher/issues/8165 * Do not ForceNew on environment_id --- builtin/providers/rancher/provider.go | 1 + .../rancher/resource_rancher_host.go | 200 ++++++++++++++++++ builtin/providers/rancher/util.go | 5 + 3 files changed, 206 insertions(+) create mode 100644 builtin/providers/rancher/resource_rancher_host.go diff --git a/builtin/providers/rancher/provider.go b/builtin/providers/rancher/provider.go index 13a6b7ce4..3025aac8c 100644 --- a/builtin/providers/rancher/provider.go +++ b/builtin/providers/rancher/provider.go @@ -50,6 +50,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "rancher_environment": resourceRancherEnvironment(), + "rancher_host": resourceRancherHost(), "rancher_registration_token": resourceRancherRegistrationToken(), "rancher_registry": resourceRancherRegistry(), "rancher_registry_credential": resourceRancherRegistryCredential(), diff --git a/builtin/providers/rancher/resource_rancher_host.go b/builtin/providers/rancher/resource_rancher_host.go new file mode 100644 index 000000000..326423d40 --- /dev/null +++ b/builtin/providers/rancher/resource_rancher_host.go @@ -0,0 +1,200 @@ +package rancher + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + rancher "github.com/rancher/go-rancher/client" +) + +// ro_labels are used internally by Rancher +// They are not documented and should not be set in Terraform +var ro_labels = []string{ + "io.rancher.host.agent_image", + "io.rancher.host.docker_version", + "io.rancher.host.kvm", + "io.rancher.host.linux_kernel_version", +} + +func resourceRancherHost() *schema.Resource { + return &schema.Resource{ + Create: resourceRancherHostCreate, + Read: resourceRancherHostRead, + Update: resourceRancherHostUpdate, + Delete: resourceRancherHostDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "environment_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "hostname": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "labels": { + Type: schema.TypeMap, + Optional: true, + }, + }, + } +} + +func resourceRancherHostCreate(d *schema.ResourceData, meta interface{}) error { + log.Printf("[INFO][rancher] Creating Host: %s", d.Id()) + client, err := meta.(*Config).EnvironmentClient(d.Get("environment_id").(string)) + if err != nil { + return err + } + + hosts, _ := client.Host.List(NewListOpts()) + hostname := d.Get("hostname").(string) + var host rancher.Host + + for _, h := range hosts.Data { + if h.Hostname == hostname { + host = h + break + } + } + + if host.Hostname == "" { + return fmt.Errorf("Failed to find host %s", hostname) + } + + d.SetId(host.Id) + + return resourceRancherHostUpdate(d, meta) +} + +func resourceRancherHostRead(d *schema.ResourceData, meta interface{}) error { + log.Printf("[INFO] Refreshing Host: %s", d.Id()) + client, err := meta.(*Config).EnvironmentClient(d.Get("environment_id").(string)) + if err != nil { + return err + } + + host, err := client.Host.ById(d.Id()) + if err != nil { + return err + } + + log.Printf("[INFO] Host Name: %s", host.Name) + + d.Set("description", host.Description) + d.Set("name", host.Name) + d.Set("hostname", host.Hostname) + + labels := host.Labels + // Remove read-only labels + for _, lbl := range ro_labels { + delete(labels, lbl) + } + d.Set("labels", host.Labels) + + return nil +} + +func resourceRancherHostUpdate(d *schema.ResourceData, meta interface{}) error { + log.Printf("[INFO] Updating Host: %s", d.Id()) + client, err := meta.(*Config).EnvironmentClient(d.Get("environment_id").(string)) + if err != nil { + return err + } + + name := d.Get("name").(string) + description := d.Get("description").(string) + + // Process labels: merge ro_labels into new labels + labels := d.Get("labels").(map[string]interface{}) + host, err := client.Host.ById(d.Id()) + if err != nil { + return err + } + for _, lbl := range ro_labels { + labels[lbl] = host.Labels[lbl] + } + + data := map[string]interface{}{ + "name": &name, + "description": &description, + "labels": &labels, + } + + var newHost rancher.Host + if err := client.Update("host", &host.Resource, data, &newHost); err != nil { + return err + } + + return resourceRancherHostRead(d, meta) +} + +func resourceRancherHostDelete(d *schema.ResourceData, meta interface{}) error { + log.Printf("[INFO] Deleting Host: %s", d.Id()) + id := d.Id() + client, err := meta.(*Config).EnvironmentClient(d.Get("environment_id").(string)) + if err != nil { + return err + } + + host, err := client.Host.ById(id) + if err != nil { + return err + } + + if err := client.Host.Delete(host); err != nil { + return fmt.Errorf("Error deleting Host: %s", err) + } + + log.Printf("[DEBUG] Waiting for host (%s) to be removed", id) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"active", "removed", "removing"}, + Target: []string{"removed"}, + Refresh: HostStateRefreshFunc(client, id), + Timeout: 10 * time.Minute, + Delay: 1 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, waitErr := stateConf.WaitForState() + if waitErr != nil { + return fmt.Errorf( + "Error waiting for host (%s) to be removed: %s", id, waitErr) + } + + d.SetId("") + return nil +} + +// HostStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch +// a Rancher Host. +func HostStateRefreshFunc(client *rancher.RancherClient, hostID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + host, err := client.Host.ById(hostID) + + if err != nil { + return nil, "", err + } + + return host, host.State, nil + } +} diff --git a/builtin/providers/rancher/util.go b/builtin/providers/rancher/util.go index 60317bf57..efa4a4c3f 100644 --- a/builtin/providers/rancher/util.go +++ b/builtin/providers/rancher/util.go @@ -37,3 +37,8 @@ func splitID(id string) (envID, resourceID string) { } return "", id } + +// NewListOpts wraps around client.NewListOpts() +func NewListOpts() *client.ListOpts { + return client.NewListOpts() +}