From 695ee42a5f56ffd97922b0d9e10461ec3fe77026 Mon Sep 17 00:00:00 2001 From: Raphael Randschau Date: Sun, 1 Apr 2018 11:16:44 -0700 Subject: [PATCH] add scaleway_user_data resource, update scaleway_server --- scaleway/import_user_data_test.go | 28 ++++++ scaleway/provider.go | 1 + scaleway/resource_server.go | 91 +++++++++++++++++++ scaleway/resource_server_test.go | 76 ++++++++++++++++ scaleway/resource_user_data.go | 120 +++++++++++++++++++++++++ scaleway/resource_user_data_test.go | 80 +++++++++++++++++ website/docs/r/server.html.markdown | 13 +++ website/docs/r/user_data.html.markdown | 46 ++++++++++ website/scaleway.erb | 3 + 9 files changed, 458 insertions(+) create mode 100644 scaleway/import_user_data_test.go create mode 100644 scaleway/resource_user_data.go create mode 100644 scaleway/resource_user_data_test.go create mode 100644 website/docs/r/user_data.html.markdown diff --git a/scaleway/import_user_data_test.go b/scaleway/import_user_data_test.go new file mode 100644 index 000000000..5da04fa64 --- /dev/null +++ b/scaleway/import_user_data_test.go @@ -0,0 +1,28 @@ +package scaleway + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccScalewayUserData_importBasic(t *testing.T) { + resourceName := "scaleway_user_data.base" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckScalewayUserDataDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckScalewayUserDataConfig, + }, + + resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/scaleway/provider.go b/scaleway/provider.go index 21816fe59..7bbac5abe 100644 --- a/scaleway/provider.go +++ b/scaleway/provider.go @@ -44,6 +44,7 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ + "scaleway_user_data": resourceScalewayUserData(), "scaleway_server": resourceScalewayServer(), "scaleway_token": resourceScalewayToken(), "scaleway_ssh_key": resourceScalewaySSHKey(), diff --git a/scaleway/resource_server.go b/scaleway/resource_server.go index bc94a78df..8934f04dd 100644 --- a/scaleway/resource_server.go +++ b/scaleway/resource_server.go @@ -2,6 +2,7 @@ package scaleway import ( "fmt" + "hash/fnv" "log" "github.com/hashicorp/terraform/helper/schema" @@ -10,6 +11,8 @@ import ( var commercialServerTypes []string +var sshHostFingerprints = "ssh-host-fingerprints" + func resourceScalewayServer() *schema.Resource { return &schema.Resource{ Create: resourceScalewayServerCreate, @@ -57,6 +60,29 @@ func resourceScalewayServer() *schema.Resource { Optional: true, Description: "The security group the server is attached to", }, + "user_data": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + Set: func(val interface{}) int { + h := fnv.New32a() + userData := val.(map[string]interface{}) + h.Write([]byte(userData["key"].(string))) + return int(h.Sum32()) + }, + Description: "User Data attached to the server on creation", + }, "volume": { Type: schema.TypeList, Optional: true, @@ -201,6 +227,17 @@ func resourceScalewayServerCreate(d *schema.ResourceData, m interface{}) error { return err } + if val, ok := d.GetOk("user_data"); ok { + s := val.(*schema.Set) + for _, v := range s.List() { + data := v.(map[string]interface{}) + err := scaleway.PatchUserdata(server.Identifier, data["key"].(string), []byte(data["value"].(string)), false) + if err != nil { + return err + } + } + } + d.SetId(server.Identifier) if d.Get("state").(string) != "stopped" { task, err := scaleway.PostServerAction(server.Identifier, "poweron") @@ -256,6 +293,26 @@ func resourceScalewayServerRead(d *schema.ResourceData, m interface{}) error { d.Set("state_detail", server.StateDetail) d.Set("tags", server.Tags) + userDatas := []map[string]interface{}{} + keys, err := scaleway.GetUserdatas(d.Id(), false) + if err != nil { + return err + } + for _, key := range keys.UserData { + if key == sshHostFingerprints { + continue + } + data, err := scaleway.GetUserdata(d.Id(), key, false) + if err != nil { + return err + } + userDatas = append(userDatas, map[string]interface{}{ + "key": key, + "value": data.String(), + }) + } + d.Set("user_data", userDatas) + d.SetConnInfo(map[string]string{ "type": "ssh", "host": server.PublicAddress.IP, @@ -304,6 +361,40 @@ func resourceScalewayServerUpdate(d *schema.ResourceData, m interface{}) error { return fmt.Errorf("Failed patching scaleway server: %q", err) } + if d.HasChange("user_data") { + remote, err := scaleway.GetUserdatas(d.Id(), false) + if err != nil { + return err + } + + toDelete := []string{} + local := d.Get("user_data").(*schema.Set) + for _, key := range remote.UserData { + exists := false + for _, v := range local.List() { + exists = exists || v.(map[string]interface{})["key"] == key + } + if !exists { + toDelete = append(toDelete, key) + } + } + for _, key := range toDelete { + if err := scaleway.DeleteUserdata(d.Id(), key, false); err != nil { + return err + } + } + + for _, v := range local.List() { + if err := scaleway.PatchUserdata( + d.Id(), + v.(map[string]interface{})["key"].(string), + []byte(v.(map[string]interface{})["value"].(string)), + false); err != nil { + return err + } + } + } + if d.HasChange("public_ip") { ips, err := scaleway.GetIPS() if err != nil { diff --git a/scaleway/resource_server_test.go b/scaleway/resource_server_test.go index 3ac1a1bf3..7c9ddc413 100644 --- a/scaleway/resource_server_test.go +++ b/scaleway/resource_server_test.go @@ -3,6 +3,7 @@ package scaleway import ( "fmt" "log" + "strings" "testing" "github.com/hashicorp/terraform/helper/resource" @@ -159,6 +160,47 @@ func TestAccScalewayServer_SecurityGroup(t *testing.T) { }) } +func TestAccScalewayServer_UserData(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckScalewayServerDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckScalewayServerConfig_UserDatas, + Check: resource.ComposeTestCheckFunc( + testAccCheckScalewayServerExists("scaleway_server.base"), + resource.TestCheckResourceAttr("scaleway_server.base", "user_data.#", "2"), + resource.TestCheckResourceAttr("scaleway_server.base", "user_data.527074092.key", "app"), + resource.TestCheckResourceAttr("scaleway_server.base", "user_data.527074092.value", "lb"), + resource.TestCheckResourceAttr("scaleway_server.base", "user_data.2562481374.key", "roles"), + resource.TestCheckResourceAttr("scaleway_server.base", "user_data.2562481374.value", "ingress,egress"), + ), + }, + resource.TestStep{ + Config: testAccCheckScalewayServerConfig_UserData, + Check: resource.ComposeTestCheckFunc( + testAccCheckScalewayServerExists("scaleway_server.base"), + resource.TestCheckResourceAttr("scaleway_server.base", "user_data.#", "1"), + resource.TestCheckResourceAttr("scaleway_server.base", "user_data.2562481374.key", "roles"), + resource.TestCheckResourceAttr("scaleway_server.base", "user_data.2562481374.value", "ingress,egress"), + ), + }, + resource.TestStep{ + Config: strings.Replace(testAccCheckScalewayServerConfig_UserDatas, "ingress,egress", "ingress", -1), + Check: resource.ComposeTestCheckFunc( + testAccCheckScalewayServerExists("scaleway_server.base"), + resource.TestCheckResourceAttr("scaleway_server.base", "user_data.#", "2"), + resource.TestCheckResourceAttr("scaleway_server.base", "user_data.527074092.key", "app"), + resource.TestCheckResourceAttr("scaleway_server.base", "user_data.527074092.value", "lb"), + resource.TestCheckResourceAttr("scaleway_server.base", "user_data.2562481374.key", "roles"), + resource.TestCheckResourceAttr("scaleway_server.base", "user_data.2562481374.value", "ingress"), + ), + }, + }, + }) +} + func testAccCheckScalewayServerDestroy(s *terraform.State) error { client := testAccProvider.Meta().(*Client).scaleway @@ -311,6 +353,40 @@ resource "scaleway_server" "base" { tags = [ "terraform-test" ] }`, armImageIdentifier) +var testAccCheckScalewayServerConfig_UserData = fmt.Sprintf(` +resource "scaleway_server" "base" { + name = "test" + # ubuntu 14.04 + image = "%s" + type = "C1" + tags = [ "terraform-test" ] + state = "stopped" + + user_data { + key = "roles" + value = "ingress,egress" + } +}`, armImageIdentifier) + +var testAccCheckScalewayServerConfig_UserDatas = fmt.Sprintf(` +resource "scaleway_server" "base" { + name = "test" + # ubuntu 14.04 + image = "%s" + type = "C1" + tags = [ "terraform-test" ] + state = "stopped" + + user_data { + key = "roles" + value = "ingress,egress" + } + user_data { + key = "app" + value = "lb" + } +}`, armImageIdentifier) + var testAccCheckScalewayServerConfig_IPAttachment = fmt.Sprintf(` resource "scaleway_ip" "base" {} diff --git a/scaleway/resource_user_data.go b/scaleway/resource_user_data.go new file mode 100644 index 000000000..7ae688cdf --- /dev/null +++ b/scaleway/resource_user_data.go @@ -0,0 +1,120 @@ +package scaleway + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + api "github.com/nicolai86/scaleway-sdk" +) + +func resourceScalewayUserData() *schema.Resource { + return &schema.Resource{ + Create: resourceScalewayUserDataCreate, + Read: resourceScalewayUserDataRead, + Update: resourceScalewayUserDataUpdate, + Delete: resourceScalewayUserDataDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "server": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The server the meta data is associated with", + }, + "key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The key of the user data to manage", + }, + "value": { + Type: schema.TypeString, + Required: true, + Description: "The value of the user", + }, + }, + } +} + +func resourceScalewayUserDataCreate(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + + mu.Lock() + if err := scaleway.PatchUserdata( + d.Get("server").(string), + d.Get("key").(string), + []byte(d.Get("value").(string)), + false); err != nil { + return err + } + mu.Unlock() + + d.SetId(fmt.Sprintf("userdata-%s-%s", d.Get("server").(string), d.Get("key").(string))) + return resourceScalewayUserDataRead(d, m) +} + +func resourceScalewayUserDataRead(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + + if d.Get("server").(string) == "" { + // import case + parts := strings.Split(d.Id(), "-") + d.Set("key", parts[len(parts)-1]) + d.Set("server", strings.Join(parts[1:len(parts)-1], "-")) + } + userdata, err := scaleway.GetUserdata( + d.Get("server").(string), + d.Get("key").(string), + false, + ) + + if err != nil { + if serr, ok := err.(api.APIError); ok { + if serr.StatusCode == 404 { + d.SetId("") + return nil + } + } + return err + } + + d.Set("value", userdata.String()) + return nil +} + +func resourceScalewayUserDataUpdate(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + + mu.Lock() + if err := scaleway.PatchUserdata( + d.Get("server").(string), + d.Get("key").(string), + []byte(d.Get("value").(string)), + false); err != nil { + return err + } + mu.Unlock() + + return resourceScalewayUserDataRead(d, m) +} + +func resourceScalewayUserDataDelete(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + + mu.Lock() + defer mu.Unlock() + + err := scaleway.DeleteUserdata( + d.Get("server").(string), + d.Get("key").(string), + false) + if err != nil { + return err + } + d.SetId("") + return nil +} diff --git a/scaleway/resource_user_data_test.go b/scaleway/resource_user_data_test.go new file mode 100644 index 000000000..8a5b2f487 --- /dev/null +++ b/scaleway/resource_user_data_test.go @@ -0,0 +1,80 @@ +package scaleway + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccScalewayUserData_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckScalewayUserDataDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckScalewayUserDataConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckScalewayUserDataExists("scaleway_user_data.base"), + resource.TestCheckResourceAttr("scaleway_user_data.base", "value", "supersecret"), + resource.TestCheckResourceAttr("scaleway_user_data.base", "key", "gcp_username"), + ), + }, + }, + }) +} + +func testAccCheckScalewayUserDataExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + client := testAccProvider.Meta().(*Client).scaleway + _, err := client.GetUserdata(rs.Primary.Attributes["server"], rs.Primary.Attributes["key"], false) + + if err != nil { + return err + } + + return nil + } +} + +func testAccCheckScalewayUserDataDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*Client).scaleway + + for _, rs := range s.RootModule().Resources { + if rs.Type != "scaleway" { + continue + } + + _, err := client.GetUserdata(rs.Primary.Attributes["server"], rs.Primary.Attributes["key"], false) + + if err == nil { + return fmt.Errorf("UserData still exists") + } + } + + return nil +} + +var testAccCheckScalewayUserDataConfig = fmt.Sprintf(` +resource "scaleway_server" "base" { + name = "test" + # ubuntu 14.04 + image = "%s" + type = "C1" + state = "stopped" +} + +resource "scaleway_user_data" "base" { + server = "${scaleway_server.base.id}" + key = "gcp_username" + value = "supersecret" +} +`, armImageIdentifier) diff --git a/website/docs/r/server.html.markdown b/website/docs/r/server.html.markdown index 075ad7f57..b10e787fc 100644 --- a/website/docs/r/server.html.markdown +++ b/website/docs/r/server.html.markdown @@ -39,6 +39,7 @@ The following arguments are supported: * `dynamic_ip_required` - (Optional) make server publicly available * `security_group` - (Optional) assign security group to server * `volume` - (Optional) attach additional volumes to your instance (see below) +* `user_data` - (Optional) additional user_data to provide to the instance (see below) * `public_ipv6` - (Read Only) if `enable_ipv6` is set this contains the ipv6 address of your instance * `state` - (Optional) allows you to define the desired state of your server. Valid values include (`stopped`, `running`) * `state_detail` - (Read Only) contains details from the scaleway API the state of your instance @@ -58,6 +59,18 @@ The `volume` mapping supports the following: * `type` - (Required) The type of volume. Can be `"l_ssd"` * `size_in_gb` - (Required) The size of the volume in gigabytes. +## User data + +You can provide additional data to your instance, which will share the lifetime +of your `scaleway_server` resource. + +The `user_data` mapping supports the following: + +* `key` - (Required) The key of the user data +* `value` - (Required) The value of the user data + +See the [API documentation](https://developer.scaleway.com/#user-data) on how to access this data from your server. + ## Attributes Reference The following attributes are exported: diff --git a/website/docs/r/user_data.html.markdown b/website/docs/r/user_data.html.markdown new file mode 100644 index 000000000..13b775a58 --- /dev/null +++ b/website/docs/r/user_data.html.markdown @@ -0,0 +1,46 @@ +--- +layout: "scaleway" +page_title: "Scaleway: user_data" +sidebar_current: "docs-scaleway-resource-user_data" +description: |- + Manages Scaleway Server UserData. +--- + +# scaleway\_user\_data + +Provides user data for servers. +For additional details please refer to [API documentation](https://developer.scaleway.com/#user-data). + +## Example Usage + +```hcl +resource "scaleway_server" "base" { + name = "test" + # ubuntu 14.04 + image = "5faef9cd-ea9b-4a63-9171-9e26bec03dbc" + type = "C1" + state = "stopped" +} + +resource "scaleway_user_data" "gcp" { + server = "${scaleway_server.base.id}" + key = "gcp_username" + value = "supersecret" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `server` - (Required) ID of server to associate the user data with +* `key` - (Required) The key of the user data object +* `value` - (Required) The value of the user data object + +## Import + +Instances can be imported using the `id`, e.g. + +``` +$ terraform import scaleway_user_data.gcp userdata-- +``` diff --git a/website/scaleway.erb b/website/scaleway.erb index 6f2713e4c..7ed20ee2f 100644 --- a/website/scaleway.erb +++ b/website/scaleway.erb @@ -43,6 +43,9 @@ > scaleway_token + > + scaleway_user_data + > scaleway_volume_attachment