From 9081cabd6ee20258c9c6bb9a70697ee70075b307 Mon Sep 17 00:00:00 2001 From: Raphael Randschau Date: Wed, 13 Jul 2016 22:03:41 +0200 Subject: [PATCH] Add scaleway provider (#7331) * Add scaleway provider this PR allows the entire scaleway stack to be managed with terraform example usage looks like this: ``` provider "scaleway" { api_key = "snap" organization = "snip" } resource "scaleway_ip" "base" { server = "${scaleway_server.base.id}" } resource "scaleway_server" "base" { name = "test" # ubuntu 14.04 image = "aecaed73-51a5-4439-a127-6d8229847145" type = "C2S" } resource "scaleway_volume" "test" { name = "test" size_in_gb = 20 type = "l_ssd" } resource "scaleway_volume_attachment" "test" { server = "${scaleway_server.base.id}" volume = "${scaleway_volume.test.id}" } resource "scaleway_security_group" "base" { name = "public" description = "public gateway" } resource "scaleway_security_group_rule" "http-ingress" { security_group = "${scaleway_security_group.base.id}" action = "accept" direction = "inbound" ip_range = "0.0.0.0/0" protocol = "TCP" port = 80 } resource "scaleway_security_group_rule" "http-egress" { security_group = "${scaleway_security_group.base.id}" action = "accept" direction = "outbound" ip_range = "0.0.0.0/0" protocol = "TCP" port = 80 } ``` Note that volume attachments require the server to be stopped, which can lead to downtimes of you attach new volumes to already used servers * Update IP read to handle 404 gracefully * Read back resource on update * Ensure IP detachment works as expected Sadly this is not part of the official scaleway api just yet * Adjust detachIP helper based on feedback from @QuentinPerez in https://github.com/scaleway/scaleway-cli/pull/378 * Cleanup documentation * Rename api_key to access_key following @stack72 suggestion and rename the provider api_key for more clarity * Make tests less chatty by using custom logger --- builtin/bins/provider-scaleway/main.go | 12 + builtin/providers/scaleway/config.go | 62 + builtin/providers/scaleway/helpers.go | 101 + builtin/providers/scaleway/provider.go | 46 + builtin/providers/scaleway/provider_test.go | 38 + .../scaleway/resource_scaleway_ip.go | 86 + .../scaleway/resource_scaleway_ip_test.go | 140 + .../resource_scaleway_security_group.go | 118 + .../resource_scaleway_security_group_rule.go | 162 + ...ource_scaleway_security_group_rule_test.go | 158 + .../resource_scaleway_security_group_test.go | 104 + .../scaleway/resource_scaleway_server.go | 183 ++ .../scaleway/resource_scaleway_server_test.go | 113 + .../scaleway/resource_scaleway_volume.go | 127 + .../resource_scaleway_volume_attachment.go | 201 ++ ...esource_scaleway_volume_attachment_test.go | 93 + .../scaleway/resource_scaleway_volume_test.go | 107 + command/internal_plugin_list.go | 2 + vendor/github.com/moul/anonuuid/LICENSE | 22 + vendor/github.com/moul/anonuuid/README.md | 170 ++ vendor/github.com/moul/anonuuid/anonuuid.go | 229 ++ .../github.com/renstrom/fuzzysearch/LICENSE | 21 + .../renstrom/fuzzysearch/fuzzy/fuzzy.go | 167 ++ .../renstrom/fuzzysearch/fuzzy/levenshtein.go | 43 + .../scaleway/scaleway-cli/LICENSE.md | 22 + .../scaleway/scaleway-cli/pkg/api/README.md | 25 + .../scaleway/scaleway-cli/pkg/api/api.go | 2667 +++++++++++++++++ .../scaleway/scaleway-cli/pkg/api/api_test.go | 21 + .../scaleway/scaleway-cli/pkg/api/cache.go | 731 +++++ .../scaleway/scaleway-cli/pkg/api/logger.go | 49 + .../scaleway-cli/pkg/scwversion/version.go | 16 + .../pkg/scwversion/version_test.go | 14 + .../providers/scaleway/index.html.markdown | 90 + .../providers/scaleway/r/ip.html.markdown | 34 + .../scaleway/r/security_group.html.markdown | 36 + .../r/security_group_rule.html.markdown | 51 + .../providers/scaleway/r/server.html.markdown | 38 + .../providers/scaleway/r/volume.html.markdown | 44 + .../r/volume_attachment.html.markdown | 48 + website/source/layouts/docs.erb | 4 + website/source/layouts/scaleway.erb | 41 + 41 files changed, 6436 insertions(+) create mode 100644 builtin/bins/provider-scaleway/main.go create mode 100644 builtin/providers/scaleway/config.go create mode 100644 builtin/providers/scaleway/helpers.go create mode 100644 builtin/providers/scaleway/provider.go create mode 100644 builtin/providers/scaleway/provider_test.go create mode 100644 builtin/providers/scaleway/resource_scaleway_ip.go create mode 100644 builtin/providers/scaleway/resource_scaleway_ip_test.go create mode 100644 builtin/providers/scaleway/resource_scaleway_security_group.go create mode 100644 builtin/providers/scaleway/resource_scaleway_security_group_rule.go create mode 100644 builtin/providers/scaleway/resource_scaleway_security_group_rule_test.go create mode 100644 builtin/providers/scaleway/resource_scaleway_security_group_test.go create mode 100644 builtin/providers/scaleway/resource_scaleway_server.go create mode 100644 builtin/providers/scaleway/resource_scaleway_server_test.go create mode 100644 builtin/providers/scaleway/resource_scaleway_volume.go create mode 100644 builtin/providers/scaleway/resource_scaleway_volume_attachment.go create mode 100644 builtin/providers/scaleway/resource_scaleway_volume_attachment_test.go create mode 100644 builtin/providers/scaleway/resource_scaleway_volume_test.go create mode 100644 vendor/github.com/moul/anonuuid/LICENSE create mode 100644 vendor/github.com/moul/anonuuid/README.md create mode 100644 vendor/github.com/moul/anonuuid/anonuuid.go create mode 100644 vendor/github.com/renstrom/fuzzysearch/LICENSE create mode 100644 vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go create mode 100644 vendor/github.com/renstrom/fuzzysearch/fuzzy/levenshtein.go create mode 100644 vendor/github.com/scaleway/scaleway-cli/LICENSE.md create mode 100644 vendor/github.com/scaleway/scaleway-cli/pkg/api/README.md create mode 100644 vendor/github.com/scaleway/scaleway-cli/pkg/api/api.go create mode 100644 vendor/github.com/scaleway/scaleway-cli/pkg/api/api_test.go create mode 100644 vendor/github.com/scaleway/scaleway-cli/pkg/api/cache.go create mode 100644 vendor/github.com/scaleway/scaleway-cli/pkg/api/logger.go create mode 100644 vendor/github.com/scaleway/scaleway-cli/pkg/scwversion/version.go create mode 100644 vendor/github.com/scaleway/scaleway-cli/pkg/scwversion/version_test.go create mode 100644 website/source/docs/providers/scaleway/index.html.markdown create mode 100644 website/source/docs/providers/scaleway/r/ip.html.markdown create mode 100644 website/source/docs/providers/scaleway/r/security_group.html.markdown create mode 100644 website/source/docs/providers/scaleway/r/security_group_rule.html.markdown create mode 100644 website/source/docs/providers/scaleway/r/server.html.markdown create mode 100644 website/source/docs/providers/scaleway/r/volume.html.markdown create mode 100644 website/source/docs/providers/scaleway/r/volume_attachment.html.markdown create mode 100644 website/source/layouts/scaleway.erb diff --git a/builtin/bins/provider-scaleway/main.go b/builtin/bins/provider-scaleway/main.go new file mode 100644 index 000000000..0d9754797 --- /dev/null +++ b/builtin/bins/provider-scaleway/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/scaleway" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: scaleway.Provider, + }) +} diff --git a/builtin/providers/scaleway/config.go b/builtin/providers/scaleway/config.go new file mode 100644 index 000000000..5d321344a --- /dev/null +++ b/builtin/providers/scaleway/config.go @@ -0,0 +1,62 @@ +package scaleway + +import ( + "fmt" + "log" + "net/http" + "os" + + "github.com/scaleway/scaleway-cli/pkg/api" + "github.com/scaleway/scaleway-cli/pkg/scwversion" +) + +// Config contains scaleway configuration values +type Config struct { + Organization string + APIKey string +} + +// Client contains scaleway api clients +type Client struct { + scaleway *api.ScalewayAPI +} + +// Client configures and returns a fully initialized Scaleway client +func (c *Config) Client() (*Client, error) { + api, err := api.NewScalewayAPI( + c.Organization, + c.APIKey, + scwversion.UserAgent(), + func(s *api.ScalewayAPI) { + s.Logger = newTerraformLogger() + }, + ) + if err != nil { + return nil, err + } + return &Client{api}, nil +} + +func newTerraformLogger() api.Logger { + return &terraformLogger{} +} + +type terraformLogger struct { +} + +func (l *terraformLogger) LogHTTP(r *http.Request) { + log.Printf("[DEBUG] %s %s\n", r.Method, r.URL.Path) +} +func (l *terraformLogger) Fatalf(format string, v ...interface{}) { + log.Printf("[FATAL] %s\n", fmt.Sprintf(format, v)) + os.Exit(1) +} +func (l *terraformLogger) Debugf(format string, v ...interface{}) { + log.Printf("[DEBUG] %s\n", fmt.Sprintf(format, v)) +} +func (l *terraformLogger) Infof(format string, v ...interface{}) { + log.Printf("[INFO ] %s\n", fmt.Sprintf(format, v)) +} +func (l *terraformLogger) Warnf(format string, v ...interface{}) { + log.Printf("[WARN ] %s\n", fmt.Sprintf(format, v)) +} diff --git a/builtin/providers/scaleway/helpers.go b/builtin/providers/scaleway/helpers.go new file mode 100644 index 000000000..b1ba9ba9d --- /dev/null +++ b/builtin/providers/scaleway/helpers.go @@ -0,0 +1,101 @@ +package scaleway + +import ( + "fmt" + "log" + "net/http" + "time" + + "github.com/scaleway/scaleway-cli/pkg/api" +) + +// Bool returns a pointer to of the bool value passed in. +func Bool(val bool) *bool { + return &val +} + +// String returns a pointer to of the string value passed in. +func String(val string) *string { + return &val +} + +// DetachIP detaches an IP from a server +func DetachIP(s *api.ScalewayAPI, ipID string) error { + var update struct { + Address string `json:"address"` + ID string `json:"id"` + Organization string `json:"organization"` + } + + ip, err := s.GetIP(ipID) + if err != nil { + return err + } + update.Address = ip.IP.Address + update.ID = ip.IP.ID + update.Organization = ip.IP.Organization + + resp, err := s.PutResponse(api.ComputeAPI, fmt.Sprintf("ips/%s", ipID), update) + if err != nil { + return err + } + if resp.StatusCode != http.StatusOK { + return err + } + resp.Body.Close() + return nil +} + +// NOTE copied from github.com/scaleway/scaleway-cli/pkg/api/helpers.go +// the helpers.go file pulls in quite a lot dependencies, and they're just convenience wrappers anyway + +func deleteServerSafe(s *api.ScalewayAPI, serverID string) error { + server, err := s.GetServer(serverID) + if err != nil { + return err + } + + if server.State != "stopped" { + if err := s.PostServerAction(serverID, "poweroff"); err != nil { + return err + } + if err := waitForServerState(s, serverID, "stopped"); err != nil { + return err + } + } + + if err := s.DeleteServer(serverID); err != nil { + return err + } + if rootVolume, ok := server.Volumes["0"]; ok { + if err := s.DeleteVolume(rootVolume.Identifier); err != nil { + return err + } + } + + return nil +} + +func waitForServerState(s *api.ScalewayAPI, serverID string, targetState string) error { + var server *api.ScalewayServer + var err error + + var currentState string + + for { + server, err = s.GetServer(serverID) + if err != nil { + return err + } + if currentState != server.State { + log.Printf("[DEBUG] Server changed state to %q\n", server.State) + currentState = server.State + } + if server.State == targetState { + break + } + time.Sleep(1 * time.Second) + } + + return nil +} diff --git a/builtin/providers/scaleway/provider.go b/builtin/providers/scaleway/provider.go new file mode 100644 index 000000000..f2d417b81 --- /dev/null +++ b/builtin/providers/scaleway/provider.go @@ -0,0 +1,46 @@ +package scaleway + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +// Provider returns a terraform.ResourceProvider. +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "access_key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("SCALEWAY_ACCESS_KEY", nil), + Description: "The API key for Scaleway API operations.", + }, + "organization": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("SCALEWAY_ORGANIZATION", nil), + Description: "The Organization ID for Scaleway API operations.", + }, + }, + + ResourcesMap: map[string]*schema.Resource{ + "scaleway_server": resourceScalewayServer(), + "scaleway_ip": resourceScalewayIP(), + "scaleway_security_group": resourceScalewaySecurityGroup(), + "scaleway_security_group_rule": resourceScalewaySecurityGroupRule(), + "scaleway_volume": resourceScalewayVolume(), + "scaleway_volume_attachment": resourceScalewayVolumeAttachment(), + }, + + ConfigureFunc: providerConfigure, + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + config := Config{ + Organization: d.Get("organization").(string), + APIKey: d.Get("access_key").(string), + } + + return config.Client() +} diff --git a/builtin/providers/scaleway/provider_test.go b/builtin/providers/scaleway/provider_test.go new file mode 100644 index 000000000..8bcc32b7c --- /dev/null +++ b/builtin/providers/scaleway/provider_test.go @@ -0,0 +1,38 @@ +package scaleway + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "scaleway": testAccProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("SCALEWAY_ORGANIZATION"); v == "" { + t.Fatal("SCALEWAY_ORGANIZATION must be set for acceptance tests") + } + if v := os.Getenv("SCALEWAY_ACCESS_KEY"); v == "" { + t.Fatal("SCALEWAY_ACCESS_KEY must be set for acceptance tests") + } +} diff --git a/builtin/providers/scaleway/resource_scaleway_ip.go b/builtin/providers/scaleway/resource_scaleway_ip.go new file mode 100644 index 000000000..b4fe7003f --- /dev/null +++ b/builtin/providers/scaleway/resource_scaleway_ip.go @@ -0,0 +1,86 @@ +package scaleway + +import ( + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/scaleway/scaleway-cli/pkg/api" +) + +func resourceScalewayIP() *schema.Resource { + return &schema.Resource{ + Create: resourceScalewayIPCreate, + Read: resourceScalewayIPRead, + Update: resourceScalewayIPUpdate, + Delete: resourceScalewayIPDelete, + Schema: map[string]*schema.Schema{ + "server": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "ip": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceScalewayIPCreate(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + resp, err := scaleway.NewIP() + if err != nil { + return err + } + + d.SetId(resp.IP.ID) + return resourceScalewayIPUpdate(d, m) +} + +func resourceScalewayIPRead(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + log.Printf("[DEBUG] Reading IP\n") + + resp, err := scaleway.GetIP(d.Id()) + if err != nil { + log.Printf("[DEBUG] Error reading ip: %q\n", err) + if serr, ok := err.(api.ScalewayAPIError); ok { + if serr.StatusCode == 404 { + d.SetId("") + return nil + } + } + return err + } + + d.Set("ip", resp.IP.Address) + d.Set("server", resp.IP.Server.Identifier) + return nil +} + +func resourceScalewayIPUpdate(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + if d.HasChange("server") { + if d.Get("server").(string) != "" { + log.Printf("[DEBUG] Attaching IP %q to server %q\n", d.Id(), d.Get("server").(string)) + if err := scaleway.AttachIP(d.Id(), d.Get("server").(string)); err != nil { + return err + } + } else { + log.Printf("[DEBUG] Detaching IP %q\n", d.Id()) + return DetachIP(scaleway, d.Id()) + } + } + + return resourceScalewayIPRead(d, m) +} + +func resourceScalewayIPDelete(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + err := scaleway.DeleteIP(d.Id()) + if err != nil { + return err + } + d.SetId("") + return nil +} diff --git a/builtin/providers/scaleway/resource_scaleway_ip_test.go b/builtin/providers/scaleway/resource_scaleway_ip_test.go new file mode 100644 index 000000000..464817eff --- /dev/null +++ b/builtin/providers/scaleway/resource_scaleway_ip_test.go @@ -0,0 +1,140 @@ +package scaleway + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccScalewayIP_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckScalewayIPDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckScalewayIPConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckScalewayIPExists("scaleway_ip.base"), + ), + }, + resource.TestStep{ + Config: testAccCheckScalewayIPAttachConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckScalewayIPExists("scaleway_ip.base"), + testAccCheckScalewayIPAttachment("scaleway_ip.base", func(serverID string) bool { + return serverID != "" + }, "attachment failed"), + ), + }, + resource.TestStep{ + Config: testAccCheckScalewayIPConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckScalewayIPExists("scaleway_ip.base"), + testAccCheckScalewayIPAttachment("scaleway_ip.base", func(serverID string) bool { + return serverID == "" + }, "detachment failed"), + ), + }, + }, + }) +} + +func testAccCheckScalewayIPDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*Client).scaleway + + for _, rs := range s.RootModule().Resources { + if rs.Type != "scaleway" { + continue + } + + _, err := client.GetIP(rs.Primary.ID) + + if err == nil { + return fmt.Errorf("IP still exists") + } + } + + return nil +} + +func testAccCheckScalewayIPAttributes() resource.TestCheckFunc { + return func(s *terraform.State) error { + return nil + } +} + +func testAccCheckScalewayIPExists(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) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No IP ID is set") + } + + client := testAccProvider.Meta().(*Client).scaleway + ip, err := client.GetIP(rs.Primary.ID) + + if err != nil { + return err + } + + if ip.IP.ID != rs.Primary.ID { + return fmt.Errorf("Record not found") + } + + return nil + } +} + +func testAccCheckScalewayIPAttachment(n string, check func(string) bool, msg string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No IP ID is set") + } + + client := testAccProvider.Meta().(*Client).scaleway + ip, err := client.GetIP(rs.Primary.ID) + + if err != nil { + return err + } + + if !check(ip.IP.Server.Identifier) { + return fmt.Errorf("IP check failed: %q", msg) + } + + return nil + } +} + +var testAccCheckScalewayIPConfig = ` +resource "scaleway_ip" "base" { +} +` + +var testAccCheckScalewayIPAttachConfig = fmt.Sprintf(` +resource "scaleway_server" "base" { + name = "test" + # ubuntu 14.04 + image = "%s" + type = "C1" + state = "stopped" +} + +resource "scaleway_ip" "base" { + server = "${scaleway_server.base.id}" +} +`, armImageIdentifier) diff --git a/builtin/providers/scaleway/resource_scaleway_security_group.go b/builtin/providers/scaleway/resource_scaleway_security_group.go new file mode 100644 index 000000000..6c5da13d6 --- /dev/null +++ b/builtin/providers/scaleway/resource_scaleway_security_group.go @@ -0,0 +1,118 @@ +package scaleway + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/scaleway/scaleway-cli/pkg/api" +) + +func resourceScalewaySecurityGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceScalewaySecurityGroupCreate, + Read: resourceScalewaySecurityGroupRead, + Update: resourceScalewaySecurityGroupUpdate, + Delete: resourceScalewaySecurityGroupDelete, + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func resourceScalewaySecurityGroupCreate(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + + req := api.ScalewayNewSecurityGroup{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Organization: scaleway.Organization, + } + + err := scaleway.PostSecurityGroup(req) + if err != nil { + if serr, ok := err.(api.ScalewayAPIError); ok { + log.Printf("[DEBUG] Error creating security group: %q\n", serr.APIMessage) + } + + return err + } + + resp, err := scaleway.GetSecurityGroups() + if err != nil { + return err + } + + for _, group := range resp.SecurityGroups { + if group.Name == req.Name { + d.SetId(group.ID) + break + } + } + + if d.Id() == "" { + return fmt.Errorf("Failed to find created security group.") + } + + return resourceScalewaySecurityGroupRead(d, m) +} + +func resourceScalewaySecurityGroupRead(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + resp, err := scaleway.GetASecurityGroup(d.Id()) + + if err != nil { + if serr, ok := err.(api.ScalewayAPIError); ok { + log.Printf("[DEBUG] Error reading security group: %q\n", serr.APIMessage) + + if serr.StatusCode == 404 { + d.SetId("") + return nil + } + } + + return err + } + + d.Set("name", resp.SecurityGroups.Name) + d.Set("description", resp.SecurityGroups.Description) + + return nil +} + +func resourceScalewaySecurityGroupUpdate(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + + var req = api.ScalewayNewSecurityGroup{ + Organization: scaleway.Organization, + Name: d.Get("name").(string), + Description: d.Get("description").(string), + } + + if err := scaleway.PutSecurityGroup(req, d.Id()); err != nil { + log.Printf("[DEBUG] Error reading security group: %q\n", err) + + return err + } + + return resourceScalewaySecurityGroupRead(d, m) +} + +func resourceScalewaySecurityGroupDelete(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + + err := scaleway.DeleteSecurityGroup(d.Id()) + if err != nil { + return err + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/scaleway/resource_scaleway_security_group_rule.go b/builtin/providers/scaleway/resource_scaleway_security_group_rule.go new file mode 100644 index 000000000..85c2f3575 --- /dev/null +++ b/builtin/providers/scaleway/resource_scaleway_security_group_rule.go @@ -0,0 +1,162 @@ +package scaleway + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/scaleway/scaleway-cli/pkg/api" +) + +func resourceScalewaySecurityGroupRule() *schema.Resource { + return &schema.Resource{ + Create: resourceScalewaySecurityGroupRuleCreate, + Read: resourceScalewaySecurityGroupRuleRead, + Update: resourceScalewaySecurityGroupRuleUpdate, + Delete: resourceScalewaySecurityGroupRuleDelete, + Schema: map[string]*schema.Schema{ + "security_group": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "action": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if value != "accept" && value != "drop" { + errors = append(errors, fmt.Errorf("%q must be one of 'accept', 'drop'", k)) + } + return + }, + }, + "direction": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if value != "inbound" && value != "outbound" { + errors = append(errors, fmt.Errorf("%q must be one of 'inbound', 'outbound'", k)) + } + return + }, + }, + "ip_range": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "protocol": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if value != "ICMP" && value != "TCP" && value != "UDP" { + errors = append(errors, fmt.Errorf("%q must be one of 'ICMP', 'TCP', 'UDP", k)) + } + return + }, + }, + "port": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + }, + } +} + +func resourceScalewaySecurityGroupRuleCreate(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + + req := api.ScalewayNewSecurityGroupRule{ + Action: d.Get("action").(string), + Direction: d.Get("direction").(string), + IPRange: d.Get("ip_range").(string), + Protocol: d.Get("protocol").(string), + DestPortFrom: d.Get("port").(int), + } + + err := scaleway.PostSecurityGroupRule(d.Get("security_group").(string), req) + if err != nil { + if serr, ok := err.(api.ScalewayAPIError); ok { + log.Printf("[DEBUG] Error creating Security Group Rule: %q\n", serr.APIMessage) + } + + return err + } + + resp, err := scaleway.GetSecurityGroupRules(d.Get("security_group").(string)) + if err != nil { + return err + } + + for _, rule := range resp.Rules { + if rule.Action == req.Action && rule.Direction == req.Direction && rule.IPRange == req.IPRange && rule.Protocol == req.Protocol { + d.SetId(rule.ID) + break + } + } + + if d.Id() == "" { + return fmt.Errorf("Failed to find created security group rule") + } + + return resourceScalewaySecurityGroupRuleRead(d, m) +} + +func resourceScalewaySecurityGroupRuleRead(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + rule, err := scaleway.GetASecurityGroupRule(d.Get("security_group").(string), d.Id()) + + if err != nil { + if serr, ok := err.(api.ScalewayAPIError); ok { + log.Printf("[DEBUG] error reading Security Group Rule: %q\n", serr.APIMessage) + + if serr.StatusCode == 404 { + d.SetId("") + return nil + } + } + + return err + } + + d.Set("action", rule.Rules.Action) + d.Set("direction", rule.Rules.Direction) + d.Set("ip_range", rule.Rules.IPRange) + d.Set("protocol", rule.Rules.Protocol) + d.Set("port", rule.Rules.DestPortFrom) + + return nil +} + +func resourceScalewaySecurityGroupRuleUpdate(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + + var req = api.ScalewayNewSecurityGroupRule{ + Action: d.Get("action").(string), + Direction: d.Get("direction").(string), + IPRange: d.Get("ip_range").(string), + Protocol: d.Get("protocol").(string), + DestPortFrom: d.Get("port").(int), + } + + if err := scaleway.PutSecurityGroupRule(req, d.Get("security_group").(string), d.Id()); err != nil { + log.Printf("[DEBUG] error updating Security Group Rule: %q", err) + + return err + } + + return resourceScalewaySecurityGroupRuleRead(d, m) +} + +func resourceScalewaySecurityGroupRuleDelete(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + + err := scaleway.DeleteSecurityGroupRule(d.Get("security_group").(string), d.Id()) + if err != nil { + return err + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/scaleway/resource_scaleway_security_group_rule_test.go b/builtin/providers/scaleway/resource_scaleway_security_group_rule_test.go new file mode 100644 index 000000000..aeafa41b6 --- /dev/null +++ b/builtin/providers/scaleway/resource_scaleway_security_group_rule_test.go @@ -0,0 +1,158 @@ +package scaleway + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/scaleway/scaleway-cli/pkg/api" +) + +func TestAccScalewaySecurityGroupRule_Basic(t *testing.T) { + var group api.ScalewaySecurityGroups + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckScalewaySecurityGroupRuleDestroy(&group), + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckScalewaySecurityGroupRuleConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckScalewaySecurityGroupsExists("scaleway_security_group.base", &group), + resource.TestCheckResourceAttr("scaleway_security_group_rule.http", "action", "drop"), + resource.TestCheckResourceAttr("scaleway_security_group_rule.http", "direction", "inbound"), + resource.TestCheckResourceAttr("scaleway_security_group_rule.http", "ip_range", "0.0.0.0/0"), + resource.TestCheckResourceAttr("scaleway_security_group_rule.http", "protocol", "TCP"), + testAccCheckScalewaySecurityGroupRuleExists("scaleway_security_group_rule.http", &group), + testAccCheckScalewaySecurityGroupRuleAttributes("scaleway_security_group_rule.http", &group), + ), + }, + }, + }) +} + +func testAccCheckScalewaySecurityGroupsExists(n string, group *api.ScalewaySecurityGroups) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Security Group Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Security Group is set") + } + + conn := testAccProvider.Meta().(*Client).scaleway + resp, err := conn.GetASecurityGroup(rs.Primary.ID) + + if err != nil { + return err + } + + if resp.SecurityGroups.ID == rs.Primary.ID { + *group = resp.SecurityGroups + return nil + } + + return fmt.Errorf("Security Group not found") + } +} + +func testAccCheckScalewaySecurityGroupRuleDestroy(group *api.ScalewaySecurityGroups) func(*terraform.State) error { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*Client).scaleway + + for _, rs := range s.RootModule().Resources { + if rs.Type != "scaleway" { + continue + } + + _, err := client.GetASecurityGroupRule(group.ID, rs.Primary.ID) + + if err == nil { + return fmt.Errorf("Security Group still exists") + } + } + + return nil + } +} + +func testAccCheckScalewaySecurityGroupRuleAttributes(n string, group *api.ScalewaySecurityGroups) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Unknown resource: %s", n) + } + + client := testAccProvider.Meta().(*Client).scaleway + rule, err := client.GetASecurityGroupRule(group.ID, rs.Primary.ID) + if err != nil { + return err + } + + if rule.Rules.Action != "drop" { + return fmt.Errorf("Wrong rule action") + } + if rule.Rules.Direction != "inbound" { + return fmt.Errorf("wrong rule direction") + } + if rule.Rules.IPRange != "0.0.0.0/0" { + return fmt.Errorf("wrong rule IP Range") + } + if rule.Rules.Protocol != "TCP" { + return fmt.Errorf("wrong rule protocol") + } + if rule.Rules.DestPortFrom != 80 { + return fmt.Errorf("Wrong port") + } + + return nil + } +} + +func testAccCheckScalewaySecurityGroupRuleExists(n string, group *api.ScalewaySecurityGroups) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + + if !ok { + return fmt.Errorf("Security Group Rule Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Security Group Rule ID is set") + } + + client := testAccProvider.Meta().(*Client).scaleway + rule, err := client.GetASecurityGroupRule(group.ID, rs.Primary.ID) + + if err != nil { + return err + } + + if rule.Rules.ID != rs.Primary.ID { + return fmt.Errorf("Record not found") + } + + return nil + } +} + +var testAccCheckScalewaySecurityGroupRuleConfig = ` +resource "scaleway_security_group" "base" { + name = "public" + description = "public gateway" +} + +resource "scaleway_security_group_rule" "http" { + security_group = "${scaleway_security_group.base.id}" + + action = "drop" + direction = "inbound" + ip_range = "0.0.0.0/0" + protocol = "TCP" + port = 80 +} +` diff --git a/builtin/providers/scaleway/resource_scaleway_security_group_test.go b/builtin/providers/scaleway/resource_scaleway_security_group_test.go new file mode 100644 index 000000000..22d351305 --- /dev/null +++ b/builtin/providers/scaleway/resource_scaleway_security_group_test.go @@ -0,0 +1,104 @@ +package scaleway + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccScalewaySecurityGroup_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckScalewaySecurityGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckScalewaySecurityGroupConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckScalewaySecurityGroupExists("scaleway_security_group.base"), + testAccCheckScalewaySecurityGroupAttributes("scaleway_security_group.base"), + resource.TestCheckResourceAttr("scaleway_security_group.base", "name", "public"), + resource.TestCheckResourceAttr("scaleway_security_group.base", "description", "public gateway"), + ), + }, + }, + }) +} + +func testAccCheckScalewaySecurityGroupDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*Client).scaleway + + for _, rs := range s.RootModule().Resources { + if rs.Type != "scaleway" { + continue + } + + _, err := client.GetASecurityGroup(rs.Primary.ID) + + if err == nil { + return fmt.Errorf("Security Group still exists") + } + } + + return nil +} + +func testAccCheckScalewaySecurityGroupAttributes(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Unknown resource: %s", n) + } + + client := testAccProvider.Meta().(*Client).scaleway + group, err := client.GetASecurityGroup(rs.Primary.ID) + if err != nil { + return err + } + + if group.SecurityGroups.Name != "public" { + return fmt.Errorf("Security Group has wrong name") + } + if group.SecurityGroups.Description != "public gateway" { + return fmt.Errorf("Security Group has wrong description") + } + + return nil + } +} + +func testAccCheckScalewaySecurityGroupExists(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) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Security Group ID is set") + } + + client := testAccProvider.Meta().(*Client).scaleway + group, err := client.GetASecurityGroup(rs.Primary.ID) + + if err != nil { + return err + } + + if group.SecurityGroups.ID != rs.Primary.ID { + return fmt.Errorf("Record not found") + } + + return nil + } +} + +var testAccCheckScalewaySecurityGroupConfig = ` +resource "scaleway_security_group" "base" { + name = "public" + description = "public gateway" +} +` diff --git a/builtin/providers/scaleway/resource_scaleway_server.go b/builtin/providers/scaleway/resource_scaleway_server.go new file mode 100644 index 000000000..0dddfa8e7 --- /dev/null +++ b/builtin/providers/scaleway/resource_scaleway_server.go @@ -0,0 +1,183 @@ +package scaleway + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/scaleway/scaleway-cli/pkg/api" +) + +func resourceScalewayServer() *schema.Resource { + return &schema.Resource{ + Create: resourceScalewayServerCreate, + Read: resourceScalewayServerRead, + Update: resourceScalewayServerUpdate, + Delete: resourceScalewayServerDelete, + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "image": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "bootscript": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "tags": &schema.Schema{ + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "ipv4_address_private": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "ipv4_address_public": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "state": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "dynamic_ip_required": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "state_detail": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceScalewayServerCreate(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + + image := d.Get("image").(string) + var server = api.ScalewayServerDefinition{ + Name: d.Get("name").(string), + Image: String(image), + Organization: scaleway.Organization, + } + + server.DynamicIPRequired = Bool(d.Get("dynamic_ip_required").(bool)) + server.CommercialType = d.Get("type").(string) + + if bootscript, ok := d.GetOk("bootscript"); ok { + server.Bootscript = String(bootscript.(string)) + } + + if tags, ok := d.GetOk("tags"); ok { + server.Tags = tags.([]string) + } + + id, err := scaleway.PostServer(server) + if err != nil { + return err + } + + d.SetId(id) + if d.Get("state").(string) != "stopped" { + err = scaleway.PostServerAction(id, "poweron") + if err != nil { + return err + } + + err = waitForServerState(scaleway, id, "running") + } + + if err != nil { + return err + } + + return resourceScalewayServerRead(d, m) +} + +func resourceScalewayServerRead(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + server, err := scaleway.GetServer(d.Id()) + + if err != nil { + if serr, ok := err.(api.ScalewayAPIError); ok { + log.Printf("[DEBUG] Error reading server: %q\n", serr.APIMessage) + + if serr.StatusCode == 404 { + d.SetId("") + return nil + } + } + + return err + } + + d.Set("ipv4_address_private", server.PrivateIP) + d.Set("ipv4_address_public", server.PublicAddress.IP) + d.Set("state", server.State) + d.Set("state_detail", server.StateDetail) + + d.SetConnInfo(map[string]string{ + "type": "ssh", + "host": server.PublicAddress.IP, + }) + + return nil +} + +func resourceScalewayServerUpdate(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + + var req api.ScalewayServerPatchDefinition + + if d.HasChange("name") { + name := d.Get("name").(string) + req.Name = &name + } + + if d.HasChange("dynamic_ip_required") { + req.DynamicIPRequired = Bool(d.Get("dynamic_ip_required").(bool)) + } + + if err := scaleway.PatchServer(d.Id(), req); err != nil { + return fmt.Errorf("Failed patching scaleway server: %q", err) + } + + return resourceScalewayServerRead(d, m) +} + +func resourceScalewayServerDelete(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + + def, err := scaleway.GetServer(d.Id()) + if err != nil { + if serr, ok := err.(api.ScalewayAPIError); ok { + if serr.StatusCode == 404 { + d.SetId("") + return nil + } + } + return err + } + + err = deleteServerSafe(scaleway, def.Identifier) + if err != nil { + return err + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/scaleway/resource_scaleway_server_test.go b/builtin/providers/scaleway/resource_scaleway_server_test.go new file mode 100644 index 000000000..b8fa3ff48 --- /dev/null +++ b/builtin/providers/scaleway/resource_scaleway_server_test.go @@ -0,0 +1,113 @@ +package scaleway + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccScalewayServer_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckScalewayServerDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckScalewayServerConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckScalewayServerExists("scaleway_server.base"), + testAccCheckScalewayServerAttributes("scaleway_server.base"), + resource.TestCheckResourceAttr( + "scaleway_server.base", "type", "C1"), + resource.TestCheckResourceAttr( + "scaleway_server.base", "name", "test"), + ), + }, + }, + }) +} + +func testAccCheckScalewayServerDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*Client).scaleway + + for _, rs := range s.RootModule().Resources { + if rs.Type != "scaleway" { + continue + } + + _, err := client.GetServer(rs.Primary.ID) + + if err == nil { + return fmt.Errorf("Server still exists") + } + } + + return nil +} + +func testAccCheckScalewayServerAttributes(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Unknown resource: %s", n) + } + + client := testAccProvider.Meta().(*Client).scaleway + server, err := client.GetServer(rs.Primary.ID) + + if err != nil { + return err + } + + if server.Name != "test" { + return fmt.Errorf("Server has wrong name") + } + if server.Image.Identifier != armImageIdentifier { + return fmt.Errorf("Wrong server image") + } + if server.CommercialType != "C1" { + return fmt.Errorf("Wrong server type") + } + + return nil + } +} + +func testAccCheckScalewayServerExists(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) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Server ID is set") + } + + client := testAccProvider.Meta().(*Client).scaleway + server, err := client.GetServer(rs.Primary.ID) + + if err != nil { + return err + } + + if server.Identifier != rs.Primary.ID { + return fmt.Errorf("Record not found") + } + + return nil + } +} + +var armImageIdentifier = "5faef9cd-ea9b-4a63-9171-9e26bec03dbc" + +var testAccCheckScalewayServerConfig = fmt.Sprintf(` +resource "scaleway_server" "base" { + name = "test" + # ubuntu 14.04 + image = "%s" + type = "C1" +}`, armImageIdentifier) diff --git a/builtin/providers/scaleway/resource_scaleway_volume.go b/builtin/providers/scaleway/resource_scaleway_volume.go new file mode 100644 index 000000000..6090a52cc --- /dev/null +++ b/builtin/providers/scaleway/resource_scaleway_volume.go @@ -0,0 +1,127 @@ +package scaleway + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/scaleway/scaleway-cli/pkg/api" +) + +const gb uint64 = 1000 * 1000 * 1000 + +func resourceScalewayVolume() *schema.Resource { + return &schema.Resource{ + Create: resourceScalewayVolumeCreate, + Read: resourceScalewayVolumeRead, + Update: resourceScalewayVolumeUpdate, + Delete: resourceScalewayVolumeDelete, + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "size_in_gb": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(int) + if value < 1 || value > 150 { + errors = append(errors, fmt.Errorf("%q be more than 1 and less than 150", k)) + } + return + }, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if value != "l_ssd" { + errors = append(errors, fmt.Errorf("%q must be l_ssd", k)) + } + return + }, + }, + "server": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceScalewayVolumeCreate(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + size := uint64(d.Get("size_in_gb").(int)) * gb + req := api.ScalewayVolumeDefinition{ + Name: d.Get("name").(string), + Size: size, + Type: d.Get("type").(string), + Organization: scaleway.Organization, + } + volumeID, err := scaleway.PostVolume(req) + if err != nil { + return fmt.Errorf("Error Creating volume: %q", err) + } + d.SetId(volumeID) + return resourceScalewayVolumeRead(d, m) +} + +func resourceScalewayVolumeRead(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + volume, err := scaleway.GetVolume(d.Id()) + if err != nil { + if serr, ok := err.(api.ScalewayAPIError); ok { + log.Printf("[DEBUG] Error reading volume: %q\n", serr.APIMessage) + + if serr.StatusCode == 404 { + d.SetId("") + return nil + } + } + + return err + } + d.Set("name", volume.Name) + d.Set("size_in_gb", volume.Size/gb) + d.Set("type", volume.VolumeType) + d.Set("server", "") + if volume.Server != nil { + d.Set("server", volume.Server.Identifier) + } + return nil +} + +func resourceScalewayVolumeUpdate(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + + var req api.ScalewayVolumePutDefinition + if d.HasChange("name") { + req.Name = String(d.Get("name").(string)) + } + + if d.HasChange("size_in_gb") { + size := uint64(d.Get("size_in_gb").(int)) * gb + req.Size = &size + } + + scaleway.PutVolume(d.Id(), req) + return resourceScalewayVolumeRead(d, m) +} + +func resourceScalewayVolumeDelete(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + err := scaleway.DeleteVolume(d.Id()) + if err != nil { + if serr, ok := err.(api.ScalewayAPIError); ok { + if serr.StatusCode == 404 { + d.SetId("") + return nil + } + } + return err + } + d.SetId("") + return nil +} diff --git a/builtin/providers/scaleway/resource_scaleway_volume_attachment.go b/builtin/providers/scaleway/resource_scaleway_volume_attachment.go new file mode 100644 index 000000000..3b24172eb --- /dev/null +++ b/builtin/providers/scaleway/resource_scaleway_volume_attachment.go @@ -0,0 +1,201 @@ +package scaleway + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/scaleway/scaleway-cli/pkg/api" +) + +func resourceScalewayVolumeAttachment() *schema.Resource { + return &schema.Resource{ + Create: resourceScalewayVolumeAttachmentCreate, + Read: resourceScalewayVolumeAttachmentRead, + Delete: resourceScalewayVolumeAttachmentDelete, + Schema: map[string]*schema.Schema{ + "server": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "volume": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceScalewayVolumeAttachmentCreate(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + + var startServerAgain = false + server, err := scaleway.GetServer(d.Get("server").(string)) + if err != nil { + fmt.Printf("Failed getting server: %q", err) + return err + } + + // volumes can only be modified when the server is powered off + if server.State != "stopped" { + startServerAgain = true + + if err := scaleway.PostServerAction(server.Identifier, "poweroff"); err != nil { + return err + } + + if err := waitForServerState(scaleway, server.Identifier, "stopped"); err != nil { + return err + } + } + + volumes := make(map[string]api.ScalewayVolume) + for i, volume := range server.Volumes { + volumes[i] = volume + } + + vol, err := scaleway.GetVolume(d.Get("volume").(string)) + if err != nil { + return err + } + volumes[fmt.Sprintf("%d", len(volumes)+1)] = *vol + + // the API request requires most volume attributes to be unset to succeed + for k, v := range volumes { + v.Size = 0 + v.CreationDate = "" + v.Organization = "" + v.ModificationDate = "" + v.VolumeType = "" + v.Server = nil + v.ExportURI = "" + + volumes[k] = v + } + + var req = api.ScalewayServerPatchDefinition{ + Volumes: &volumes, + } + if err := scaleway.PatchServer(d.Get("server").(string), req); err != nil { + return fmt.Errorf("Failed attaching volume to server: %q", err) + } + + if startServerAgain { + if err := scaleway.PostServerAction(d.Get("server").(string), "poweron"); err != nil { + return err + } + + if err := waitForServerState(scaleway, d.Get("server").(string), "running"); err != nil { + return err + } + } + + d.SetId(fmt.Sprintf("scaleway-server:%s/volume/%s", d.Get("server").(string), d.Get("volume").(string))) + + return resourceScalewayVolumeAttachmentRead(d, m) +} + +func resourceScalewayVolumeAttachmentRead(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + + server, err := scaleway.GetServer(d.Get("server").(string)) + if err != nil { + if serr, ok := err.(api.ScalewayAPIError); ok { + log.Printf("[DEBUG] Error reading server: %q\n", serr.APIMessage) + + if serr.StatusCode == 404 { + d.SetId("") + return nil + } + } + return err + } + + if _, err := scaleway.GetVolume(d.Get("volume").(string)); err != nil { + if serr, ok := err.(api.ScalewayAPIError); ok { + log.Printf("[DEBUG] Error reading volume: %q\n", serr.APIMessage) + + if serr.StatusCode == 404 { + d.SetId("") + return nil + } + } + return err + } + + for _, volume := range server.Volumes { + if volume.Identifier == d.Get("volume").(string) { + return nil + } + } + + log.Printf("[DEBUG] Volume %q not attached to server %q\n", d.Get("volume").(string), d.Get("server").(string)) + d.SetId("") + return nil +} + +func resourceScalewayVolumeAttachmentDelete(d *schema.ResourceData, m interface{}) error { + scaleway := m.(*Client).scaleway + var startServerAgain = false + + server, err := scaleway.GetServer(d.Get("server").(string)) + if err != nil { + return err + } + + // volumes can only be modified when the server is powered off + if server.State != "stopped" { + startServerAgain = true + + if err := scaleway.PostServerAction(server.Identifier, "poweroff"); err != nil { + return err + } + + if err := waitForServerState(scaleway, server.Identifier, "stopped"); err != nil { + return err + } + } + + volumes := make(map[string]api.ScalewayVolume) + for _, volume := range server.Volumes { + if volume.Identifier != d.Get("volume").(string) { + volumes[fmt.Sprintf("%d", len(volumes))] = volume + } + } + + // the API request requires most volume attributes to be unset to succeed + for k, v := range volumes { + v.Size = 0 + v.CreationDate = "" + v.Organization = "" + v.ModificationDate = "" + v.VolumeType = "" + v.Server = nil + v.ExportURI = "" + + volumes[k] = v + } + + var req = api.ScalewayServerPatchDefinition{ + Volumes: &volumes, + } + if err := scaleway.PatchServer(d.Get("server").(string), req); err != nil { + return err + } + + if startServerAgain { + if err := scaleway.PostServerAction(d.Get("server").(string), "poweron"); err != nil { + return err + } + + if err := waitForServerState(scaleway, d.Get("server").(string), "running"); err != nil { + return err + } + } + + d.SetId("") + + return nil +} diff --git a/builtin/providers/scaleway/resource_scaleway_volume_attachment_test.go b/builtin/providers/scaleway/resource_scaleway_volume_attachment_test.go new file mode 100644 index 000000000..33d54f9f9 --- /dev/null +++ b/builtin/providers/scaleway/resource_scaleway_volume_attachment_test.go @@ -0,0 +1,93 @@ +package scaleway + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccScalewayVolumeAttachment_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckScalewayVolumeAttachmentDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckScalewayVolumeAttachmentConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckScalewayVolumeAttachmentExists("scaleway_volume_attachment.test"), + ), + }, + }, + }) +} + +func testAccCheckScalewayVolumeAttachmentDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*Client).scaleway + + for _, rs := range s.RootModule().Resources { + if rs.Type != "scaleway" { + continue + } + + s, err := client.GetServer(rs.Primary.Attributes["server"]) + if err != nil { + fmt.Printf("Failed getting server: %q", err) + return err + } + + for _, volume := range s.Volumes { + if volume.Identifier == rs.Primary.Attributes["volume"] { + return fmt.Errorf("Attachment still exists") + } + } + } + + return nil +} + +func testAccCheckScalewayVolumeAttachmentExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*Client).scaleway + + rs, _ := s.RootModule().Resources[n] + + server, err := client.GetServer(rs.Primary.Attributes["server"]) + if err != nil { + fmt.Printf("Failed getting server: %q", err) + return err + } + + for _, volume := range server.Volumes { + if volume.Identifier == rs.Primary.Attributes["volume"] { + return nil + } + } + + return fmt.Errorf("Attachment does not exist") + } +} + +var x86_64ImageIdentifier = "aecaed73-51a5-4439-a127-6d8229847145" + +var testAccCheckScalewayVolumeAttachmentConfig = fmt.Sprintf(` +resource "scaleway_server" "base" { + name = "test" + # ubuntu 14.04 + image = "%s" + type = "C2S" + # state = "stopped" +} + +resource "scaleway_volume" "test" { + name = "test" + size_in_gb = 20 + type = "l_ssd" +} + +resource "scaleway_volume_attachment" "test" { + server = "${scaleway_server.base.id}" + volume = "${scaleway_volume.test.id}" +}`, x86_64ImageIdentifier) diff --git a/builtin/providers/scaleway/resource_scaleway_volume_test.go b/builtin/providers/scaleway/resource_scaleway_volume_test.go new file mode 100644 index 000000000..fa3feb062 --- /dev/null +++ b/builtin/providers/scaleway/resource_scaleway_volume_test.go @@ -0,0 +1,107 @@ +package scaleway + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccScalewayVolume_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckScalewayVolumeDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckScalewayVolumeConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckScalewayVolumeExists("scaleway_volume.test"), + testAccCheckScalewayVolumeAttributes("scaleway_volume.test"), + ), + }, + }, + }) +} + +func testAccCheckScalewayVolumeDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*Client).scaleway + + for _, rs := range s.RootModule().Resources { + if rs.Type != "scaleway" { + continue + } + + _, err := client.GetVolume(rs.Primary.ID) + + if err == nil { + return fmt.Errorf("Volume still exists") + } + } + + return nil +} + +func testAccCheckScalewayVolumeAttributes(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Unknown resource: %s", n) + } + + client := testAccProvider.Meta().(*Client).scaleway + volume, err := client.GetVolume(rs.Primary.ID) + + if err != nil { + return err + } + + if volume.Name != "test" { + return fmt.Errorf("volume has wrong name: %q", volume.Name) + } + if volume.Size != 2000000000 { + return fmt.Errorf("volume has wrong size: %d", volume.Size) + } + if volume.VolumeType != "l_ssd" { + return fmt.Errorf("volume has volume type: %q", volume.VolumeType) + } + + return nil + } +} + +func testAccCheckScalewayVolumeExists(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) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Volume ID is set") + } + + client := testAccProvider.Meta().(*Client).scaleway + volume, err := client.GetVolume(rs.Primary.ID) + + if err != nil { + return err + } + + if volume.Identifier != rs.Primary.ID { + return fmt.Errorf("Record not found") + } + + return nil + } +} + +var testAccCheckScalewayVolumeConfig = ` +resource "scaleway_volume" "test" { + name = "test" + size_in_gb = 2 + type = "l_ssd" +} +` diff --git a/command/internal_plugin_list.go b/command/internal_plugin_list.go index 4ce91b409..5bb36224a 100644 --- a/command/internal_plugin_list.go +++ b/command/internal_plugin_list.go @@ -39,6 +39,7 @@ import ( powerdnsprovider "github.com/hashicorp/terraform/builtin/providers/powerdns" randomprovider "github.com/hashicorp/terraform/builtin/providers/random" rundeckprovider "github.com/hashicorp/terraform/builtin/providers/rundeck" + scalewayprovider "github.com/hashicorp/terraform/builtin/providers/scaleway" softlayerprovider "github.com/hashicorp/terraform/builtin/providers/softlayer" statuscakeprovider "github.com/hashicorp/terraform/builtin/providers/statuscake" templateprovider "github.com/hashicorp/terraform/builtin/providers/template" @@ -92,6 +93,7 @@ var InternalProviders = map[string]plugin.ProviderFunc{ "powerdns": powerdnsprovider.Provider, "random": randomprovider.Provider, "rundeck": rundeckprovider.Provider, + "scaleway": scalewayprovider.Provider, "softlayer": softlayerprovider.Provider, "statuscake": statuscakeprovider.Provider, "template": templateprovider.Provider, diff --git a/vendor/github.com/moul/anonuuid/LICENSE b/vendor/github.com/moul/anonuuid/LICENSE new file mode 100644 index 000000000..492e2c629 --- /dev/null +++ b/vendor/github.com/moul/anonuuid/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Manfred Touron + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/github.com/moul/anonuuid/README.md b/vendor/github.com/moul/anonuuid/README.md new file mode 100644 index 000000000..f5c9a57eb --- /dev/null +++ b/vendor/github.com/moul/anonuuid/README.md @@ -0,0 +1,170 @@ +# AnonUUID + +[![Build Status](https://travis-ci.org/moul/anonuuid.svg)](https://travis-ci.org/moul/anonuuid) +[![GoDoc](https://godoc.org/github.com/moul/anonuuid?status.svg)](https://godoc.org/github.com/moul/anonuuid) +[![Coverage Status](https://coveralls.io/repos/moul/anonuuid/badge.svg?branch=master&service=github)](https://coveralls.io/github/moul/anonuuid?branch=master) + +:wrench: Anonymize UUIDs outputs (written in Golang) + +![AnonUUID Logo](https://raw.githubusercontent.com/moul/anonuuid/master/assets/anonuuid.png) + +**anonuuid** anonymize an input string by replacing all UUIDs by an anonymized +new one. + +The fake UUIDs are cached, so if AnonUUID encounter the same real UUIDs multiple +times, the translation will be the same. + +## Usage + +```console +$ anonuuid --help +NAME: + anonuuid - Anonymize UUIDs outputs + +USAGE: + anonuuid [global options] command [command options] [arguments...] + +VERSION: + 1.0.0-dev + +AUTHOR(S): + Manfred Touron + +COMMANDS: + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --hexspeak Generate hexspeak style fake UUIDs + --random, -r Generate random fake UUIDs + --keep-beginning Keep first part of the UUID unchanged + --keep-end Keep last part of the UUID unchanged + --prefix, -p Prefix generated UUIDs + --suffix Suffix generated UUIDs + --help, -h show help + --version, -v print the version + ``` + +## Example + +Replace all UUIDs and cache the correspondance. + +```command +$ anonuuid git:(master) ✗ cat < 32 { + part = part[:32] + } + uuid := part[:8] + "-" + part[8:12] + "-1" + part[13:16] + "-" + part[16:20] + "-" + part[20:32] + + err := IsUUID(uuid) + if err != nil { + return "", err + } + + return uuid, nil +} + +// GenerateRandomUUID returns an UUID based on random strings +func GenerateRandomUUID(length int) (string, error) { + var letters = []rune("abcdef0123456789") + + b := make([]rune, length) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return FormatUUID(string(b)) +} + +// GenerateHexspeakUUID returns an UUID formatted string containing hexspeak words +func GenerateHexspeakUUID(i int) (string, error) { + if i < 0 { + i = -i + } + hexspeaks := []string{ + "0ff1ce", + "31337", + "4b1d", + "badc0de", + "badcafe", + "badf00d", + "deadbabe", + "deadbeef", + "deadc0de", + "deadfeed", + "fee1bad", + } + return FormatUUID(hexspeaks[i%len(hexspeaks)]) +} + +// GenerateLenUUID returns an UUID formatted string based on an index number +func GenerateLenUUID(i int) (string, error) { + if i < 0 { + i = 2<<29 + i + } + return FormatUUID(fmt.Sprintf("%x", i)) +} diff --git a/vendor/github.com/renstrom/fuzzysearch/LICENSE b/vendor/github.com/renstrom/fuzzysearch/LICENSE new file mode 100644 index 000000000..9cc753370 --- /dev/null +++ b/vendor/github.com/renstrom/fuzzysearch/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Peter Renström + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go b/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go new file mode 100644 index 000000000..63277d51e --- /dev/null +++ b/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go @@ -0,0 +1,167 @@ +// Fuzzy searching allows for flexibly matching a string with partial input, +// useful for filtering data very quickly based on lightweight user input. +package fuzzy + +import ( + "unicode" + "unicode/utf8" +) + +var noop = func(r rune) rune { return r } + +// Match returns true if source matches target using a fuzzy-searching +// algorithm. Note that it doesn't implement Levenshtein distance (see +// RankMatch instead), but rather a simplified version where there's no +// approximation. The method will return true only if each character in the +// source can be found in the target and occurs after the preceding matches. +func Match(source, target string) bool { + return match(source, target, noop) +} + +// MatchFold is a case-insensitive version of Match. +func MatchFold(source, target string) bool { + return match(source, target, unicode.ToLower) +} + +func match(source, target string, fn func(rune) rune) bool { + lenDiff := len(target) - len(source) + + if lenDiff < 0 { + return false + } + + if lenDiff == 0 && source == target { + return true + } + +Outer: + for _, r1 := range source { + for i, r2 := range target { + if fn(r1) == fn(r2) { + target = target[i+utf8.RuneLen(r2):] + continue Outer + } + } + return false + } + + return true +} + +// Find will return a list of strings in targets that fuzzy matches source. +func Find(source string, targets []string) []string { + return find(source, targets, noop) +} + +// FindFold is a case-insensitive version of Find. +func FindFold(source string, targets []string) []string { + return find(source, targets, unicode.ToLower) +} + +func find(source string, targets []string, fn func(rune) rune) []string { + var matches []string + + for _, target := range targets { + if match(source, target, fn) { + matches = append(matches, target) + } + } + + return matches +} + +// RankMatch is similar to Match except it will measure the Levenshtein +// distance between the source and the target and return its result. If there +// was no match, it will return -1. +// Given the requirements of match, RankMatch only needs to perform a subset of +// the Levenshtein calculation, only deletions need be considered, required +// additions and substitutions would fail the match test. +func RankMatch(source, target string) int { + return rank(source, target, noop) +} + +// RankMatchFold is a case-insensitive version of RankMatch. +func RankMatchFold(source, target string) int { + return rank(source, target, unicode.ToLower) +} + +func rank(source, target string, fn func(rune) rune) int { + lenDiff := len(target) - len(source) + + if lenDiff < 0 { + return -1 + } + + if lenDiff == 0 && source == target { + return 0 + } + + runeDiff := 0 + +Outer: + for _, r1 := range source { + for i, r2 := range target { + if fn(r1) == fn(r2) { + target = target[i+utf8.RuneLen(r2):] + continue Outer + } else { + runeDiff++ + } + } + return -1 + } + + // Count up remaining char + for len(target) > 0 { + target = target[utf8.RuneLen(rune(target[0])):] + runeDiff++ + } + + return runeDiff +} + +// RankFind is similar to Find, except it will also rank all matches using +// Levenshtein distance. +func RankFind(source string, targets []string) Ranks { + var r Ranks + for _, target := range find(source, targets, noop) { + distance := LevenshteinDistance(source, target) + r = append(r, Rank{source, target, distance}) + } + return r +} + +// RankFindFold is a case-insensitive version of RankFind. +func RankFindFold(source string, targets []string) Ranks { + var r Ranks + for _, target := range find(source, targets, unicode.ToLower) { + distance := LevenshteinDistance(source, target) + r = append(r, Rank{source, target, distance}) + } + return r +} + +type Rank struct { + // Source is used as the source for matching. + Source string + + // Target is the word matched against. + Target string + + // Distance is the Levenshtein distance between Source and Target. + Distance int +} + +type Ranks []Rank + +func (r Ranks) Len() int { + return len(r) +} + +func (r Ranks) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + +func (r Ranks) Less(i, j int) bool { + return r[i].Distance < r[j].Distance +} diff --git a/vendor/github.com/renstrom/fuzzysearch/fuzzy/levenshtein.go b/vendor/github.com/renstrom/fuzzysearch/fuzzy/levenshtein.go new file mode 100644 index 000000000..237923d34 --- /dev/null +++ b/vendor/github.com/renstrom/fuzzysearch/fuzzy/levenshtein.go @@ -0,0 +1,43 @@ +package fuzzy + +// LevenshteinDistance measures the difference between two strings. +// The Levenshtein distance between two words is the minimum number of +// single-character edits (i.e. insertions, deletions or substitutions) +// required to change one word into the other. +// +// This implemention is optimized to use O(min(m,n)) space and is based on the +// optimized C version found here: +// http://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Levenshtein_distance#C +func LevenshteinDistance(s, t string) int { + r1, r2 := []rune(s), []rune(t) + column := make([]int, len(r1)+1) + + for y := 1; y <= len(r1); y++ { + column[y] = y + } + + for x := 1; x <= len(r2); x++ { + column[0] = x + + for y, lastDiag := 1, x-1; y <= len(r1); y++ { + oldDiag := column[y] + cost := 0 + if r1[y-1] != r2[x-1] { + cost = 1 + } + column[y] = min(column[y]+1, column[y-1]+1, lastDiag+cost) + lastDiag = oldDiag + } + } + + return column[len(r1)] +} + +func min(a, b, c int) int { + if a < b && a < c { + return a + } else if b < c { + return b + } + return c +} diff --git a/vendor/github.com/scaleway/scaleway-cli/LICENSE.md b/vendor/github.com/scaleway/scaleway-cli/LICENSE.md new file mode 100644 index 000000000..7503a16ca --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-cli/LICENSE.md @@ -0,0 +1,22 @@ +The MIT License +=============== + +Copyright (c) **2014-2016 Scaleway ([@scaleway](https://twitter.com/scaleway))** + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/scaleway/scaleway-cli/pkg/api/README.md b/vendor/github.com/scaleway/scaleway-cli/pkg/api/README.md new file mode 100644 index 000000000..559a7018d --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-cli/pkg/api/README.md @@ -0,0 +1,25 @@ +# Scaleway's API + +[![GoDoc](https://godoc.org/github.com/scaleway/scaleway-cli/pkg/api?status.svg)](https://godoc.org/github.com/scaleway/scaleway-cli/pkg/api) + +This package contains facilities to play with the Scaleway API, it includes the following features: + +- dedicated configuration file containing credentials to deal with the API +- caching to resolve UUIDs without contacting the API + +## Links + +- [API documentation](https://developer.scaleway.com) +- [Official Python SDK](https://github.com/scaleway/python-scaleway) +- Projects using this SDK + - https://github.com/scaleway/devhub + - https://github.com/scaleway/docker-machine-driver-scaleway + - https://github.com/scaleway-community/scaleway-ubuntu-coreos/blob/master/overlay/usr/local/update-firewall/scw-api/cache.go + - https://github.com/pulcy/quark + - https://github.com/hex-sh/terraform-provider-scaleway + - https://github.com/tscolari/bosh-scaleway-cpi +- Other **golang** clients + - https://github.com/lalyos/onlabs + - https://github.com/meatballhat/packer-builder-onlinelabs + - https://github.com/nlamirault/go-scaleway + - https://github.com/golang/build/blob/master/cmd/scaleway/scaleway.go diff --git a/vendor/github.com/scaleway/scaleway-cli/pkg/api/api.go b/vendor/github.com/scaleway/scaleway-cli/pkg/api/api.go new file mode 100644 index 000000000..cd3a81e28 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-cli/pkg/api/api.go @@ -0,0 +1,2667 @@ +// Copyright (C) 2015 Scaleway. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE.md file. + +// Interact with Scaleway API + +// Package api contains client and functions to interact with Scaleway API +package api + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "sort" + "strings" + "text/tabwriter" + "text/template" + "time" +) + +// Default values +var ( + ComputeAPI = "https://api.scaleway.com/" + AccountAPI = "https://account.scaleway.com/" + MetadataAPI = "http://169.254.42.42/" + MarketplaceAPI = "https://api-marketplace.scaleway.com" +) + +func init() { + if url := os.Getenv("SCW_COMPUTE_API"); url != "" { + ComputeAPI = url + } + if url := os.Getenv("SCW_ACCOUNT_API"); url != "" { + AccountAPI = url + } + if url := os.Getenv("SCW_METADATA_API"); url != "" { + MetadataAPI = url + } + if url := os.Getenv("SCW_MARKETPLACE_API"); url != "" { + MarketplaceAPI = url + } +} + +// ScalewayAPI is the interface used to communicate with the Scaleway API +type ScalewayAPI struct { + // Organization is the identifier of the Scaleway organization + Organization string + + // Token is the authentication token for the Scaleway organization + Token string + + // Password is the authentication password + password string + + userAgent string + + // Cache is used to quickly resolve identifiers from names + Cache *ScalewayCache + + client *http.Client + verbose bool + + // + Logger +} + +// ScalewayAPIError represents a Scaleway API Error +type ScalewayAPIError struct { + // Message is a human-friendly error message + APIMessage string `json:"message,omitempty"` + + // Type is a string code that defines the kind of error + Type string `json:"type,omitempty"` + + // Fields contains detail about validation error + Fields map[string][]string `json:"fields,omitempty"` + + // StatusCode is the HTTP status code received + StatusCode int `json:"-"` + + // Message + Message string `json:"-"` +} + +// Error returns a string representing the error +func (e ScalewayAPIError) Error() string { + var b bytes.Buffer + for k, v := range map[string]interface{}{ + "StatusCode": e.StatusCode, + "Type": e.Type, + "Message": e.Message, + "APIMessage": e.APIMessage, + } { + fmt.Fprintf(&b, " %-30s %s", fmt.Sprintf("%s: ", k), v) + } + return b.String() +} + +// HideAPICredentials removes API credentials from a string +func (s *ScalewayAPI) HideAPICredentials(input string) string { + output := input + if s.Token != "" { + output = strings.Replace(output, s.Token, "00000000-0000-4000-8000-000000000000", -1) + } + if s.Organization != "" { + output = strings.Replace(output, s.Organization, "00000000-0000-5000-9000-000000000000", -1) + } + if s.password != "" { + output = strings.Replace(output, s.password, "XX-XX-XX-XX", -1) + } + return output +} + +// ScalewayIPAddress represents a Scaleway IP address +type ScalewayIPAddress struct { + // Identifier is a unique identifier for the IP address + Identifier string `json:"id,omitempty"` + + // IP is an IPv4 address + IP string `json:"address,omitempty"` + + // Dynamic is a flag that defines an IP that change on each reboot + Dynamic *bool `json:"dynamic,omitempty"` +} + +// ScalewayVolume represents a Scaleway Volume +type ScalewayVolume struct { + // Identifier is a unique identifier for the volume + Identifier string `json:"id,omitempty"` + + // Size is the allocated size of the volume + Size uint64 `json:"size,omitempty"` + + // CreationDate is the creation date of the volume + CreationDate string `json:"creation_date,omitempty"` + + // ModificationDate is the date of the last modification of the volume + ModificationDate string `json:"modification_date,omitempty"` + + // Organization is the organization owning the volume + Organization string `json:"organization,omitempty"` + + // Name is the name of the volume + Name string `json:"name,omitempty"` + + // Server is the server using this image + Server *struct { + Identifier string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + } `json:"server,omitempty"` + + // VolumeType is a Scaleway identifier for the kind of volume (default: l_ssd) + VolumeType string `json:"volume_type,omitempty"` + + // ExportURI represents the url used by initrd/scripts to attach the volume + ExportURI string `json:"export_uri,omitempty"` +} + +// ScalewayOneVolume represents the response of a GET /volumes/UUID API call +type ScalewayOneVolume struct { + Volume ScalewayVolume `json:"volume,omitempty"` +} + +// ScalewayVolumes represents a group of Scaleway volumes +type ScalewayVolumes struct { + // Volumes holds scaleway volumes of the response + Volumes []ScalewayVolume `json:"volumes,omitempty"` +} + +// ScalewayVolumeDefinition represents a Scaleway volume definition +type ScalewayVolumeDefinition struct { + // Name is the user-defined name of the volume + Name string `json:"name"` + + // Image is the image used by the volume + Size uint64 `json:"size"` + + // Bootscript is the bootscript used by the volume + Type string `json:"volume_type"` + + // Organization is the owner of the volume + Organization string `json:"organization"` +} + +// ScalewayVolumePutDefinition represents a Scaleway volume with nullable fields (for PUT) +type ScalewayVolumePutDefinition struct { + Identifier *string `json:"id,omitempty"` + Size *uint64 `json:"size,omitempty"` + CreationDate *string `json:"creation_date,omitempty"` + ModificationDate *string `json:"modification_date,omitempty"` + Organization *string `json:"organization,omitempty"` + Name *string `json:"name,omitempty"` + Server struct { + Identifier *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"server,omitempty"` + VolumeType *string `json:"volume_type,omitempty"` + ExportURI *string `json:"export_uri,omitempty"` +} + +// ScalewayImage represents a Scaleway Image +type ScalewayImage struct { + // Identifier is a unique identifier for the image + Identifier string `json:"id,omitempty"` + + // Name is a user-defined name for the image + Name string `json:"name,omitempty"` + + // CreationDate is the creation date of the image + CreationDate string `json:"creation_date,omitempty"` + + // ModificationDate is the date of the last modification of the image + ModificationDate string `json:"modification_date,omitempty"` + + // RootVolume is the root volume bound to the image + RootVolume ScalewayVolume `json:"root_volume,omitempty"` + + // Public is true for public images and false for user images + Public bool `json:"public,omitempty"` + + // Bootscript is the bootscript bound to the image + DefaultBootscript *ScalewayBootscript `json:"default_bootscript,omitempty"` + + // Organization is the owner of the image + Organization string `json:"organization,omitempty"` + + // Arch is the architecture target of the image + Arch string `json:"arch,omitempty"` + + // FIXME: extra_volumes +} + +// ScalewayImageIdentifier represents a Scaleway Image Identifier +type ScalewayImageIdentifier struct { + Identifier string + Arch string + Region string + Owner string +} + +// ScalewayOneImage represents the response of a GET /images/UUID API call +type ScalewayOneImage struct { + Image ScalewayImage `json:"image,omitempty"` +} + +// ScalewayImages represents a group of Scaleway images +type ScalewayImages struct { + // Images holds scaleway images of the response + Images []ScalewayImage `json:"images,omitempty"` +} + +// ScalewaySnapshot represents a Scaleway Snapshot +type ScalewaySnapshot struct { + // Identifier is a unique identifier for the snapshot + Identifier string `json:"id,omitempty"` + + // Name is a user-defined name for the snapshot + Name string `json:"name,omitempty"` + + // CreationDate is the creation date of the snapshot + CreationDate string `json:"creation_date,omitempty"` + + // ModificationDate is the date of the last modification of the snapshot + ModificationDate string `json:"modification_date,omitempty"` + + // Size is the allocated size of the volume + Size uint64 `json:"size,omitempty"` + + // Organization is the owner of the snapshot + Organization string `json:"organization"` + + // State is the current state of the snapshot + State string `json:"state"` + + // VolumeType is the kind of volume behind the snapshot + VolumeType string `json:"volume_type"` + + // BaseVolume is the volume from which the snapshot inherits + BaseVolume ScalewayVolume `json:"base_volume,omitempty"` +} + +// ScalewayOneSnapshot represents the response of a GET /snapshots/UUID API call +type ScalewayOneSnapshot struct { + Snapshot ScalewaySnapshot `json:"snapshot,omitempty"` +} + +// ScalewaySnapshots represents a group of Scaleway snapshots +type ScalewaySnapshots struct { + // Snapshots holds scaleway snapshots of the response + Snapshots []ScalewaySnapshot `json:"snapshots,omitempty"` +} + +// ScalewayBootscript represents a Scaleway Bootscript +type ScalewayBootscript struct { + Bootcmdargs string `json:"bootcmdargs,omitempty"` + Dtb string `json:"dtb,omitempty"` + Initrd string `json:"initrd,omitempty"` + Kernel string `json:"kernel,omitempty"` + + // Arch is the architecture target of the bootscript + Arch string `json:"architecture,omitempty"` + + // Identifier is a unique identifier for the bootscript + Identifier string `json:"id,omitempty"` + + // Organization is the owner of the bootscript + Organization string `json:"organization,omitempty"` + + // Name is a user-defined name for the bootscript + Title string `json:"title,omitempty"` + + // Public is true for public bootscripts and false for user bootscripts + Public bool `json:"public,omitempty"` + + Default bool `json:"default,omitempty"` +} + +// ScalewayOneBootscript represents the response of a GET /bootscripts/UUID API call +type ScalewayOneBootscript struct { + Bootscript ScalewayBootscript `json:"bootscript,omitempty"` +} + +// ScalewayBootscripts represents a group of Scaleway bootscripts +type ScalewayBootscripts struct { + // Bootscripts holds Scaleway bootscripts of the response + Bootscripts []ScalewayBootscript `json:"bootscripts,omitempty"` +} + +// ScalewayTask represents a Scaleway Task +type ScalewayTask struct { + // Identifier is a unique identifier for the task + Identifier string `json:"id,omitempty"` + + // StartDate is the start date of the task + StartDate string `json:"started_at,omitempty"` + + // TerminationDate is the termination date of the task + TerminationDate string `json:"terminated_at,omitempty"` + + HrefFrom string `json:"href_from,omitempty"` + + Description string `json:"description,omitempty"` + + Status string `json:"status,omitempty"` + + Progress int `json:"progress,omitempty"` +} + +// ScalewayOneTask represents the response of a GET /tasks/UUID API call +type ScalewayOneTask struct { + Task ScalewayTask `json:"task,omitempty"` +} + +// ScalewayTasks represents a group of Scaleway tasks +type ScalewayTasks struct { + // Tasks holds scaleway tasks of the response + Tasks []ScalewayTask `json:"tasks,omitempty"` +} + +// ScalewaySecurityGroupRule definition +type ScalewaySecurityGroupRule struct { + Direction string `json:"direction"` + Protocol string `json:"protocol"` + IPRange string `json:"ip_range"` + DestPortFrom int `json:"dest_port_from,omitempty"` + Action string `json:"action"` + Postion int `json:"position"` + DestPortTo string `json:"dest_port_to"` + Editable bool `json:"editable"` + ID string `json:"id"` +} + +// ScalewayGetSecurityGroupRules represents the response of a GET /security_group/{groupID}/rules +type ScalewayGetSecurityGroupRules struct { + Rules []ScalewaySecurityGroupRule `json:"rules"` +} + +// ScalewayGetSecurityGroupRule represents the response of a GET /security_group/{groupID}/rules/{ruleID} +type ScalewayGetSecurityGroupRule struct { + Rules ScalewaySecurityGroupRule `json:"rule"` +} + +// ScalewayNewSecurityGroupRule definition POST/PUT request /security_group/{groupID} +type ScalewayNewSecurityGroupRule struct { + Action string `json:"action"` + Direction string `json:"direction"` + IPRange string `json:"ip_range"` + Protocol string `json:"protocol"` + DestPortFrom int `json:"dest_port_from,omitempty"` +} + +// ScalewaySecurityGroups definition +type ScalewaySecurityGroups struct { + Description string `json:"description"` + ID string `json:"id"` + Organization string `json:"organization"` + Name string `json:"name"` + Servers []ScalewaySecurityGroup `json:"servers"` + EnableDefaultSecurity bool `json:"enable_default_security"` + OrganizationDefault bool `json:"organization_default"` +} + +// ScalewayGetSecurityGroups represents the response of a GET /security_groups/ +type ScalewayGetSecurityGroups struct { + SecurityGroups []ScalewaySecurityGroups `json:"security_groups"` +} + +// ScalewayGetSecurityGroup represents the response of a GET /security_groups/{groupID} +type ScalewayGetSecurityGroup struct { + SecurityGroups ScalewaySecurityGroups `json:"security_group"` +} + +// ScalewayIPDefinition represents the IP's fields +type ScalewayIPDefinition struct { + Organization string `json:"organization"` + Reverse string `json:"reverse"` + ID string `json:"id"` + Server struct { + Identifier string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + } `json:"server,omitempty"` + Address string `json:"address"` +} + +// ScalewayGetIPS represents the response of a GET /ips/ +type ScalewayGetIPS struct { + IPS []ScalewayIPDefinition `json:"ips"` +} + +// ScalewayGetIP represents the response of a GET /ips/{id_ip} +type ScalewayGetIP struct { + IP ScalewayIPDefinition `json:"ip"` +} + +// ScalewaySecurityGroup represents a Scaleway security group +type ScalewaySecurityGroup struct { + // Identifier is a unique identifier for the security group + Identifier string `json:"id,omitempty"` + + // Name is the user-defined name of the security group + Name string `json:"name,omitempty"` +} + +// ScalewayNewSecurityGroup definition POST/PUT request /security_groups +type ScalewayNewSecurityGroup struct { + Organization string `json:"organization"` + Name string `json:"name"` + Description string `json:"description"` +} + +// ScalewayServer represents a Scaleway server +type ScalewayServer struct { + // Arch is the architecture target of the server + Arch string `json:"arch,omitempty"` + + // Identifier is a unique identifier for the server + Identifier string `json:"id,omitempty"` + + // Name is the user-defined name of the server + Name string `json:"name,omitempty"` + + // CreationDate is the creation date of the server + CreationDate string `json:"creation_date,omitempty"` + + // ModificationDate is the date of the last modification of the server + ModificationDate string `json:"modification_date,omitempty"` + + // Image is the image used by the server + Image ScalewayImage `json:"image,omitempty"` + + // DynamicIPRequired is a flag that defines a server with a dynamic ip address attached + DynamicIPRequired *bool `json:"dynamic_ip_required,omitempty"` + + // PublicIP is the public IP address bound to the server + PublicAddress ScalewayIPAddress `json:"public_ip,omitempty"` + + // State is the current status of the server + State string `json:"state,omitempty"` + + // StateDetail is the detailed status of the server + StateDetail string `json:"state_detail,omitempty"` + + // PrivateIP represents the private IPV4 attached to the server (changes on each boot) + PrivateIP string `json:"private_ip,omitempty"` + + // Bootscript is the unique identifier of the selected bootscript + Bootscript *ScalewayBootscript `json:"bootscript,omitempty"` + + // Hostname represents the ServerName in a format compatible with unix's hostname + Hostname string `json:"hostname,omitempty"` + + // Tags represents user-defined tags + Tags []string `json:"tags,omitempty"` + + // Volumes are the attached volumes + Volumes map[string]ScalewayVolume `json:"volumes,omitempty"` + + // SecurityGroup is the selected security group object + SecurityGroup ScalewaySecurityGroup `json:"security_group,omitempty"` + + // Organization is the owner of the server + Organization string `json:"organization,omitempty"` + + // CommercialType is the commercial type of the server (i.e: C1, C2[SML], VC1S) + CommercialType string `json:"commercial_type,omitempty"` + + // Location of the server + Location struct { + Platform string `json:"platform_id,omitempty"` + Chassis string `json:"chassis_id,omitempty"` + Cluster string `json:"cluster_id,omitempty"` + Hypervisor string `json:"hypervisor_id,omitempty"` + Blade string `json:"blade_id,omitempty"` + Node string `json:"node_id,omitempty"` + } `json:"location,omitempty"` + + IPV6 *ScalewayIPV6Definition `json:"ipv6,omitempty"` + + EnableIPV6 bool `json:"enable_ipv6,omitempty"` +} + +// ScalewayIPV6Definition represents a Scaleway ipv6 +type ScalewayIPV6Definition struct { + Netmask string `json:"netmask"` + Gateway string `json:"gateway"` + Address string `json:"address"` +} + +// ScalewayServerPatchDefinition represents a Scaleway server with nullable fields (for PATCH) +type ScalewayServerPatchDefinition struct { + Arch *string `json:"arch,omitempty"` + Name *string `json:"name,omitempty"` + CreationDate *string `json:"creation_date,omitempty"` + ModificationDate *string `json:"modification_date,omitempty"` + Image *ScalewayImage `json:"image,omitempty"` + DynamicIPRequired *bool `json:"dynamic_ip_required,omitempty"` + PublicAddress *ScalewayIPAddress `json:"public_ip,omitempty"` + State *string `json:"state,omitempty"` + StateDetail *string `json:"state_detail,omitempty"` + PrivateIP *string `json:"private_ip,omitempty"` + Bootscript *string `json:"bootscript,omitempty"` + Hostname *string `json:"hostname,omitempty"` + Volumes *map[string]ScalewayVolume `json:"volumes,omitempty"` + SecurityGroup *ScalewaySecurityGroup `json:"security_group,omitempty"` + Organization *string `json:"organization,omitempty"` + Tags *[]string `json:"tags,omitempty"` + IPV6 *ScalewayIPV6Definition `json:"ipv6,omitempty"` + EnableIPV6 *bool `json:"enable_ipv6,omitempty"` +} + +// ScalewayServerDefinition represents a Scaleway server with image definition +type ScalewayServerDefinition struct { + // Name is the user-defined name of the server + Name string `json:"name"` + + // Image is the image used by the server + Image *string `json:"image,omitempty"` + + // Volumes are the attached volumes + Volumes map[string]string `json:"volumes,omitempty"` + + // DynamicIPRequired is a flag that defines a server with a dynamic ip address attached + DynamicIPRequired *bool `json:"dynamic_ip_required,omitempty"` + + // Bootscript is the bootscript used by the server + Bootscript *string `json:"bootscript"` + + // Tags are the metadata tags attached to the server + Tags []string `json:"tags,omitempty"` + + // Organization is the owner of the server + Organization string `json:"organization"` + + // CommercialType is the commercial type of the server (i.e: C1, C2[SML], VC1S) + CommercialType string `json:"commercial_type"` + + PublicIP string `json:"public_ip,omitempty"` + + EnableIPV6 bool `json:"enable_ipv6,omitempty"` +} + +// ScalewayOneServer represents the response of a GET /servers/UUID API call +type ScalewayOneServer struct { + Server ScalewayServer `json:"server,omitempty"` +} + +// ScalewayServers represents a group of Scaleway servers +type ScalewayServers struct { + // Servers holds scaleway servers of the response + Servers []ScalewayServer `json:"servers,omitempty"` +} + +// ScalewayServerAction represents an action to perform on a Scaleway server +type ScalewayServerAction struct { + // Action is the name of the action to trigger + Action string `json:"action,omitempty"` +} + +// ScalewaySnapshotDefinition represents a Scaleway snapshot definition +type ScalewaySnapshotDefinition struct { + VolumeIDentifier string `json:"volume_id"` + Name string `json:"name,omitempty"` + Organization string `json:"organization"` +} + +// ScalewayImageDefinition represents a Scaleway image definition +type ScalewayImageDefinition struct { + SnapshotIDentifier string `json:"root_volume"` + Name string `json:"name,omitempty"` + Organization string `json:"organization"` + Arch string `json:"arch"` + DefaultBootscript *string `json:"default_bootscript,omitempty"` +} + +// ScalewayRoleDefinition represents a Scaleway Token UserId Role +type ScalewayRoleDefinition struct { + Organization ScalewayOrganizationDefinition `json:"organization,omitempty"` + Role string `json:"role,omitempty"` +} + +// ScalewayTokenDefinition represents a Scaleway Token +type ScalewayTokenDefinition struct { + UserID string `json:"user_id"` + Description string `json:"description,omitempty"` + Roles ScalewayRoleDefinition `json:"roles"` + Expires string `json:"expires"` + InheritsUsersPerms bool `json:"inherits_user_perms"` + ID string `json:"id"` +} + +// ScalewayTokensDefinition represents a Scaleway Tokens +type ScalewayTokensDefinition struct { + Token ScalewayTokenDefinition `json:"token"` +} + +// ScalewayContainerData represents a Scaleway container data (S3) +type ScalewayContainerData struct { + LastModified string `json:"last_modified"` + Name string `json:"name"` + Size string `json:"size"` +} + +// ScalewayGetContainerDatas represents a list of Scaleway containers data (S3) +type ScalewayGetContainerDatas struct { + Container []ScalewayContainerData `json:"container"` +} + +// ScalewayContainer represents a Scaleway container (S3) +type ScalewayContainer struct { + ScalewayOrganizationDefinition `json:"organization"` + Name string `json:"name"` + Size string `json:"size"` +} + +// ScalewayGetContainers represents a list of Scaleway containers (S3) +type ScalewayGetContainers struct { + Containers []ScalewayContainer `json:"containers"` +} + +// ScalewayConnectResponse represents the answer from POST /tokens +type ScalewayConnectResponse struct { + Token ScalewayTokenDefinition `json:"token"` +} + +// ScalewayConnect represents the data to connect +type ScalewayConnect struct { + Email string `json:"email"` + Password string `json:"password"` + Description string `json:"description"` + Expires bool `json:"expires"` +} + +// ScalewayOrganizationDefinition represents a Scaleway Organization +type ScalewayOrganizationDefinition struct { + ID string `json:"id"` + Name string `json:"name"` + Users []ScalewayUserDefinition `json:"users"` +} + +// ScalewayOrganizationsDefinition represents a Scaleway Organizations +type ScalewayOrganizationsDefinition struct { + Organizations []ScalewayOrganizationDefinition `json:"organizations"` +} + +// ScalewayUserDefinition represents a Scaleway User +type ScalewayUserDefinition struct { + Email string `json:"email"` + Firstname string `json:"firstname"` + Fullname string `json:"fullname"` + ID string `json:"id"` + Lastname string `json:"lastname"` + Organizations []ScalewayOrganizationDefinition `json:"organizations"` + Roles []ScalewayRoleDefinition `json:"roles"` + SSHPublicKeys []ScalewayKeyDefinition `json:"ssh_public_keys"` +} + +// ScalewayUsersDefinition represents the response of a GET /user +type ScalewayUsersDefinition struct { + User ScalewayUserDefinition `json:"user"` +} + +// ScalewayKeyDefinition represents a key +type ScalewayKeyDefinition struct { + Key string `json:"key"` + Fingerprint string `json:"fingerprint,omitempty"` +} + +// ScalewayUserPatchSSHKeyDefinition represents a User Patch +type ScalewayUserPatchSSHKeyDefinition struct { + SSHPublicKeys []ScalewayKeyDefinition `json:"ssh_public_keys"` +} + +// ScalewayDashboardResp represents a dashboard received from the API +type ScalewayDashboardResp struct { + Dashboard ScalewayDashboard +} + +// ScalewayDashboard represents a dashboard +type ScalewayDashboard struct { + VolumesCount int `json:"volumes_count"` + RunningServersCount int `json:"running_servers_count"` + ImagesCount int `json:"images_count"` + SnapshotsCount int `json:"snapshots_count"` + ServersCount int `json:"servers_count"` + IPsCount int `json:"ips_count"` +} + +// ScalewayPermissions represents the response of GET /permissions +type ScalewayPermissions map[string]ScalewayPermCategory + +// ScalewayPermCategory represents ScalewayPermissions's fields +type ScalewayPermCategory map[string][]string + +// ScalewayPermissionDefinition represents the permissions +type ScalewayPermissionDefinition struct { + Permissions ScalewayPermissions `json:"permissions"` +} + +// ScalewayUserdatas represents the response of a GET /user_data +type ScalewayUserdatas struct { + UserData []string `json:"user_data"` +} + +// ScalewayQuota represents a map of quota (name, value) +type ScalewayQuota map[string]int + +// ScalewayGetQuotas represents the response of GET /organizations/{orga_id}/quotas +type ScalewayGetQuotas struct { + Quotas ScalewayQuota `json:"quotas"` +} + +// ScalewayUserdata represents []byte +type ScalewayUserdata []byte + +// FuncMap used for json inspection +var FuncMap = template.FuncMap{ + "json": func(v interface{}) string { + a, _ := json.Marshal(v) + return string(a) + }, +} + +// MarketLocalImageDefinition represents localImage of marketplace version +type MarketLocalImageDefinition struct { + Arch string `json:"arch"` + ID string `json:"id"` + Zone string `json:"zone"` +} + +// MarketLocalImages represents an array of local images +type MarketLocalImages struct { + LocalImages []MarketLocalImageDefinition `json:"local_images"` +} + +// MarketLocalImage represents local image +type MarketLocalImage struct { + LocalImages MarketLocalImageDefinition `json:"local_image"` +} + +// MarketVersionDefinition represents version of marketplace image +type MarketVersionDefinition struct { + CreationDate string `json:"creation_date"` + ID string `json:"id"` + Image struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"image"` + ModificationDate string `json:"modification_date"` + Name string `json:"name"` + MarketLocalImages +} + +// MarketVersions represents an array of marketplace image versions +type MarketVersions struct { + Versions []MarketVersionDefinition `json:"versions"` +} + +// MarketVersion represents version of marketplace image +type MarketVersion struct { + Version MarketVersionDefinition `json:"version"` +} + +// MarketImage represents MarketPlace image +type MarketImage struct { + Categories []string `json:"categories"` + CreationDate string `json:"creation_date"` + CurrentPublicVersion string `json:"current_public_version"` + Description string `json:"description"` + ID string `json:"id"` + Logo string `json:"logo"` + ModificationDate string `json:"modification_date"` + Name string `json:"name"` + Organization struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"organization"` + Public bool `json:"-"` + MarketVersions +} + +// MarketImages represents MarketPlace images +type MarketImages struct { + Images []MarketImage `json:"images"` +} + +// NewScalewayAPI creates a ready-to-use ScalewayAPI client +func NewScalewayAPI(organization, token, userAgent string, options ...func(*ScalewayAPI)) (*ScalewayAPI, error) { + cache, err := NewScalewayCache() + if err != nil { + return nil, err + } + s := &ScalewayAPI{ + // exposed + Organization: organization, + Token: token, + Cache: cache, + Logger: NewDefaultLogger(), + verbose: os.Getenv("SCW_VERBOSE_API") != "", + password: "", + userAgent: userAgent, + + // internal + client: &http.Client{}, + } + for _, option := range options { + option(s) + } + + if os.Getenv("SCW_TLSVERIFY") == "0" { + s.client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } + + return s, nil +} + +// ClearCache clears the cache +func (s *ScalewayAPI) ClearCache() { + s.Cache.Clear() +} + +// Sync flushes out the cache to the disk +func (s *ScalewayAPI) Sync() { + s.Cache.Save() +} + +// GetResponse returns an http.Response object for the requested resource +func (s *ScalewayAPI) GetResponse(apiURL, resource string) (*http.Response, error) { + uri := fmt.Sprintf("%s/%s", strings.TrimRight(apiURL, "/"), resource) + + req, err := http.NewRequest("GET", uri, nil) + if err != nil { + return nil, err + } + req.Header.Set("X-Auth-Token", s.Token) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", s.userAgent) + + s.LogHTTP(req) + + return s.client.Do(req) +} + +// PostResponse returns an http.Response object for the updated resource +func (s *ScalewayAPI) PostResponse(apiURL, resource string, data interface{}) (*http.Response, error) { + uri := fmt.Sprintf("%s/%s", strings.TrimRight(apiURL, "/"), resource) + payload := new(bytes.Buffer) + encoder := json.NewEncoder(payload) + if err := encoder.Encode(data); err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", uri, payload) + if err != nil { + return nil, err + } + req.Header.Set("X-Auth-Token", s.Token) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", s.userAgent) + + s.LogHTTP(req) + + return s.client.Do(req) +} + +// PatchResponse returns an http.Response object for the updated resource +func (s *ScalewayAPI) PatchResponse(apiURL, resource string, data interface{}) (*http.Response, error) { + uri := fmt.Sprintf("%s/%s", strings.TrimRight(apiURL, "/"), resource) + payload := new(bytes.Buffer) + encoder := json.NewEncoder(payload) + if err := encoder.Encode(data); err != nil { + return nil, err + } + + req, err := http.NewRequest("PATCH", uri, payload) + if err != nil { + return nil, err + } + req.Header.Set("X-Auth-Token", s.Token) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", s.userAgent) + + s.LogHTTP(req) + + return s.client.Do(req) +} + +// PutResponse returns an http.Response object for the updated resource +func (s *ScalewayAPI) PutResponse(apiURL, resource string, data interface{}) (*http.Response, error) { + uri := fmt.Sprintf("%s/%s", strings.TrimRight(apiURL, "/"), resource) + payload := new(bytes.Buffer) + encoder := json.NewEncoder(payload) + if err := encoder.Encode(data); err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", uri, payload) + if err != nil { + return nil, err + } + req.Header.Set("X-Auth-Token", s.Token) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", s.userAgent) + + s.LogHTTP(req) + + return s.client.Do(req) +} + +// DeleteResponse returns an http.Response object for the deleted resource +func (s *ScalewayAPI) DeleteResponse(apiURL, resource string) (*http.Response, error) { + uri := fmt.Sprintf("%s/%s", strings.TrimRight(apiURL, "/"), resource) + + req, err := http.NewRequest("DELETE", uri, nil) + if err != nil { + return nil, err + } + req.Header.Set("X-Auth-Token", s.Token) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", s.userAgent) + + s.LogHTTP(req) + + return s.client.Do(req) +} + +// handleHTTPError checks the statusCode and displays the error +func (s *ScalewayAPI) handleHTTPError(goodStatusCode []int, resp *http.Response) ([]byte, error) { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode >= 500 { + return nil, errors.New(string(body)) + } + good := false + for _, code := range goodStatusCode { + if code == resp.StatusCode { + good = true + } + } + if !good { + var scwError ScalewayAPIError + + if err := json.Unmarshal(body, &scwError); err != nil { + return nil, err + } + scwError.StatusCode = resp.StatusCode + s.Debugf("%s", scwError.Error()) + return nil, scwError + } + if s.verbose { + var js bytes.Buffer + + err = json.Indent(&js, body, "", " ") + if err != nil { + s.Debugf("%s", string(body)) + } else { + s.Debugf("%s", js.String()) + } + } + return body, nil +} + +// GetServers gets the list of servers from the ScalewayAPI +func (s *ScalewayAPI) GetServers(all bool, limit int) (*[]ScalewayServer, error) { + query := url.Values{} + if !all { + query.Set("state", "running") + } + if limit > 0 { + // FIXME: wait for the API to be ready + // query.Set("per_page", strconv.Itoa(limit)) + } + if all && limit == 0 { + s.Cache.ClearServers() + } + resp, err := s.GetResponse(ComputeAPI, "servers?"+query.Encode()) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var servers ScalewayServers + if err = json.Unmarshal(body, &servers); err != nil { + return nil, err + } + for _, server := range servers.Servers { + // FIXME region, arch, owner, title + s.Cache.InsertServer(server.Identifier, "fr-1", server.Arch, server.Organization, server.Name) + } + // FIXME: when API limit is ready, remove the following code + if limit > 0 && limit < len(servers.Servers) { + servers.Servers = servers.Servers[0:limit] + } + return &servers.Servers, nil +} + +// GetServer gets a server from the ScalewayAPI +func (s *ScalewayAPI) GetServer(serverID string) (*ScalewayServer, error) { + resp, err := s.GetResponse(ComputeAPI, "servers/"+serverID) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + + var oneServer ScalewayOneServer + + if err = json.Unmarshal(body, &oneServer); err != nil { + return nil, err + } + // FIXME region, arch, owner, title + s.Cache.InsertServer(oneServer.Server.Identifier, "fr-1", oneServer.Server.Arch, oneServer.Server.Organization, oneServer.Server.Name) + return &oneServer.Server, nil +} + +// PostServerAction posts an action on a server +func (s *ScalewayAPI) PostServerAction(serverID, action string) error { + data := ScalewayServerAction{ + Action: action, + } + resp, err := s.PostResponse(ComputeAPI, fmt.Sprintf("servers/%s/action", serverID), data) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + _, err = s.handleHTTPError([]int{202}, resp) + return err +} + +// DeleteServer deletes a server +func (s *ScalewayAPI) DeleteServer(serverID string) error { + defer s.Cache.RemoveServer(serverID) + resp, err := s.DeleteResponse(ComputeAPI, fmt.Sprintf("servers/%s", serverID)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + if _, err = s.handleHTTPError([]int{204}, resp); err != nil { + return err + } + return nil +} + +// PostServer creates a new server +func (s *ScalewayAPI) PostServer(definition ScalewayServerDefinition) (string, error) { + definition.Organization = s.Organization + + resp, err := s.PostResponse(ComputeAPI, "servers", definition) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return "", err + } + + body, err := s.handleHTTPError([]int{201}, resp) + if err != nil { + return "", err + } + var server ScalewayOneServer + + if err = json.Unmarshal(body, &server); err != nil { + return "", err + } + // FIXME region, arch, owner, title + s.Cache.InsertServer(server.Server.Identifier, "fr-1", server.Server.Arch, server.Server.Organization, server.Server.Name) + return server.Server.Identifier, nil +} + +// PatchUserSSHKey updates a user +func (s *ScalewayAPI) PatchUserSSHKey(UserID string, definition ScalewayUserPatchSSHKeyDefinition) error { + resp, err := s.PatchResponse(AccountAPI, fmt.Sprintf("users/%s", UserID), definition) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + if _, err := s.handleHTTPError([]int{200}, resp); err != nil { + return err + } + return nil +} + +// PatchServer updates a server +func (s *ScalewayAPI) PatchServer(serverID string, definition ScalewayServerPatchDefinition) error { + resp, err := s.PatchResponse(ComputeAPI, fmt.Sprintf("servers/%s", serverID), definition) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + if _, err := s.handleHTTPError([]int{200}, resp); err != nil { + return err + } + return nil +} + +// PostSnapshot creates a new snapshot +func (s *ScalewayAPI) PostSnapshot(volumeID string, name string) (string, error) { + definition := ScalewaySnapshotDefinition{ + VolumeIDentifier: volumeID, + Name: name, + Organization: s.Organization, + } + resp, err := s.PostResponse(ComputeAPI, "snapshots", definition) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return "", err + } + + body, err := s.handleHTTPError([]int{201}, resp) + if err != nil { + return "", err + } + var snapshot ScalewayOneSnapshot + + if err = json.Unmarshal(body, &snapshot); err != nil { + return "", err + } + // FIXME region, arch, owner, title + s.Cache.InsertSnapshot(snapshot.Snapshot.Identifier, "fr-1", "", snapshot.Snapshot.Organization, snapshot.Snapshot.Name) + return snapshot.Snapshot.Identifier, nil +} + +// PostImage creates a new image +func (s *ScalewayAPI) PostImage(volumeID string, name string, bootscript string, arch string) (string, error) { + definition := ScalewayImageDefinition{ + SnapshotIDentifier: volumeID, + Name: name, + Organization: s.Organization, + Arch: arch, + } + if bootscript != "" { + definition.DefaultBootscript = &bootscript + } + + resp, err := s.PostResponse(ComputeAPI, "images", definition) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return "", err + } + + body, err := s.handleHTTPError([]int{201}, resp) + if err != nil { + return "", err + } + var image ScalewayOneImage + + if err = json.Unmarshal(body, &image); err != nil { + return "", err + } + // FIXME region, arch, owner, title + s.Cache.InsertImage(image.Image.Identifier, "fr-1", image.Image.Arch, image.Image.Organization, image.Image.Name, "") + return image.Image.Identifier, nil +} + +// PostVolume creates a new volume +func (s *ScalewayAPI) PostVolume(definition ScalewayVolumeDefinition) (string, error) { + definition.Organization = s.Organization + if definition.Type == "" { + definition.Type = "l_ssd" + } + + resp, err := s.PostResponse(ComputeAPI, "volumes", definition) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return "", err + } + + body, err := s.handleHTTPError([]int{201}, resp) + if err != nil { + return "", err + } + var volume ScalewayOneVolume + + if err = json.Unmarshal(body, &volume); err != nil { + return "", err + } + // FIXME: s.Cache.InsertVolume(volume.Volume.Identifier, volume.Volume.Name) + return volume.Volume.Identifier, nil +} + +// PutVolume updates a volume +func (s *ScalewayAPI) PutVolume(volumeID string, definition ScalewayVolumePutDefinition) error { + resp, err := s.PutResponse(ComputeAPI, fmt.Sprintf("volumes/%s", volumeID), definition) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + _, err = s.handleHTTPError([]int{200}, resp) + return err +} + +// ResolveServer attempts to find a matching Identifier for the input string +func (s *ScalewayAPI) ResolveServer(needle string) (ScalewayResolverResults, error) { + servers := s.Cache.LookUpServers(needle, true) + if len(servers) == 0 { + if _, err := s.GetServers(true, 0); err != nil { + return nil, err + } + servers = s.Cache.LookUpServers(needle, true) + } + return servers, nil +} + +// ResolveVolume attempts to find a matching Identifier for the input string +func (s *ScalewayAPI) ResolveVolume(needle string) (ScalewayResolverResults, error) { + volumes := s.Cache.LookUpVolumes(needle, true) + if len(volumes) == 0 { + if _, err := s.GetVolumes(); err != nil { + return nil, err + } + volumes = s.Cache.LookUpVolumes(needle, true) + } + return volumes, nil +} + +// ResolveSnapshot attempts to find a matching Identifier for the input string +func (s *ScalewayAPI) ResolveSnapshot(needle string) (ScalewayResolverResults, error) { + snapshots := s.Cache.LookUpSnapshots(needle, true) + if len(snapshots) == 0 { + if _, err := s.GetSnapshots(); err != nil { + return nil, err + } + snapshots = s.Cache.LookUpSnapshots(needle, true) + } + return snapshots, nil +} + +// ResolveImage attempts to find a matching Identifier for the input string +func (s *ScalewayAPI) ResolveImage(needle string) (ScalewayResolverResults, error) { + images := s.Cache.LookUpImages(needle, true) + if len(images) == 0 { + if _, err := s.GetImages(); err != nil { + return nil, err + } + images = s.Cache.LookUpImages(needle, true) + } + return images, nil +} + +// ResolveBootscript attempts to find a matching Identifier for the input string +func (s *ScalewayAPI) ResolveBootscript(needle string) (ScalewayResolverResults, error) { + bootscripts := s.Cache.LookUpBootscripts(needle, true) + if len(bootscripts) == 0 { + if _, err := s.GetBootscripts(); err != nil { + return nil, err + } + bootscripts = s.Cache.LookUpBootscripts(needle, true) + } + return bootscripts, nil +} + +// GetImages gets the list of images from the ScalewayAPI +func (s *ScalewayAPI) GetImages() (*[]MarketImage, error) { + images, err := s.GetMarketPlaceImages("") + if err != nil { + return nil, err + } + s.Cache.ClearImages() + for i, image := range images.Images { + if image.CurrentPublicVersion != "" { + for _, version := range image.Versions { + if version.ID == image.CurrentPublicVersion { + for _, localImage := range version.LocalImages { + images.Images[i].Public = true + s.Cache.InsertImage(localImage.ID, localImage.Zone, localImage.Arch, image.Organization.ID, image.Name, image.CurrentPublicVersion) + } + } + } + } + } + resp, err := s.GetResponse(ComputeAPI, "images?organization="+s.Organization) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var OrgaImages ScalewayImages + + if err = json.Unmarshal(body, &OrgaImages); err != nil { + return nil, err + } + for _, orgaImage := range OrgaImages.Images { + s.Cache.InsertImage(orgaImage.Identifier, "fr-1", orgaImage.Arch, orgaImage.Organization, orgaImage.Name, "") + images.Images = append(images.Images, MarketImage{ + Categories: []string{"MyImages"}, + CreationDate: orgaImage.CreationDate, + CurrentPublicVersion: orgaImage.Identifier, + ModificationDate: orgaImage.ModificationDate, + Name: orgaImage.Name, + Public: false, + MarketVersions: MarketVersions{ + Versions: []MarketVersionDefinition{ + { + CreationDate: orgaImage.CreationDate, + ID: orgaImage.Identifier, + ModificationDate: orgaImage.ModificationDate, + MarketLocalImages: MarketLocalImages{ + LocalImages: []MarketLocalImageDefinition{ + { + Arch: orgaImage.Arch, + ID: orgaImage.Identifier, + Zone: "fr-1", + }, + }, + }, + }, + }, + }, + }) + } + return &images.Images, nil +} + +// GetImage gets an image from the ScalewayAPI +func (s *ScalewayAPI) GetImage(imageID string) (*ScalewayImage, error) { + resp, err := s.GetResponse(ComputeAPI, "images/"+imageID) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var oneImage ScalewayOneImage + + if err = json.Unmarshal(body, &oneImage); err != nil { + return nil, err + } + // FIXME region, arch, owner, title + s.Cache.InsertImage(oneImage.Image.Identifier, "fr-1", oneImage.Image.Arch, oneImage.Image.Organization, oneImage.Image.Name, "") + return &oneImage.Image, nil +} + +// DeleteImage deletes a image +func (s *ScalewayAPI) DeleteImage(imageID string) error { + defer s.Cache.RemoveImage(imageID) + resp, err := s.DeleteResponse(ComputeAPI, fmt.Sprintf("images/%s", imageID)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + if _, err := s.handleHTTPError([]int{204}, resp); err != nil { + return err + } + return nil +} + +// DeleteSnapshot deletes a snapshot +func (s *ScalewayAPI) DeleteSnapshot(snapshotID string) error { + defer s.Cache.RemoveSnapshot(snapshotID) + resp, err := s.DeleteResponse(ComputeAPI, fmt.Sprintf("snapshots/%s", snapshotID)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + if _, err := s.handleHTTPError([]int{204}, resp); err != nil { + return err + } + return nil +} + +// DeleteVolume deletes a volume +func (s *ScalewayAPI) DeleteVolume(volumeID string) error { + defer s.Cache.RemoveVolume(volumeID) + resp, err := s.DeleteResponse(ComputeAPI, fmt.Sprintf("volumes/%s", volumeID)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + if _, err := s.handleHTTPError([]int{204}, resp); err != nil { + return err + } + return nil +} + +// GetSnapshots gets the list of snapshots from the ScalewayAPI +func (s *ScalewayAPI) GetSnapshots() (*[]ScalewaySnapshot, error) { + query := url.Values{} + s.Cache.ClearSnapshots() + + resp, err := s.GetResponse(ComputeAPI, "snapshots?"+query.Encode()) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var snapshots ScalewaySnapshots + + if err = json.Unmarshal(body, &snapshots); err != nil { + return nil, err + } + for _, snapshot := range snapshots.Snapshots { + // FIXME region, arch, owner, title + s.Cache.InsertSnapshot(snapshot.Identifier, "fr-1", "", snapshot.Organization, snapshot.Name) + } + return &snapshots.Snapshots, nil +} + +// GetSnapshot gets a snapshot from the ScalewayAPI +func (s *ScalewayAPI) GetSnapshot(snapshotID string) (*ScalewaySnapshot, error) { + resp, err := s.GetResponse(ComputeAPI, "snapshots/"+snapshotID) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var oneSnapshot ScalewayOneSnapshot + + if err = json.Unmarshal(body, &oneSnapshot); err != nil { + return nil, err + } + // FIXME region, arch, owner, title + s.Cache.InsertSnapshot(oneSnapshot.Snapshot.Identifier, "fr-1", "", oneSnapshot.Snapshot.Organization, oneSnapshot.Snapshot.Name) + return &oneSnapshot.Snapshot, nil +} + +// GetVolumes gets the list of volumes from the ScalewayAPI +func (s *ScalewayAPI) GetVolumes() (*[]ScalewayVolume, error) { + query := url.Values{} + s.Cache.ClearVolumes() + + resp, err := s.GetResponse(ComputeAPI, "volumes?"+query.Encode()) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var volumes ScalewayVolumes + + if err = json.Unmarshal(body, &volumes); err != nil { + return nil, err + } + for _, volume := range volumes.Volumes { + // FIXME region, arch, owner, title + s.Cache.InsertVolume(volume.Identifier, "fr-1", "", volume.Organization, volume.Name) + } + return &volumes.Volumes, nil +} + +// GetVolume gets a volume from the ScalewayAPI +func (s *ScalewayAPI) GetVolume(volumeID string) (*ScalewayVolume, error) { + resp, err := s.GetResponse(ComputeAPI, "volumes/"+volumeID) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var oneVolume ScalewayOneVolume + + if err = json.Unmarshal(body, &oneVolume); err != nil { + return nil, err + } + // FIXME region, arch, owner, title + s.Cache.InsertVolume(oneVolume.Volume.Identifier, "fr-1", "", oneVolume.Volume.Organization, oneVolume.Volume.Name) + return &oneVolume.Volume, nil +} + +// GetBootscripts gets the list of bootscripts from the ScalewayAPI +func (s *ScalewayAPI) GetBootscripts() (*[]ScalewayBootscript, error) { + query := url.Values{} + s.Cache.ClearBootscripts() + resp, err := s.GetResponse(ComputeAPI, "bootscripts?"+query.Encode()) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var bootscripts ScalewayBootscripts + + if err = json.Unmarshal(body, &bootscripts); err != nil { + return nil, err + } + for _, bootscript := range bootscripts.Bootscripts { + // FIXME region, arch, owner, title + s.Cache.InsertBootscript(bootscript.Identifier, "fr-1", bootscript.Arch, bootscript.Organization, bootscript.Title) + } + return &bootscripts.Bootscripts, nil +} + +// GetBootscript gets a bootscript from the ScalewayAPI +func (s *ScalewayAPI) GetBootscript(bootscriptID string) (*ScalewayBootscript, error) { + resp, err := s.GetResponse(ComputeAPI, "bootscripts/"+bootscriptID) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var oneBootscript ScalewayOneBootscript + + if err = json.Unmarshal(body, &oneBootscript); err != nil { + return nil, err + } + // FIXME region, arch, owner, title + s.Cache.InsertBootscript(oneBootscript.Bootscript.Identifier, "fr-1", oneBootscript.Bootscript.Arch, oneBootscript.Bootscript.Organization, oneBootscript.Bootscript.Title) + return &oneBootscript.Bootscript, nil +} + +// GetUserdatas gets list of userdata for a server +func (s *ScalewayAPI) GetUserdatas(serverID string, metadata bool) (*ScalewayUserdatas, error) { + var url, endpoint string + + endpoint = ComputeAPI + if metadata { + url = "/user_data" + endpoint = MetadataAPI + } else { + url = fmt.Sprintf("servers/%s/user_data", serverID) + } + + resp, err := s.GetResponse(endpoint, url) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var userdatas ScalewayUserdatas + + if err = json.Unmarshal(body, &userdatas); err != nil { + return nil, err + } + return &userdatas, nil +} + +func (s *ScalewayUserdata) String() string { + return string(*s) +} + +// GetUserdata gets a specific userdata for a server +func (s *ScalewayAPI) GetUserdata(serverID, key string, metadata bool) (*ScalewayUserdata, error) { + var url, endpoint string + + endpoint = ComputeAPI + if metadata { + url = fmt.Sprintf("/user_data/%s", key) + endpoint = MetadataAPI + } else { + url = fmt.Sprintf("servers/%s/user_data/%s", serverID, key) + } + + var err error + resp, err := s.GetResponse(endpoint, url) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("no such user_data %q (%d)", key, resp.StatusCode) + } + var data ScalewayUserdata + data, err = ioutil.ReadAll(resp.Body) + return &data, err +} + +// PatchUserdata sets a user data +func (s *ScalewayAPI) PatchUserdata(serverID, key string, value []byte, metadata bool) error { + var resource, endpoint string + + endpoint = ComputeAPI + if metadata { + resource = fmt.Sprintf("/user_data/%s", key) + endpoint = MetadataAPI + } else { + resource = fmt.Sprintf("servers/%s/user_data/%s", serverID, key) + } + + uri := fmt.Sprintf("%s/%s", strings.TrimRight(endpoint, "/"), resource) + payload := new(bytes.Buffer) + payload.Write(value) + + req, err := http.NewRequest("PATCH", uri, payload) + if err != nil { + return err + } + + req.Header.Set("X-Auth-Token", s.Token) + req.Header.Set("Content-Type", "text/plain") + req.Header.Set("User-Agent", s.userAgent) + + s.LogHTTP(req) + + resp, err := s.client.Do(req) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + if resp.StatusCode == 204 { + return nil + } + + return fmt.Errorf("cannot set user_data (%d)", resp.StatusCode) +} + +// DeleteUserdata deletes a server user_data +func (s *ScalewayAPI) DeleteUserdata(serverID, key string, metadata bool) error { + var url, endpoint string + + endpoint = ComputeAPI + if metadata { + url = fmt.Sprintf("/user_data/%s", key) + endpoint = MetadataAPI + } else { + url = fmt.Sprintf("servers/%s/user_data/%s", serverID, key) + } + + resp, err := s.DeleteResponse(endpoint, url) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + _, err = s.handleHTTPError([]int{204}, resp) + return err +} + +// GetTasks get the list of tasks from the ScalewayAPI +func (s *ScalewayAPI) GetTasks() (*[]ScalewayTask, error) { + query := url.Values{} + resp, err := s.GetResponse(ComputeAPI, "tasks?"+query.Encode()) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var tasks ScalewayTasks + + if err = json.Unmarshal(body, &tasks); err != nil { + return nil, err + } + return &tasks.Tasks, nil +} + +// CheckCredentials performs a dummy check to ensure we can contact the API +func (s *ScalewayAPI) CheckCredentials() error { + query := url.Values{} + query.Set("token_id", s.Token) + + resp, err := s.GetResponse(AccountAPI, "tokens?"+query.Encode()) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + if _, err := s.handleHTTPError([]int{200}, resp); err != nil { + return err + } + return nil +} + +// GetUserID returns the userID +func (s *ScalewayAPI) GetUserID() (string, error) { + resp, err := s.GetResponse(AccountAPI, fmt.Sprintf("tokens/%s", s.Token)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return "", err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return "", err + } + var token ScalewayTokensDefinition + + if err = json.Unmarshal(body, &token); err != nil { + return "", err + } + return token.Token.UserID, nil +} + +// GetOrganization returns Organization +func (s *ScalewayAPI) GetOrganization() (*ScalewayOrganizationsDefinition, error) { + resp, err := s.GetResponse(AccountAPI, "organizations") + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var data ScalewayOrganizationsDefinition + + if err = json.Unmarshal(body, &data); err != nil { + return nil, err + } + return &data, nil +} + +// GetUser returns the user +func (s *ScalewayAPI) GetUser() (*ScalewayUserDefinition, error) { + userID, err := s.GetUserID() + if err != nil { + return nil, err + } + resp, err := s.GetResponse(AccountAPI, fmt.Sprintf("users/%s", userID)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var user ScalewayUsersDefinition + + if err = json.Unmarshal(body, &user); err != nil { + return nil, err + } + return &user.User, nil +} + +// GetPermissions returns the permissions +func (s *ScalewayAPI) GetPermissions() (*ScalewayPermissionDefinition, error) { + resp, err := s.GetResponse(AccountAPI, fmt.Sprintf("tokens/%s/permissions", s.Token)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var permissions ScalewayPermissionDefinition + + if err = json.Unmarshal(body, &permissions); err != nil { + return nil, err + } + return &permissions, nil +} + +// GetDashboard returns the dashboard +func (s *ScalewayAPI) GetDashboard() (*ScalewayDashboard, error) { + resp, err := s.GetResponse(ComputeAPI, "dashboard") + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var dashboard ScalewayDashboardResp + + if err = json.Unmarshal(body, &dashboard); err != nil { + return nil, err + } + return &dashboard.Dashboard, nil +} + +// GetServerID returns exactly one server matching +func (s *ScalewayAPI) GetServerID(needle string) (string, error) { + // Parses optional type prefix, i.e: "server:name" -> "name" + _, needle = parseNeedle(needle) + + servers, err := s.ResolveServer(needle) + if err != nil { + return "", fmt.Errorf("Unable to resolve server %s: %s", needle, err) + } + if len(servers) == 1 { + return servers[0].Identifier, nil + } + if len(servers) == 0 { + return "", fmt.Errorf("No such server: %s", needle) + } + return "", showResolverResults(needle, servers) +} + +func showResolverResults(needle string, results ScalewayResolverResults) error { + w := tabwriter.NewWriter(os.Stderr, 20, 1, 3, ' ', 0) + defer w.Flush() + sort.Sort(results) + for _, result := range results { + if result.Arch == "" { + result.Arch = "n/a" + } + fmt.Fprintf(w, "- %s\t%s\t%s\t%s\n", result.TruncIdentifier(), result.CodeName(), result.Name, result.Arch) + } + return fmt.Errorf("Too many candidates for %s (%d)", needle, len(results)) +} + +// GetVolumeID returns exactly one volume matching +func (s *ScalewayAPI) GetVolumeID(needle string) (string, error) { + // Parses optional type prefix, i.e: "volume:name" -> "name" + _, needle = parseNeedle(needle) + + volumes, err := s.ResolveVolume(needle) + if err != nil { + return "", fmt.Errorf("Unable to resolve volume %s: %s", needle, err) + } + if len(volumes) == 1 { + return volumes[0].Identifier, nil + } + if len(volumes) == 0 { + return "", fmt.Errorf("No such volume: %s", needle) + } + return "", showResolverResults(needle, volumes) +} + +// GetSnapshotID returns exactly one snapshot matching +func (s *ScalewayAPI) GetSnapshotID(needle string) (string, error) { + // Parses optional type prefix, i.e: "snapshot:name" -> "name" + _, needle = parseNeedle(needle) + + snapshots, err := s.ResolveSnapshot(needle) + if err != nil { + return "", fmt.Errorf("Unable to resolve snapshot %s: %s", needle, err) + } + if len(snapshots) == 1 { + return snapshots[0].Identifier, nil + } + if len(snapshots) == 0 { + return "", fmt.Errorf("No such snapshot: %s", needle) + } + return "", showResolverResults(needle, snapshots) +} + +// FilterImagesByArch removes entry that doesn't match with architecture +func FilterImagesByArch(res ScalewayResolverResults, arch string) (ret ScalewayResolverResults) { + if arch == "*" { + return res + } + for _, result := range res { + if result.Arch == arch { + ret = append(ret, result) + } + } + return +} + +// GetImageID returns exactly one image matching +func (s *ScalewayAPI) GetImageID(needle, arch string) (*ScalewayImageIdentifier, error) { + // Parses optional type prefix, i.e: "image:name" -> "name" + _, needle = parseNeedle(needle) + + images, err := s.ResolveImage(needle) + if err != nil { + return nil, fmt.Errorf("Unable to resolve image %s: %s", needle, err) + } + images = FilterImagesByArch(images, arch) + if len(images) == 1 { + return &ScalewayImageIdentifier{ + Identifier: images[0].Identifier, + Arch: images[0].Arch, + // FIXME region, owner hardcoded + Region: "fr-1", + Owner: "", + }, nil + } + if len(images) == 0 { + return nil, fmt.Errorf("No such image: %s", needle) + } + return nil, showResolverResults(needle, images) +} + +// GetSecurityGroups returns a ScalewaySecurityGroups +func (s *ScalewayAPI) GetSecurityGroups() (*ScalewayGetSecurityGroups, error) { + resp, err := s.GetResponse(ComputeAPI, "security_groups") + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var securityGroups ScalewayGetSecurityGroups + + if err = json.Unmarshal(body, &securityGroups); err != nil { + return nil, err + } + return &securityGroups, nil +} + +// GetSecurityGroupRules returns a ScalewaySecurityGroupRules +func (s *ScalewayAPI) GetSecurityGroupRules(groupID string) (*ScalewayGetSecurityGroupRules, error) { + resp, err := s.GetResponse(ComputeAPI, fmt.Sprintf("security_groups/%s/rules", groupID)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var securityGroupRules ScalewayGetSecurityGroupRules + + if err = json.Unmarshal(body, &securityGroupRules); err != nil { + return nil, err + } + return &securityGroupRules, nil +} + +// GetASecurityGroupRule returns a ScalewaySecurityGroupRule +func (s *ScalewayAPI) GetASecurityGroupRule(groupID string, rulesID string) (*ScalewayGetSecurityGroupRule, error) { + resp, err := s.GetResponse(ComputeAPI, fmt.Sprintf("security_groups/%s/rules/%s", groupID, rulesID)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var securityGroupRules ScalewayGetSecurityGroupRule + + if err = json.Unmarshal(body, &securityGroupRules); err != nil { + return nil, err + } + return &securityGroupRules, nil +} + +// GetASecurityGroup returns a ScalewaySecurityGroup +func (s *ScalewayAPI) GetASecurityGroup(groupsID string) (*ScalewayGetSecurityGroup, error) { + resp, err := s.GetResponse(ComputeAPI, fmt.Sprintf("security_groups/%s", groupsID)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var securityGroups ScalewayGetSecurityGroup + + if err = json.Unmarshal(body, &securityGroups); err != nil { + return nil, err + } + return &securityGroups, nil +} + +// PostSecurityGroup posts a group on a server +func (s *ScalewayAPI) PostSecurityGroup(group ScalewayNewSecurityGroup) error { + resp, err := s.PostResponse(ComputeAPI, "security_groups", group) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + _, err = s.handleHTTPError([]int{201}, resp) + return err +} + +// PostSecurityGroupRule posts a rule on a server +func (s *ScalewayAPI) PostSecurityGroupRule(SecurityGroupID string, rules ScalewayNewSecurityGroupRule) error { + resp, err := s.PostResponse(ComputeAPI, fmt.Sprintf("security_groups/%s/rules", SecurityGroupID), rules) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + _, err = s.handleHTTPError([]int{201}, resp) + return err +} + +// DeleteSecurityGroup deletes a SecurityGroup +func (s *ScalewayAPI) DeleteSecurityGroup(securityGroupID string) error { + resp, err := s.DeleteResponse(ComputeAPI, fmt.Sprintf("security_groups/%s", securityGroupID)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + _, err = s.handleHTTPError([]int{204}, resp) + return err +} + +// PutSecurityGroup updates a SecurityGroup +func (s *ScalewayAPI) PutSecurityGroup(group ScalewayNewSecurityGroup, securityGroupID string) error { + resp, err := s.PutResponse(ComputeAPI, fmt.Sprintf("security_groups/%s", securityGroupID), group) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + _, err = s.handleHTTPError([]int{200}, resp) + return err +} + +// PutSecurityGroupRule updates a SecurityGroupRule +func (s *ScalewayAPI) PutSecurityGroupRule(rules ScalewayNewSecurityGroupRule, securityGroupID, RuleID string) error { + resp, err := s.PutResponse(ComputeAPI, fmt.Sprintf("security_groups/%s/rules/%s", securityGroupID, RuleID), rules) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + _, err = s.handleHTTPError([]int{200}, resp) + return err +} + +// DeleteSecurityGroupRule deletes a SecurityGroupRule +func (s *ScalewayAPI) DeleteSecurityGroupRule(SecurityGroupID, RuleID string) error { + resp, err := s.DeleteResponse(ComputeAPI, fmt.Sprintf("security_groups/%s/rules/%s", SecurityGroupID, RuleID)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + _, err = s.handleHTTPError([]int{204}, resp) + return err +} + +// GetContainers returns a ScalewayGetContainers +func (s *ScalewayAPI) GetContainers() (*ScalewayGetContainers, error) { + resp, err := s.GetResponse(ComputeAPI, "containers") + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var containers ScalewayGetContainers + + if err = json.Unmarshal(body, &containers); err != nil { + return nil, err + } + return &containers, nil +} + +// GetContainerDatas returns a ScalewayGetContainerDatas +func (s *ScalewayAPI) GetContainerDatas(container string) (*ScalewayGetContainerDatas, error) { + resp, err := s.GetResponse(ComputeAPI, fmt.Sprintf("containers/%s", container)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var datas ScalewayGetContainerDatas + + if err = json.Unmarshal(body, &datas); err != nil { + return nil, err + } + return &datas, nil +} + +// GetIPS returns a ScalewayGetIPS +func (s *ScalewayAPI) GetIPS() (*ScalewayGetIPS, error) { + resp, err := s.GetResponse(ComputeAPI, "ips") + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var ips ScalewayGetIPS + + if err = json.Unmarshal(body, &ips); err != nil { + return nil, err + } + return &ips, nil +} + +// NewIP returns a new IP +func (s *ScalewayAPI) NewIP() (*ScalewayGetIP, error) { + var orga struct { + Organization string `json:"organization"` + } + orga.Organization = s.Organization + resp, err := s.PostResponse(ComputeAPI, "ips", orga) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{201}, resp) + if err != nil { + return nil, err + } + var ip ScalewayGetIP + + if err = json.Unmarshal(body, &ip); err != nil { + return nil, err + } + return &ip, nil +} + +// AttachIP attachs an IP to a server +func (s *ScalewayAPI) AttachIP(ipID, serverID string) error { + var update struct { + Address string `json:"address"` + ID string `json:"id"` + Reverse *string `json:"reverse"` + Organization string `json:"organization"` + Server string `json:"server"` + } + + ip, err := s.GetIP(ipID) + if err != nil { + return err + } + update.Address = ip.IP.Address + update.ID = ip.IP.ID + update.Organization = ip.IP.Organization + update.Server = serverID + resp, err := s.PutResponse(ComputeAPI, fmt.Sprintf("ips/%s", ipID), update) + if err != nil { + return err + } + _, err = s.handleHTTPError([]int{200}, resp) + return err +} + +// DeleteIP deletes an IP +func (s *ScalewayAPI) DeleteIP(ipID string) error { + resp, err := s.DeleteResponse(ComputeAPI, fmt.Sprintf("ips/%s", ipID)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + if _, err := s.handleHTTPError([]int{204}, resp); err != nil { + return err + } + return nil +} + +// GetIP returns a ScalewayGetIP +func (s *ScalewayAPI) GetIP(ipID string) (*ScalewayGetIP, error) { + resp, err := s.GetResponse(ComputeAPI, fmt.Sprintf("ips/%s", ipID)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var ip ScalewayGetIP + + if err = json.Unmarshal(body, &ip); err != nil { + return nil, err + } + return &ip, nil +} + +// GetQuotas returns a ScalewayGetQuotas +func (s *ScalewayAPI) GetQuotas() (*ScalewayGetQuotas, error) { + resp, err := s.GetResponse(AccountAPI, fmt.Sprintf("organizations/%s/quotas", s.Organization)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var quotas ScalewayGetQuotas + + if err = json.Unmarshal(body, "as); err != nil { + return nil, err + } + return "as, nil +} + +// GetBootscriptID returns exactly one bootscript matching +func (s *ScalewayAPI) GetBootscriptID(needle, arch string) (string, error) { + // Parses optional type prefix, i.e: "bootscript:name" -> "name" + if len(strings.Split(needle, ":")) == 1 { + return needle, nil + } + + _, needle = parseNeedle(needle) + + bootscripts, err := s.ResolveBootscript(needle) + if err != nil { + return "", fmt.Errorf("Unable to resolve bootscript %s: %s", needle, err) + } + bootscripts.FilterByArch(arch) + if len(bootscripts) == 1 { + return bootscripts[0].Identifier, nil + } + if len(bootscripts) == 0 { + return "", fmt.Errorf("No such bootscript: %s", needle) + } + return "", showResolverResults(needle, bootscripts) +} + +func rootNetDial(network, addr string) (net.Conn, error) { + dialer := net.Dialer{ + Timeout: 10 * time.Second, + KeepAlive: 10 * time.Second, + } + + // bruteforce privileged ports + var localAddr net.Addr + var err error + for port := 1; port <= 1024; port++ { + localAddr, err = net.ResolveTCPAddr("tcp", fmt.Sprintf(":%d", port)) + + // this should never happen + if err != nil { + return nil, err + } + + dialer.LocalAddr = localAddr + + conn, err := dialer.Dial(network, addr) + + // if err is nil, dialer.Dial succeed, so let's go + // else, err != nil, but we don't care + if err == nil { + return conn, nil + } + } + // if here, all privileged ports were tried without success + return nil, fmt.Errorf("bind: permission denied, are you root ?") +} + +// SetPassword register the password +func (s *ScalewayAPI) SetPassword(password string) { + s.password = password +} + +// GetMarketPlaceImages returns images from marketplace +func (s *ScalewayAPI) GetMarketPlaceImages(uuidImage string) (*MarketImages, error) { + resp, err := s.GetResponse(MarketplaceAPI, fmt.Sprintf("images/%s", uuidImage)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var ret MarketImages + + if uuidImage != "" { + ret.Images = make([]MarketImage, 1) + + var img MarketImage + + if err = json.Unmarshal(body, &img); err != nil { + return nil, err + } + ret.Images[0] = img + } else { + if err = json.Unmarshal(body, &ret); err != nil { + return nil, err + } + } + return &ret, nil +} + +// GetMarketPlaceImageVersions returns image version +func (s *ScalewayAPI) GetMarketPlaceImageVersions(uuidImage, uuidVersion string) (*MarketVersions, error) { + resp, err := s.GetResponse(MarketplaceAPI, fmt.Sprintf("images/%v/versions/%s", uuidImage, uuidVersion)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var ret MarketVersions + + if uuidImage != "" { + var version MarketVersion + ret.Versions = make([]MarketVersionDefinition, 1) + + if err = json.Unmarshal(body, &version); err != nil { + return nil, err + } + ret.Versions[0] = version.Version + } else { + if err = json.Unmarshal(body, &ret); err != nil { + return nil, err + } + } + return &ret, nil +} + +// GetMarketPlaceImageCurrentVersion return the image current version +func (s *ScalewayAPI) GetMarketPlaceImageCurrentVersion(uuidImage string) (*MarketVersion, error) { + resp, err := s.GetResponse(MarketplaceAPI, fmt.Sprintf("images/%v/versions/current", uuidImage)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var ret MarketVersion + + if err = json.Unmarshal(body, &ret); err != nil { + return nil, err + } + return &ret, nil +} + +// GetMarketPlaceLocalImages returns images from local region +func (s *ScalewayAPI) GetMarketPlaceLocalImages(uuidImage, uuidVersion, uuidLocalImage string) (*MarketLocalImages, error) { + resp, err := s.GetResponse(MarketplaceAPI, fmt.Sprintf("images/%v/versions/%s/local_images/%s", uuidImage, uuidVersion, uuidLocalImage)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + body, err := s.handleHTTPError([]int{200}, resp) + if err != nil { + return nil, err + } + var ret MarketLocalImages + if uuidLocalImage != "" { + var localImage MarketLocalImage + ret.LocalImages = make([]MarketLocalImageDefinition, 1) + + if err = json.Unmarshal(body, &localImage); err != nil { + return nil, err + } + ret.LocalImages[0] = localImage.LocalImages + } else { + if err = json.Unmarshal(body, &ret); err != nil { + return nil, err + } + } + return &ret, nil +} + +// PostMarketPlaceImage adds new image +func (s *ScalewayAPI) PostMarketPlaceImage(images MarketImage) error { + resp, err := s.PostResponse(MarketplaceAPI, "images/", images) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + _, err = s.handleHTTPError([]int{202}, resp) + return err +} + +// PostMarketPlaceImageVersion adds new image version +func (s *ScalewayAPI) PostMarketPlaceImageVersion(uuidImage string, version MarketVersion) error { + resp, err := s.PostResponse(MarketplaceAPI, fmt.Sprintf("images/%v/versions", uuidImage), version) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + _, err = s.handleHTTPError([]int{202}, resp) + return err +} + +// PostMarketPlaceLocalImage adds new local image +func (s *ScalewayAPI) PostMarketPlaceLocalImage(uuidImage, uuidVersion, uuidLocalImage string, local MarketLocalImage) error { + resp, err := s.PostResponse(MarketplaceAPI, fmt.Sprintf("images/%v/versions/%s/local_images/%v", uuidImage, uuidVersion, uuidLocalImage), local) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + _, err = s.handleHTTPError([]int{202}, resp) + return err +} + +// PutMarketPlaceImage updates image +func (s *ScalewayAPI) PutMarketPlaceImage(uudiImage string, images MarketImage) error { + resp, err := s.PutResponse(MarketplaceAPI, fmt.Sprintf("images/%v", uudiImage), images) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + _, err = s.handleHTTPError([]int{200}, resp) + return err +} + +// PutMarketPlaceImageVersion updates image version +func (s *ScalewayAPI) PutMarketPlaceImageVersion(uuidImage, uuidVersion string, version MarketVersion) error { + resp, err := s.PutResponse(MarketplaceAPI, fmt.Sprintf("images/%v/versions/%v", uuidImage, uuidVersion), version) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + _, err = s.handleHTTPError([]int{200}, resp) + return err +} + +// PutMarketPlaceLocalImage updates local image +func (s *ScalewayAPI) PutMarketPlaceLocalImage(uuidImage, uuidVersion, uuidLocalImage string, local MarketLocalImage) error { + resp, err := s.PostResponse(MarketplaceAPI, fmt.Sprintf("images/%v/versions/%s/local_images/%v", uuidImage, uuidVersion, uuidLocalImage), local) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + _, err = s.handleHTTPError([]int{200}, resp) + return err +} + +// DeleteMarketPlaceImage deletes image +func (s *ScalewayAPI) DeleteMarketPlaceImage(uudImage string) error { + resp, err := s.DeleteResponse(MarketplaceAPI, fmt.Sprintf("images/%v", uudImage)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + _, err = s.handleHTTPError([]int{204}, resp) + return err +} + +// DeleteMarketPlaceImageVersion delete image version +func (s *ScalewayAPI) DeleteMarketPlaceImageVersion(uuidImage, uuidVersion string) error { + resp, err := s.DeleteResponse(MarketplaceAPI, fmt.Sprintf("images/%v/versions/%v", uuidImage, uuidVersion)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + _, err = s.handleHTTPError([]int{204}, resp) + return err +} + +// DeleteMarketPlaceLocalImage deletes local image +func (s *ScalewayAPI) DeleteMarketPlaceLocalImage(uuidImage, uuidVersion, uuidLocalImage string) error { + resp, err := s.DeleteResponse(MarketplaceAPI, fmt.Sprintf("images/%v/versions/%s/local_images/%v", uuidImage, uuidVersion, uuidLocalImage)) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + _, err = s.handleHTTPError([]int{204}, resp) + return err +} diff --git a/vendor/github.com/scaleway/scaleway-cli/pkg/api/api_test.go b/vendor/github.com/scaleway/scaleway-cli/pkg/api/api_test.go new file mode 100644 index 000000000..de127c947 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-cli/pkg/api/api_test.go @@ -0,0 +1,21 @@ +package api + +import ( + "testing" + + "github.com/scaleway/scaleway-cli/pkg/scwversion" + . "github.com/smartystreets/goconvey/convey" +) + +func TestNewScalewayAPI(t *testing.T) { + Convey("Testing NewScalewayAPI()", t, func() { + api, err := NewScalewayAPI("my-organization", "my-token", scwversion.UserAgent()) + So(err, ShouldBeNil) + So(api, ShouldNotBeNil) + So(api.Token, ShouldEqual, "my-token") + So(api.Organization, ShouldEqual, "my-organization") + So(api.Cache, ShouldNotBeNil) + So(api.client, ShouldNotBeNil) + So(api.Logger, ShouldNotBeNil) + }) +} diff --git a/vendor/github.com/scaleway/scaleway-cli/pkg/api/cache.go b/vendor/github.com/scaleway/scaleway-cli/pkg/api/cache.go new file mode 100644 index 000000000..72d119059 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-cli/pkg/api/cache.go @@ -0,0 +1,731 @@ +// Copyright (C) 2015 Scaleway. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE.md file. + +package api + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + + "github.com/moul/anonuuid" + "github.com/renstrom/fuzzysearch/fuzzy" +) + +const ( + // CacheRegion permits to access at the region field + CacheRegion = iota + // CacheArch permits to access at the arch field + CacheArch + // CacheOwner permits to access at the owner field + CacheOwner + // CacheTitle permits to access at the title field + CacheTitle + // CacheMarketPlaceUUID is used to determine the UUID of local images + CacheMarketPlaceUUID + + // CacheMaxfield is used to determine the size of array + CacheMaxfield +) + +// ScalewayCache is used not to query the API to resolve full identifiers +type ScalewayCache struct { + // Images contains names of Scaleway images indexed by identifier + Images map[string][CacheMaxfield]string `json:"images"` + + // Snapshots contains names of Scaleway snapshots indexed by identifier + Snapshots map[string][CacheMaxfield]string `json:"snapshots"` + + // Volumes contains names of Scaleway volumes indexed by identifier + Volumes map[string][CacheMaxfield]string `json:"volumes"` + + // Bootscripts contains names of Scaleway bootscripts indexed by identifier + Bootscripts map[string][CacheMaxfield]string `json:"bootscripts"` + + // Servers contains names of Scaleway servers indexed by identifier + Servers map[string][CacheMaxfield]string `json:"servers"` + + // Path is the path to the cache file + Path string `json:"-"` + + // Modified tells if the cache needs to be overwritten or not + Modified bool `json:"-"` + + // Lock allows ScalewayCache to be used concurrently + Lock sync.Mutex `json:"-"` +} + +const ( + // IdentifierUnknown is used when we don't know explicitely the type key of the object (used for nil comparison) + IdentifierUnknown = 1 << iota + // IdentifierServer is the type key of cached server objects + IdentifierServer + // IdentifierImage is the type key of cached image objects + IdentifierImage + // IdentifierSnapshot is the type key of cached snapshot objects + IdentifierSnapshot + // IdentifierBootscript is the type key of cached bootscript objects + IdentifierBootscript + // IdentifierVolume is the type key of cached volume objects + IdentifierVolume +) + +// ScalewayResolverResult is a structure containing human-readable information +// about resolver results. This structure is used to display the user choices. +type ScalewayResolverResult struct { + Identifier string + Type int + Name string + Arch string + Needle string + RankMatch int +} + +// ScalewayResolverResults is a list of `ScalewayResolverResult` +type ScalewayResolverResults []ScalewayResolverResult + +// NewScalewayResolverResult returns a new ScalewayResolverResult +func NewScalewayResolverResult(Identifier, Name, Arch string, Type int) ScalewayResolverResult { + if err := anonuuid.IsUUID(Identifier); err != nil { + log.Fatal(err) + } + return ScalewayResolverResult{ + Identifier: Identifier, + Type: Type, + Name: Name, + Arch: Arch, + } +} + +func (s ScalewayResolverResults) Len() int { + return len(s) +} + +func (s ScalewayResolverResults) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s ScalewayResolverResults) Less(i, j int) bool { + return s[i].RankMatch < s[j].RankMatch +} + +// TruncIdentifier returns first 8 characters of an Identifier (UUID) +func (s *ScalewayResolverResult) TruncIdentifier() string { + return s.Identifier[:8] +} + +func identifierTypeName(kind int) string { + switch kind { + case IdentifierServer: + return "Server" + case IdentifierImage: + return "Image" + case IdentifierSnapshot: + return "Snapshot" + case IdentifierVolume: + return "Volume" + case IdentifierBootscript: + return "Bootscript" + } + return "" +} + +// CodeName returns a full resource name with typed prefix +func (s *ScalewayResolverResult) CodeName() string { + name := strings.ToLower(s.Name) + name = regexp.MustCompile(`[^a-z0-9-]`).ReplaceAllString(name, "-") + name = regexp.MustCompile(`--+`).ReplaceAllString(name, "-") + name = strings.Trim(name, "-") + + return fmt.Sprintf("%s:%s", strings.ToLower(identifierTypeName(s.Type)), name) +} + +// FilterByArch deletes the elements which not match with arch +func (s *ScalewayResolverResults) FilterByArch(arch string) { +REDO: + for i := range *s { + if (*s)[i].Arch != arch { + (*s)[i] = (*s)[len(*s)-1] + *s = (*s)[:len(*s)-1] + goto REDO + } + } +} + +// NewScalewayCache loads a per-user cache +func NewScalewayCache() (*ScalewayCache, error) { + homeDir := os.Getenv("HOME") // *nix + if homeDir == "" { // Windows + homeDir = os.Getenv("USERPROFILE") + } + if homeDir == "" { + homeDir = "/tmp" + } + cachePath := filepath.Join(homeDir, ".scw-cache.db") + var cache ScalewayCache + cache.Path = cachePath + _, err := os.Stat(cachePath) + if os.IsNotExist(err) { + cache.Clear() + return &cache, nil + } else if err != nil { + return nil, err + } + file, err := ioutil.ReadFile(cachePath) + if err != nil { + return nil, err + } + err = json.Unmarshal(file, &cache) + if err != nil { + // fix compatibility with older version + if err = os.Remove(cachePath); err != nil { + return nil, err + } + cache.Clear() + return &cache, nil + } + if cache.Images == nil { + cache.Images = make(map[string][CacheMaxfield]string) + } + if cache.Snapshots == nil { + cache.Snapshots = make(map[string][CacheMaxfield]string) + } + if cache.Volumes == nil { + cache.Volumes = make(map[string][CacheMaxfield]string) + } + if cache.Servers == nil { + cache.Servers = make(map[string][CacheMaxfield]string) + } + if cache.Bootscripts == nil { + cache.Bootscripts = make(map[string][CacheMaxfield]string) + } + return &cache, nil +} + +// Clear removes all information from the cache +func (s *ScalewayCache) Clear() { + s.Images = make(map[string][CacheMaxfield]string) + s.Snapshots = make(map[string][CacheMaxfield]string) + s.Volumes = make(map[string][CacheMaxfield]string) + s.Bootscripts = make(map[string][CacheMaxfield]string) + s.Servers = make(map[string][CacheMaxfield]string) + s.Modified = true +} + +// Flush flushes the cache database +func (c *ScalewayCache) Flush() error { + return os.Remove(c.Path) +} + +// Save atomically overwrites the current cache database +func (c *ScalewayCache) Save() error { + c.Lock.Lock() + defer c.Lock.Unlock() + + log.Printf("Writing cache file to disk") + + if c.Modified { + file, err := ioutil.TempFile(filepath.Dir(c.Path), filepath.Base(c.Path)) + if err != nil { + return err + } + defer file.Close() + encoder := json.NewEncoder(file) + err = encoder.Encode(*c) + if err != nil { + os.Remove(file.Name()) + return err + } + + if err := os.Rename(file.Name(), c.Path); err != nil { + os.Remove(file.Name()) + return err + } + } + return nil +} + +// ComputeRankMatch fills `ScalewayResolverResult.RankMatch` with its `fuzzy` score +func (s *ScalewayResolverResult) ComputeRankMatch(needle string) { + s.Needle = needle + s.RankMatch = fuzzy.RankMatch(needle, s.Name) +} + +// LookUpImages attempts to return identifiers matching a pattern +func (c *ScalewayCache) LookUpImages(needle string, acceptUUID bool) ScalewayResolverResults { + c.Lock.Lock() + defer c.Lock.Unlock() + + var res ScalewayResolverResults + + if acceptUUID && anonuuid.IsUUID(needle) == nil { + if fields, ok := c.Images[needle]; ok { + entry := NewScalewayResolverResult(needle, fields[CacheTitle], fields[CacheArch], IdentifierImage) + entry.ComputeRankMatch(needle) + res = append(res, entry) + } + } + + needle = regexp.MustCompile(`^user/`).ReplaceAllString(needle, "") + // FIXME: if 'user/' is in needle, only watch for a user image + nameRegex := regexp.MustCompile(`(?i)` + regexp.MustCompile(`[_-]`).ReplaceAllString(needle, ".*")) + var exactMatches ScalewayResolverResults + for identifier, fields := range c.Images { + if fields[CacheTitle] == needle { + entry := NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], IdentifierImage) + entry.ComputeRankMatch(needle) + exactMatches = append(exactMatches, entry) + } + if strings.HasPrefix(identifier, needle) || nameRegex.MatchString(fields[CacheTitle]) { + entry := NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], IdentifierImage) + entry.ComputeRankMatch(needle) + res = append(res, entry) + } else if strings.HasPrefix(fields[CacheMarketPlaceUUID], needle) || nameRegex.MatchString(fields[CacheMarketPlaceUUID]) { + entry := NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], IdentifierImage) + entry.ComputeRankMatch(needle) + res = append(res, entry) + } + } + + if len(exactMatches) == 1 { + return exactMatches + } + + return removeDuplicatesResults(res) +} + +// LookUpSnapshots attempts to return identifiers matching a pattern +func (c *ScalewayCache) LookUpSnapshots(needle string, acceptUUID bool) ScalewayResolverResults { + c.Lock.Lock() + defer c.Lock.Unlock() + + var res ScalewayResolverResults + + if acceptUUID && anonuuid.IsUUID(needle) == nil { + if fields, ok := c.Snapshots[needle]; ok { + entry := NewScalewayResolverResult(needle, fields[CacheTitle], fields[CacheArch], IdentifierSnapshot) + entry.ComputeRankMatch(needle) + res = append(res, entry) + } + } + + needle = regexp.MustCompile(`^user/`).ReplaceAllString(needle, "") + nameRegex := regexp.MustCompile(`(?i)` + regexp.MustCompile(`[_-]`).ReplaceAllString(needle, ".*")) + var exactMatches ScalewayResolverResults + for identifier, fields := range c.Snapshots { + if fields[CacheTitle] == needle { + entry := NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], IdentifierSnapshot) + entry.ComputeRankMatch(needle) + exactMatches = append(exactMatches, entry) + } + if strings.HasPrefix(identifier, needle) || nameRegex.MatchString(fields[CacheTitle]) { + entry := NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], IdentifierSnapshot) + entry.ComputeRankMatch(needle) + res = append(res, entry) + } + } + + if len(exactMatches) == 1 { + return exactMatches + } + + return removeDuplicatesResults(res) +} + +// LookUpVolumes attempts to return identifiers matching a pattern +func (c *ScalewayCache) LookUpVolumes(needle string, acceptUUID bool) ScalewayResolverResults { + c.Lock.Lock() + defer c.Lock.Unlock() + + var res ScalewayResolverResults + + if acceptUUID && anonuuid.IsUUID(needle) == nil { + if fields, ok := c.Volumes[needle]; ok { + entry := NewScalewayResolverResult(needle, fields[CacheTitle], fields[CacheArch], IdentifierVolume) + entry.ComputeRankMatch(needle) + res = append(res, entry) + } + } + + nameRegex := regexp.MustCompile(`(?i)` + regexp.MustCompile(`[_-]`).ReplaceAllString(needle, ".*")) + var exactMatches ScalewayResolverResults + for identifier, fields := range c.Volumes { + if fields[CacheTitle] == needle { + entry := NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], IdentifierVolume) + entry.ComputeRankMatch(needle) + exactMatches = append(exactMatches, entry) + } + if strings.HasPrefix(identifier, needle) || nameRegex.MatchString(fields[CacheTitle]) { + entry := NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], IdentifierVolume) + entry.ComputeRankMatch(needle) + res = append(res, entry) + } + } + + if len(exactMatches) == 1 { + return exactMatches + } + + return removeDuplicatesResults(res) +} + +// LookUpBootscripts attempts to return identifiers matching a pattern +func (c *ScalewayCache) LookUpBootscripts(needle string, acceptUUID bool) ScalewayResolverResults { + c.Lock.Lock() + defer c.Lock.Unlock() + + var res ScalewayResolverResults + + if acceptUUID && anonuuid.IsUUID(needle) == nil { + if fields, ok := c.Bootscripts[needle]; ok { + entry := NewScalewayResolverResult(needle, fields[CacheTitle], fields[CacheArch], IdentifierBootscript) + entry.ComputeRankMatch(needle) + res = append(res, entry) + } + } + + nameRegex := regexp.MustCompile(`(?i)` + regexp.MustCompile(`[_-]`).ReplaceAllString(needle, ".*")) + var exactMatches ScalewayResolverResults + for identifier, fields := range c.Bootscripts { + if fields[CacheTitle] == needle { + entry := NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], IdentifierBootscript) + entry.ComputeRankMatch(needle) + exactMatches = append(exactMatches, entry) + } + if strings.HasPrefix(identifier, needle) || nameRegex.MatchString(fields[CacheTitle]) { + entry := NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], IdentifierBootscript) + entry.ComputeRankMatch(needle) + res = append(res, entry) + } + } + + if len(exactMatches) == 1 { + return exactMatches + } + + return removeDuplicatesResults(res) +} + +// LookUpServers attempts to return identifiers matching a pattern +func (c *ScalewayCache) LookUpServers(needle string, acceptUUID bool) ScalewayResolverResults { + c.Lock.Lock() + defer c.Lock.Unlock() + + var res ScalewayResolverResults + + if acceptUUID && anonuuid.IsUUID(needle) == nil { + if fields, ok := c.Servers[needle]; ok { + entry := NewScalewayResolverResult(needle, fields[CacheTitle], fields[CacheArch], IdentifierServer) + entry.ComputeRankMatch(needle) + res = append(res, entry) + } + } + + nameRegex := regexp.MustCompile(`(?i)` + regexp.MustCompile(`[_-]`).ReplaceAllString(needle, ".*")) + var exactMatches ScalewayResolverResults + for identifier, fields := range c.Servers { + if fields[CacheTitle] == needle { + entry := NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], IdentifierServer) + entry.ComputeRankMatch(needle) + exactMatches = append(exactMatches, entry) + } + if strings.HasPrefix(identifier, needle) || nameRegex.MatchString(fields[CacheTitle]) { + entry := NewScalewayResolverResult(identifier, fields[CacheTitle], fields[CacheArch], IdentifierServer) + entry.ComputeRankMatch(needle) + res = append(res, entry) + } + } + + if len(exactMatches) == 1 { + return exactMatches + } + + return removeDuplicatesResults(res) +} + +// removeDuplicatesResults transforms an array into a unique array +func removeDuplicatesResults(elements ScalewayResolverResults) ScalewayResolverResults { + encountered := map[string]ScalewayResolverResult{} + + // Create a map of all unique elements. + for v := range elements { + encountered[elements[v].Identifier] = elements[v] + } + + // Place all keys from the map into a slice. + results := ScalewayResolverResults{} + for _, result := range encountered { + results = append(results, result) + } + return results +} + +// parseNeedle parses a user needle and try to extract a forced object type +// i.e: +// - server:blah-blah -> kind=server, needle=blah-blah +// - blah-blah -> kind="", needle=blah-blah +// - not-existing-type:blah-blah +func parseNeedle(input string) (identifierType int, needle string) { + parts := strings.Split(input, ":") + if len(parts) == 2 { + switch parts[0] { + case "server": + return IdentifierServer, parts[1] + case "image": + return IdentifierImage, parts[1] + case "snapshot": + return IdentifierSnapshot, parts[1] + case "bootscript": + return IdentifierBootscript, parts[1] + case "volume": + return IdentifierVolume, parts[1] + } + } + return IdentifierUnknown, input +} + +// LookUpIdentifiers attempts to return identifiers matching a pattern +func (c *ScalewayCache) LookUpIdentifiers(needle string) ScalewayResolverResults { + results := ScalewayResolverResults{} + + identifierType, needle := parseNeedle(needle) + + if identifierType&(IdentifierUnknown|IdentifierServer) > 0 { + for _, result := range c.LookUpServers(needle, false) { + entry := NewScalewayResolverResult(result.Identifier, result.Name, result.Arch, IdentifierServer) + entry.ComputeRankMatch(needle) + results = append(results, entry) + } + } + + if identifierType&(IdentifierUnknown|IdentifierImage) > 0 { + for _, result := range c.LookUpImages(needle, false) { + entry := NewScalewayResolverResult(result.Identifier, result.Name, result.Arch, IdentifierImage) + entry.ComputeRankMatch(needle) + results = append(results, entry) + } + } + + if identifierType&(IdentifierUnknown|IdentifierSnapshot) > 0 { + for _, result := range c.LookUpSnapshots(needle, false) { + entry := NewScalewayResolverResult(result.Identifier, result.Name, result.Arch, IdentifierSnapshot) + entry.ComputeRankMatch(needle) + results = append(results, entry) + } + } + + if identifierType&(IdentifierUnknown|IdentifierVolume) > 0 { + for _, result := range c.LookUpVolumes(needle, false) { + entry := NewScalewayResolverResult(result.Identifier, result.Name, result.Arch, IdentifierVolume) + entry.ComputeRankMatch(needle) + results = append(results, entry) + } + } + + if identifierType&(IdentifierUnknown|IdentifierBootscript) > 0 { + for _, result := range c.LookUpBootscripts(needle, false) { + entry := NewScalewayResolverResult(result.Identifier, result.Name, result.Arch, IdentifierBootscript) + entry.ComputeRankMatch(needle) + results = append(results, entry) + } + } + + return results +} + +// InsertServer registers a server in the cache +func (c *ScalewayCache) InsertServer(identifier, region, arch, owner, name string) { + c.Lock.Lock() + defer c.Lock.Unlock() + + fields, exists := c.Servers[identifier] + if !exists || fields[CacheTitle] != name { + c.Servers[identifier] = [CacheMaxfield]string{region, arch, owner, name} + c.Modified = true + } +} + +// RemoveServer removes a server from the cache +func (c *ScalewayCache) RemoveServer(identifier string) { + c.Lock.Lock() + defer c.Lock.Unlock() + + delete(c.Servers, identifier) + c.Modified = true +} + +// ClearServers removes all servers from the cache +func (c *ScalewayCache) ClearServers() { + c.Lock.Lock() + defer c.Lock.Unlock() + + c.Servers = make(map[string][CacheMaxfield]string) + c.Modified = true +} + +// InsertImage registers an image in the cache +func (c *ScalewayCache) InsertImage(identifier, region, arch, owner, name, marketPlaceUUID string) { + c.Lock.Lock() + defer c.Lock.Unlock() + + fields, exists := c.Images[identifier] + if !exists || fields[CacheTitle] != name { + c.Images[identifier] = [CacheMaxfield]string{region, arch, owner, name, marketPlaceUUID} + c.Modified = true + } +} + +// RemoveImage removes a server from the cache +func (c *ScalewayCache) RemoveImage(identifier string) { + c.Lock.Lock() + defer c.Lock.Unlock() + + delete(c.Images, identifier) + c.Modified = true +} + +// ClearImages removes all images from the cache +func (c *ScalewayCache) ClearImages() { + c.Lock.Lock() + defer c.Lock.Unlock() + + c.Images = make(map[string][CacheMaxfield]string) + c.Modified = true +} + +// InsertSnapshot registers an snapshot in the cache +func (c *ScalewayCache) InsertSnapshot(identifier, region, arch, owner, name string) { + c.Lock.Lock() + defer c.Lock.Unlock() + + fields, exists := c.Snapshots[identifier] + if !exists || fields[CacheTitle] != name { + c.Snapshots[identifier] = [CacheMaxfield]string{region, arch, owner, name} + c.Modified = true + } +} + +// RemoveSnapshot removes a server from the cache +func (c *ScalewayCache) RemoveSnapshot(identifier string) { + c.Lock.Lock() + defer c.Lock.Unlock() + + delete(c.Snapshots, identifier) + c.Modified = true +} + +// ClearSnapshots removes all snapshots from the cache +func (c *ScalewayCache) ClearSnapshots() { + c.Lock.Lock() + defer c.Lock.Unlock() + + c.Snapshots = make(map[string][CacheMaxfield]string) + c.Modified = true +} + +// InsertVolume registers an volume in the cache +func (c *ScalewayCache) InsertVolume(identifier, region, arch, owner, name string) { + c.Lock.Lock() + defer c.Lock.Unlock() + + fields, exists := c.Volumes[identifier] + if !exists || fields[CacheTitle] != name { + c.Volumes[identifier] = [CacheMaxfield]string{region, arch, owner, name} + c.Modified = true + } +} + +// RemoveVolume removes a server from the cache +func (c *ScalewayCache) RemoveVolume(identifier string) { + c.Lock.Lock() + defer c.Lock.Unlock() + + delete(c.Volumes, identifier) + c.Modified = true +} + +// ClearVolumes removes all volumes from the cache +func (c *ScalewayCache) ClearVolumes() { + c.Lock.Lock() + defer c.Lock.Unlock() + + c.Volumes = make(map[string][CacheMaxfield]string) + c.Modified = true +} + +// InsertBootscript registers an bootscript in the cache +func (c *ScalewayCache) InsertBootscript(identifier, region, arch, owner, name string) { + c.Lock.Lock() + defer c.Lock.Unlock() + + fields, exists := c.Bootscripts[identifier] + if !exists || fields[CacheTitle] != name { + c.Bootscripts[identifier] = [CacheMaxfield]string{region, arch, owner, name} + c.Modified = true + } +} + +// RemoveBootscript removes a bootscript from the cache +func (c *ScalewayCache) RemoveBootscript(identifier string) { + c.Lock.Lock() + defer c.Lock.Unlock() + + delete(c.Bootscripts, identifier) + c.Modified = true +} + +// ClearBootscripts removes all bootscripts from the cache +func (c *ScalewayCache) ClearBootscripts() { + c.Lock.Lock() + defer c.Lock.Unlock() + + c.Bootscripts = make(map[string][CacheMaxfield]string) + c.Modified = true +} + +// GetNbServers returns the number of servers in the cache +func (c *ScalewayCache) GetNbServers() int { + c.Lock.Lock() + defer c.Lock.Unlock() + + return len(c.Servers) +} + +// GetNbImages returns the number of images in the cache +func (c *ScalewayCache) GetNbImages() int { + c.Lock.Lock() + defer c.Lock.Unlock() + + return len(c.Images) +} + +// GetNbSnapshots returns the number of snapshots in the cache +func (c *ScalewayCache) GetNbSnapshots() int { + c.Lock.Lock() + defer c.Lock.Unlock() + + return len(c.Snapshots) +} + +// GetNbVolumes returns the number of volumes in the cache +func (c *ScalewayCache) GetNbVolumes() int { + c.Lock.Lock() + defer c.Lock.Unlock() + + return len(c.Volumes) +} + +// GetNbBootscripts returns the number of bootscripts in the cache +func (c *ScalewayCache) GetNbBootscripts() int { + c.Lock.Lock() + defer c.Lock.Unlock() + + return len(c.Bootscripts) +} diff --git a/vendor/github.com/scaleway/scaleway-cli/pkg/api/logger.go b/vendor/github.com/scaleway/scaleway-cli/pkg/api/logger.go new file mode 100644 index 000000000..d14a59dcb --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-cli/pkg/api/logger.go @@ -0,0 +1,49 @@ +// Copyright (C) 2015 Scaleway. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE.md file. + +package api + +import ( + "fmt" + "log" + "net/http" + "os" +) + +// Logger handles logging concerns for the Scaleway API SDK +type Logger interface { + LogHTTP(*http.Request) + Fatalf(format string, v ...interface{}) + Debugf(format string, v ...interface{}) + Infof(format string, v ...interface{}) + Warnf(format string, v ...interface{}) +} + +// NewDefaultLogger returns a logger which is configured for stdout +func NewDefaultLogger() Logger { + return &defaultLogger{ + Logger: log.New(os.Stdout, "", log.LstdFlags), + } +} + +type defaultLogger struct { + *log.Logger +} + +func (l *defaultLogger) LogHTTP(r *http.Request) { + l.Printf("%s %s\n", r.Method, r.URL.Path) +} +func (l *defaultLogger) Fatalf(format string, v ...interface{}) { + l.Printf("[FATAL] %s\n", fmt.Sprintf(format, v)) + os.Exit(1) +} +func (l *defaultLogger) Debugf(format string, v ...interface{}) { + l.Printf("[DEBUG] %s\n", fmt.Sprintf(format, v)) +} +func (l *defaultLogger) Infof(format string, v ...interface{}) { + l.Printf("[INFO ] %s\n", fmt.Sprintf(format, v)) +} +func (l *defaultLogger) Warnf(format string, v ...interface{}) { + l.Printf("[WARN ] %s\n", fmt.Sprintf(format, v)) +} diff --git a/vendor/github.com/scaleway/scaleway-cli/pkg/scwversion/version.go b/vendor/github.com/scaleway/scaleway-cli/pkg/scwversion/version.go new file mode 100644 index 000000000..33e0f3719 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-cli/pkg/scwversion/version.go @@ -0,0 +1,16 @@ +package scwversion + +import "fmt" + +var ( + // VERSION represents the semver version of the package + VERSION = "v1.9.0+dev" + + // GITCOMMIT represents the git commit hash of the package, it is configured at build time + GITCOMMIT string +) + +// UserAgent returns a string to be used by API +func UserAgent() string { + return fmt.Sprintf("scw/%v", VERSION) +} diff --git a/vendor/github.com/scaleway/scaleway-cli/pkg/scwversion/version_test.go b/vendor/github.com/scaleway/scaleway-cli/pkg/scwversion/version_test.go new file mode 100644 index 000000000..39582e39c --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-cli/pkg/scwversion/version_test.go @@ -0,0 +1,14 @@ +package scwversion + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestInit(t *testing.T) { + Convey("Testing init()", t, func() { + So(VERSION, ShouldNotEqual, "") + // So(GITCOMMIT, ShouldNotEqual, "") + }) +} diff --git a/website/source/docs/providers/scaleway/index.html.markdown b/website/source/docs/providers/scaleway/index.html.markdown new file mode 100644 index 000000000..cd1d77ab3 --- /dev/null +++ b/website/source/docs/providers/scaleway/index.html.markdown @@ -0,0 +1,90 @@ +--- +layout: "scaleway" +page_title: "Provider: Scaleway" +sidebar_current: "docs-scaleway-index" +description: |- + The Scaleway provider is used to interact with Scaleway ARM cloud provider. +--- + +# Scaleway Provider + +The Scaleway provider is used to manage Scaleway resources. + +Use the navigation to the left to read about the available resources. + +## Example Usage + +Here is an example that will setup the following: ++ An ARM Server. ++ An IP Address. ++ A security group. + +(create this as sl.tf and run terraform commands from this directory): + +```hcl +provider "scaleway" { + access_key = "" + organization = "" +} + +resource "scaleway_ip" "ip" { + server = "${scaleway_server.test.id}" +} + +resource "scaleway_server" "test" { + name = "test" + image = "aecaed73-51a5-4439-a127-6d8229847145" + type = "C2S" +} + +resource "scaleway_volume" "test" { + name = "test" + size_in_gb = 20 + type = "l_ssd" +} + +resource "scaleway_volume_attachment" "test" { + server = "${scaleway_server.test.id}" + volume = "${scaleway_volume.test.id}" +} + +resource "scaleway_security_group" "http" { + name = "http" + description = "allow HTTP and HTTPS traffic" +} + +resource "scaleway_security_group_rule" "http_accept" { + security_group = "${scaleway_security_group.http.id}" + + action = "accept" + direction = "inbound" + ip_range = "0.0.0.0/0" + protocol = "TCP" + dest_port_from = 80 +} + +resource "scaleway_security_group_rule" "https_accept" { + security_group = "${scaleway_security_group.http.id}" + + action = "accept" + direction = "inbound" + ip_range = "0.0.0.0/0" + protocol = "TCP" + dest_port_from = 443 +} + +``` + +You'll need to provide your Scaleway organization and access key, +so that Terraform can connect. If you don't want to put +credentials in your configuration file, you can leave them +out: + +``` +provider "scaleway" {} +``` + +...and instead set these environment variables: + +- **SCALEWAY_ORGANIZATION**: Your Scaleway organization +- **SCALEWAY_ACCESS_KEY**: Your API Access key diff --git a/website/source/docs/providers/scaleway/r/ip.html.markdown b/website/source/docs/providers/scaleway/r/ip.html.markdown new file mode 100644 index 000000000..cefb412b6 --- /dev/null +++ b/website/source/docs/providers/scaleway/r/ip.html.markdown @@ -0,0 +1,34 @@ +--- +layout: "scaleway" +page_title: "Scaleway: ip" +sidebar_current: "docs-scaleway-resource-ip" +description: |- + Manages Scaleway IPs. +--- + +# scaleway\ip + +Provides IPs for ARM servers. This allows IPs to be created, updated and deleted. +For additional details please refer to [API documentation](https://developer.scaleway.com/#ips). + +## Example Usage + +``` +resource "scaleway_ip" "test_ip" { +} +``` + +## Argument Reference + +The following arguments are supported: + +* `server` - (Optional) ID of ARM server to associate IP with + +Field `server` are editable. + +## Attributes Reference + +The following attributes are exported: + +* `id` - id of the new resource +* `ip` - IP of the new resource diff --git a/website/source/docs/providers/scaleway/r/security_group.html.markdown b/website/source/docs/providers/scaleway/r/security_group.html.markdown new file mode 100644 index 000000000..f02567b28 --- /dev/null +++ b/website/source/docs/providers/scaleway/r/security_group.html.markdown @@ -0,0 +1,36 @@ +--- +layout: "scaleway" +page_title: "Scaleway: security_group" +sidebar_current: "docs-scaleway-resource-security_group" +description: |- + Manages Scaleway security groups. +--- + +# scaleway\security_group + +Provides security groups. This allows security groups to be created, updated and deleted. +For additional details please refer to [API documentation](https://developer.scaleway.com/#security-groups). + +## Example Usage + +``` +resource "scaleway_security_group" "test" { + name = "test" + description = "test" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) name of security group +* `description` - (Required) description of security group + +Field `name`, `description` are editable. + +## Attributes Reference + +The following attributes are exported: + +* `id` - id of the new resource diff --git a/website/source/docs/providers/scaleway/r/security_group_rule.html.markdown b/website/source/docs/providers/scaleway/r/security_group_rule.html.markdown new file mode 100644 index 000000000..de5f695fb --- /dev/null +++ b/website/source/docs/providers/scaleway/r/security_group_rule.html.markdown @@ -0,0 +1,51 @@ +--- +layout: "scaleway" +page_title: "Scaleway: security_group_rule" +sidebar_current: "docs-scaleway-resource-security_group_rule" +description: |- + Manages Scaleway security group rules. +--- + +# scaleway\security_group_rule + +Provides security group rules. This allows security group rules to be created, updated and deleted. +For additional details please refer to [API documentation](https://developer.scaleway.com/#security-groups-manage-rules). + +## Example Usage + +``` +resource "scaleway_security_group" "test" { + name = "test" + description = "test" +} + +resource "scaleway_security_group_rule" "smtp_drop_1" { + security_group = "${scaleway_security_group.test.id}" + + action = "accept" + direction = "inbound" + ip_range = "0.0.0.0/0" + protocol = "TCP" + dest_port_from = 25 +} + +``` + +## Argument Reference + +The following arguments are supported: + +* `action` - (Required) action of rule (`accept`, `drop`) +* `direction` - (Required) direction of rule (`inbound`, `outbound`) +* `ip_range` - (Required) ip_range of rule +* `protocol` - (Required) protocol of rule (`ICMP`, `TCP`, `UDP`) +* `dest_port_from` - (Optional) port range from +* `dest_port_to` - (Optional) port from to + +Field `action`, `direction`, `ip_range`, `protocol`, `dest_port_from`, `dest_port_to` are editable. + +## Attributes Reference + +The following attributes are exported: + +* `id` - id of the new resource diff --git a/website/source/docs/providers/scaleway/r/server.html.markdown b/website/source/docs/providers/scaleway/r/server.html.markdown new file mode 100644 index 000000000..0d6008bab --- /dev/null +++ b/website/source/docs/providers/scaleway/r/server.html.markdown @@ -0,0 +1,38 @@ +--- +layout: "scaleway" +page_title: "Scaleway: server" +sidebar_current: "docs-scaleway-resource-server" +description: |- + Manages Scaleway servers. +--- + +# scaleway\server + +Provides ARM servers. This allows servers to be created, updated and deleted. +For additional details please refer to [API documentation](https://developer.scaleway.com/#servers). + +## Example Usage + +``` +resource "scaleway_server" "test" { + name = "test" + image = "5faef9cd-ea9b-4a63-9171-9e26bec03dbc" + type = "C1" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) name of ARM server +* `image` - (Required) base image of ARM server +* `type` - (Required) type of ARM server + +Field `name`, `type` are editable. + +## Attributes Reference + +The following attributes are exported: + +* `id` - id of the new resource diff --git a/website/source/docs/providers/scaleway/r/volume.html.markdown b/website/source/docs/providers/scaleway/r/volume.html.markdown new file mode 100644 index 000000000..3041bbced --- /dev/null +++ b/website/source/docs/providers/scaleway/r/volume.html.markdown @@ -0,0 +1,44 @@ +--- +layout: "scaleway" +page_title: "Scaleway: volume" +sidebar_current: "docs-scaleway-resource-volume" +description: |- + Manages Scaleway Volumes. +--- + +# scaleway\volume + +Provides ARM volumes. This allows volumes to be created, updated and deleted. +For additional details please refer to [API documentation](https://developer.scaleway.com/#volumes). + +## Example Usage + +``` +resource "scaleway_volume" "test" { + name = "test" + image = "aecaed73-51a5-4439-a127-6d8229847145" + type = "C2S" + volumes = ["${scaleway_volume.test.id}"] +} + +resource "scaleway_volume" "test" { + name = "test" + size_in_gb = 20 + type = "l_ssd" +} + +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) name of volume +* `size_in_gb` - (Required) size of the volume in GB +* `type` - (Required) type of volume + +## Attributes Reference + +The following attributes are exported: + +* `id` - id of the new resource diff --git a/website/source/docs/providers/scaleway/r/volume_attachment.html.markdown b/website/source/docs/providers/scaleway/r/volume_attachment.html.markdown new file mode 100644 index 000000000..271ac1d68 --- /dev/null +++ b/website/source/docs/providers/scaleway/r/volume_attachment.html.markdown @@ -0,0 +1,48 @@ +--- +layout: "scaleway" +page_title: "Scaleway: volume attachment" +sidebar_current: "docs-scaleway-resource-volume attachment" +description: |- + Manages Scaleway Volume attachments for servers. +--- + +# scaleway\volume\_attachment + +This allows volumes to be attached to servers. + +**Warning:** Attaching volumes requires the servers to be powered off. This will lead +to downtime if the server is already in use. + +## Example Usage + +``` +resource "scaleway_server" "test" { + name = "test" + image = "aecaed73-51a5-4439-a127-6d8229847145" + type = "C2S" +} + +resource "scaleway_volume" "test" { + name = "test" + size_in_gb = 20 + type = "l_ssd" +} + +resource "scaleway_volume_attachment" "test" { + server = "${scaleway_server.test.id}" + volume = "${scaleway_volume.test.id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `server` - (Required) id of the server +* `volume` - (Required) id of the volume to be attached + +## Attributes Reference + +The following attributes are exported: + +* `id` - id of the new resource diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index b589bdfb6..287ff0350 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -298,6 +298,10 @@ SoftLayer + > + Scaleway + + > Template diff --git a/website/source/layouts/scaleway.erb b/website/source/layouts/scaleway.erb new file mode 100644 index 000000000..f9ab577c2 --- /dev/null +++ b/website/source/layouts/scaleway.erb @@ -0,0 +1,41 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + + <%= yield %> +<% end %>