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
This commit is contained in:
Raphael Randschau 2016-07-13 22:03:41 +02:00 committed by Paul Stack
parent 0ce6337a2a
commit 9081cabd6e
41 changed files with 6436 additions and 0 deletions

View File

@ -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,
})
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
`

View File

@ -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"
}
`

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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"
}
`

View File

@ -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,

22
vendor/github.com/moul/anonuuid/LICENSE generated vendored Normal file
View File

@ -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.

170
vendor/github.com/moul/anonuuid/README.md generated vendored Normal file
View File

@ -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 <https://github.com/moul>
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 <<EOF | anonuuid
VOLUMES_0_SERVER_ID=15573749-c89d-41dd-a655-16e79bed52e0
VOLUMES_0_SERVER_NAME=hello
VOLUMES_0_ID=c245c3cb-3336-4567-ada1-70cb1fe4eefe
VOLUMES_0_SIZE=50000000000
ORGANIZATION=fe1e54e8-d69d-4f7c-a9f1-42069e03da31
TEST=15573749-c89d-41dd-a655-16e79bed52e0
EOF
VOLUMES_0_SERVER_ID=00000000-0000-0000-0000-000000000000
VOLUMES_0_SERVER_NAME=hello
VOLUMES_0_ID=11111111-1111-1111-1111-111111111111
VOLUMES_0_SIZE=50000000000
ORGANIZATION=22222222-2222-2222-2222-222222222222
TEST=00000000-0000-0000-0000-000000000000
```
---
Inline
```command
$ echo 'VOLUMES_0_SERVER_ID=15573749-c89d-41dd-a655-16e79bed52e0 VOLUMES_0_SERVER_NAME=bitrig1 VOLUMES_0_ID=c245c3cb-3336-4567-ada1-70cb1fe4eefe VOLUMES_0_SIZE=50000000000 ORGANIZATION=fe1e54e8-d69d-4f7c-a9f1-42069e03da31 TEST=15573749-c89d-41dd-a655-16e79bed52e0' | ./anonuuid
VOLUMES_0_SERVER_ID=00000000-0000-0000-0000-000000000000 VOLUMES_0_SERVER_NAME=bitrig1 VOLUMES_0_ID=11111111-1111-1111-1111-111111111111 VOLUMES_0_SIZE=50000000000 ORGANIZATION=22222222-2222-2222-2222-222222222222 TEST=00000000-0000-0000-0000-000000000000
```
---
```command
$ curl -s https://api.pathwar.net/achievements\?max_results\=2 | anonuuid | jq .
{
"_items": [
{
"_updated": "Thu, 30 Apr 2015 13:00:58 GMT",
"description": "You",
"_links": {
"self": {
"href": "achievements/00000000-0000-0000-0000-000000000000",
"title": "achievement"
}
},
"_created": "Thu, 30 Apr 2015 13:00:58 GMT",
"_id": "00000000-0000-0000-0000-000000000000",
"_etag": "b1e9f850accfcb952c58384db41d89728890a69f",
"name": "finish-20-levels"
},
{
"_updated": "Thu, 30 Apr 2015 13:01:07 GMT",
"description": "You",
"_links": {
"self": {
"href": "achievements/11111111-1111-1111-1111-111111111111",
"title": "achievement"
}
},
"_created": "Thu, 30 Apr 2015 13:01:07 GMT",
"_id": "11111111-1111-1111-1111-111111111111",
"_etag": "c346f5e1c4f7658f2dfc4124efa87aba909a9821",
"name": "buy-30-levels"
}
],
"_links": {
"self": {
"href": "achievements?max_results=2",
"title": "achievements"
},
"last": {
"href": "achievements?max_results=2&page=23",
"title": "last page"
},
"parent": {
"href": "/",
"title": "home"
},
"next": {
"href": "achievements?max_results=2&page=2",
"title": "next page"
}
},
"_meta": {
"max_results": 2,
"total": 46,
"page": 1
}
}
```
## Install
Using go
- `go get github.com/moul/anonuuid/...`
## Changelog
### master (unreleased)
* Add mutex to protect the cache field ([@QuentinPerez](https://github.com/QuentinPerez))
* Switch from `Party` to `Godep`
* Support of `--suffix=xxx`, `--keep-beginning` and `--keep-end` options ([#4](https://github.com/moul/anonuuid/issues/4))
* Using **party** to stabilize vendor package versions ([#8](https://github.com/moul/anonuuid/issues/8))
* Add homebrew package ([#6](https://github.com/moul/anonuuid/issues/6))
[full commits list](https://github.com/moul/anonuuid/compare/v1.0.0...master)
### [v1.0.0](https://github.com/moul/anonuuid/releases/tag/v1.0.0) (2015-10-07)
**Initial release**
#### Features
* Support of `--hexspeak` option
* Support of `--random` option
* Support of `--prefix` option
* Anonymize input stream
* Anonymize files
## License
MIT

229
vendor/github.com/moul/anonuuid/anonuuid.go generated vendored Normal file
View File

@ -0,0 +1,229 @@
package anonuuid
import (
"fmt"
"log"
"math/rand"
"regexp"
"strings"
"sync"
"time"
)
var (
// UUIDRegex is the regex used to find UUIDs in texts
UUIDRegex = "[a-z0-9]{8}-[a-z0-9]{4}-[1-5][a-z0-9]{3}-[a-z0-9]{4}-[a-z0-9]{12}"
)
// AnonUUID is the main structure, it contains the cache map and helpers
type AnonUUID struct {
cache map[string]string
guard sync.Mutex // cache guard
// Hexspeak flag will generate hexspeak style fake UUIDs
Hexspeak bool
// Random flag will generate random fake UUIDs
Random bool
// Prefix will be the beginning of all the generated UUIDs
Prefix string
// Suffix will be the end of all the generated UUIDs
Suffix string
// AllowNonUUIDInput tells FakeUUID to accept non UUID input string
AllowNonUUIDInput bool
// KeepBeginning tells FakeUUID to let the beginning of the UUID as it is
KeepBeginning bool
// KeepEnd tells FakeUUID to let the last part of the UUID as it is
KeepEnd bool
}
// Sanitize takes a string as input and return sanitized string
func (a *AnonUUID) Sanitize(input string) string {
r := regexp.MustCompile(UUIDRegex)
return r.ReplaceAllStringFunc(input, func(m string) string {
parts := r.FindStringSubmatch(m)
return a.FakeUUID(parts[0])
})
}
// FakeUUID takes a word (real UUID or standard string) and returns its corresponding (mapped) fakeUUID
func (a *AnonUUID) FakeUUID(input string) string {
if !a.AllowNonUUIDInput {
err := IsUUID(input)
if err != nil {
return "invaliduuid"
}
}
a.guard.Lock()
defer a.guard.Unlock()
if _, ok := a.cache[input]; !ok {
if a.KeepBeginning {
a.Prefix = input[:8]
}
if a.KeepEnd {
a.Suffix = input[36-12:]
}
if a.Prefix != "" {
matched, err := regexp.MatchString("^[a-z0-9]+$", a.Prefix)
if err != nil || !matched {
a.Prefix = "invalidprefix"
}
}
if a.Suffix != "" {
matched, err := regexp.MatchString("^[a-z0-9]+$", a.Suffix)
if err != nil || !matched {
a.Suffix = "invalsuffix"
}
}
var fakeUUID string
var err error
if a.Hexspeak {
fakeUUID, err = GenerateHexspeakUUID(len(a.cache))
} else if a.Random {
fakeUUID, err = GenerateRandomUUID(10)
} else {
fakeUUID, err = GenerateLenUUID(len(a.cache))
}
if err != nil {
log.Fatalf("Failed to generate an UUID: %v", err)
}
if a.Prefix != "" {
fakeUUID, err = PrefixUUID(a.Prefix, fakeUUID)
if err != nil {
panic(err)
}
}
if a.Suffix != "" {
fakeUUID, err = SuffixUUID(a.Suffix, fakeUUID)
if err != nil {
panic(err)
}
}
// FIXME: check for duplicates and retry
a.cache[input] = fakeUUID
}
return a.cache[input]
}
// New returns a prepared AnonUUID structure
func New() *AnonUUID {
return &AnonUUID{
cache: make(map[string]string),
Hexspeak: false,
Random: false,
}
}
func init() {
rand.Seed(time.Now().UTC().UnixNano())
}
// PrefixUUID returns a prefixed UUID
func PrefixUUID(prefix string, uuid string) (string, error) {
uuidLetters := uuid[:8] + uuid[9:13] + uuid[14:18] + uuid[19:23] + uuid[24:36]
prefixedUUID, err := FormatUUID(prefix + uuidLetters)
if err != nil {
return "", err
}
return prefixedUUID, nil
}
// SuffixUUID returns a suffixed UUID
func SuffixUUID(suffix string, uuid string) (string, error) {
uuidLetters := uuid[:8] + uuid[9:13] + uuid[14:18] + uuid[19:23] + uuid[24:36]
uuidLetters = uuidLetters[:32-len(suffix)] + suffix
suffixedUUID, err := FormatUUID(uuidLetters)
if err != nil {
return "", err
}
return suffixedUUID, nil
}
// IsUUID returns nil if the input is an UUID, else it returns an error
func IsUUID(input string) error {
matched, err := regexp.MatchString("^"+UUIDRegex+"$", input)
if err != nil {
return err
}
if !matched {
return fmt.Errorf("String '%s' is not a valid UUID", input)
}
return nil
}
// FormatUUID takes a string in input and return an UUID formatted string by repeating the string and placing dashes if necessary
func FormatUUID(part string) (string, error) {
if len(part) < 1 {
return "", fmt.Errorf("Empty UUID")
}
if len(part) < 32 {
part = strings.Repeat(part, 32)
}
if len(part) > 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))
}

21
vendor/github.com/renstrom/fuzzysearch/LICENSE generated vendored Normal file
View File

@ -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.

167
vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go generated vendored Normal file
View File

@ -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
}

View File

@ -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
}

22
vendor/github.com/scaleway/scaleway-cli/LICENSE.md generated vendored Normal file
View File

@ -0,0 +1,22 @@
The MIT License
===============
Copyright (c) **2014-2016 Scaleway <opensource@scaleway.com> ([@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.

View File

@ -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

2667
vendor/github.com/scaleway/scaleway-cli/pkg/api/api.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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)
})
}

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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, "")
})
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -298,6 +298,10 @@
<a href="/docs/providers/softlayer/index.html">SoftLayer</a>
</li>
<li<%= sidebar_current("docs-providers-scaleway") %>>
<a href="/docs/providers/scaleway/index.html">Scaleway</a>
</li>
<li<%= sidebar_current("docs-providers-template") %>>
<a href="/docs/providers/template/index.html">Template</a>
</li>

View File

@ -0,0 +1,41 @@
<% wrap_layout :inner do %>
<% content_for :sidebar do %>
<div class="docs-sidebar hidden-print affix-top" role="complementary">
<ul class="nav docs-sidenav">
<li<%= sidebar_current("docs-home") %>>
<a href="/docs/providers/index.html">&laquo; Documentation Home</a>
</li>
<li<%= sidebar_current("docs-scaleway-index") %>>
<a href="/docs/providers/scaleway/index.html">Scaleway Provider</a>
</li>
<li<%= sidebar_current(/^docs-scaleway-resource/) %>>
<a href="#">Resources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-scaleway-resource-ip") %>>
<a href="/docs/providers/scaleway/r/ip.html">IP</a>
</li>
<li<%= sidebar_current("docs-scaleway-resource-server") %>>
<a href="/docs/providers/scaleway/r/server.html">server</a>
</li>
<li<%= sidebar_current("docs-scaleway-resource-security_group") %>>
<a href="/docs/providers/scaleway/r/security_group.html">security_group</a>
</li>
<li<%= sidebar_current("docs-scaleway-resource-security_group_rule") %>>
<a href="/docs/providers/scaleway/r/security_group_rule.html">security_group_rule</a>
</li>
<li<%= sidebar_current("docs-scaleway-resource-volume") %>>
<a href="/docs/providers/scaleway/r/volume.html">volume</a>
</li>
<li<%= sidebar_current("docs-scaleway-resource-volume_attachment") %>>
<a href="/docs/providers/scaleway/r/volume_attachment.html">volume_attachment</a>
</li>
</ul>
</li>
</ul>
</div>
<% end %>
<%= yield %>
<% end %>