New Provider: Spotinst (#5001)

* providers/spotinst: Add support for Spotinst resources

* providers/spotinst: Fix merge conflict - layouts/docs.erb

* docs/providers/spotinst: Fix the resource description field

* providers/spotinst: Fix the acceptance tests

* providers/spotinst: Mark the device_index as a required field

* providers/spotinst: Change the associate_public_ip_address field to TypeBool

* docs/providers/spotinst: Update the description of the adjustment field

* providers/spotinst: Rename IamRole to IamInstanceProfile to make it more compatible with the AWS provider

* docs/providers/spotinst: Rename iam_role to iam_instance_profile

* providers/spotinst: Deprecate the iam_role attribute

* providers/spotinst: Fix a misspelled var (IamRole)

* providers/spotinst: Fix possible null pointer exception related to "iam_instance_profile"

* docs/providers/spotinst: Add "load_balancer_names" missing description

* providers/spotinst: New resource "spotinst_subscription" added

* providers/spotinst: Eliminate a possible null pointer exception in "spotinst_aws_group"

* providers/spotinst: Eliminate a possible null pointer exception in "spotinst_subscription"

* providers/spotinst: Mark spotinst_subscription as deleted in destroy

* providers/spotinst: Add support for custom event format in spotinst_subscription

* providers/spotinst: Disable the destroy step of spotinst_subscription

* providers/spotinst: Add support for update subscriptions

* providers/spotinst: Merge fixed conflict - layouts/docs.erb

* providers/spotinst: Vendor dependencies

* providers/spotinst: Return a detailed error message

* provider/spotinst: Update the plugin list

* providers/spotinst: Vendor dependencies using govendor

* providers/spotinst: New resource "spotinst_healthcheck" added

* providers/spotinst: Update the Spotinst SDK

* providers/spotinst: Comment out unnecessary log.Printf

* providers/spotinst: Fix the acceptance tests

* providers/spotinst: Gofmt fixes

* providers/spotinst: Use multiple functions to expand each block

* providers/spotinst: Allow ondemand_count to be zero

* providers/spotinst: Change security_group_ids from TypeSet to TypeList

* providers/spotinst: Remove unnecessary `ForceNew` fields

* providers/spotinst: Update the Spotinst SDK

* providers/spotinst: Add support for capacity unit

* providers/spotinst: Add support for EBS volume pool

* providers/spotinst: Delete health check

* providers/spotinst: Allow to set multiple availability zones

* providers/spotinst: Gofmt

* providers/spotinst: Omit empty strings from the load_balancer_names field

* providers/spotinst: Update the Spotinst SDK to v1.1.9

* providers/spotinst: Add support for new strategy parameters

* providers/spotinst: Update the Spotinst SDK to v1.2.0

* providers/spotinst: Add support for Kubernetes integration

* providers/spotinst: Fix merge conflict - vendor/vendor.json

* providers/spotinst: Update the Spotinst SDK to v1.2.1

* providers/spotinst: Add support for Application Load Balancers

* providers/spotinst: Do not allow to set ondemand_count to 0

* providers/spotinst: Update the Spotinst SDK to v1.2.2

* providers/spotinst: Add support for scaling policy operators

* providers/spotinst: Add dimensions to spotinst_aws_group tests

* providers/spotinst: Allow both ARN and name for IAM instance profiles

* providers/spotinst: Allow ondemand_count=0

* providers/spotinst: Split out the set funcs into flatten style funcs

* providers/spotinst: Update the Spotinst SDK to v1.2.3

* providers/spotinst: Add support for EBS optimized flag

* providers/spotinst: Update the Spotinst SDK to v2.0.0

* providers/spotinst: Use stringutil.Stringify for debugging

* providers/spotinst: Update the Spotinst SDK to v2.0.1

* providers/spotinst: Key pair is now optional

* providers/spotinst: Make sure we do not nullify signals on strategy update

* providers/spotinst: Hash both Strategy and EBS Block Device

* providers/spotinst: Hash AWS load balancer

* providers/spotinst: Update the Spotinst SDK to v2.0.2

* providers/spotinst: Verify namespace exists before appending policy

* providers/spotinst: Image ID will be in a separate block from now on, so as to allow ignoring changes only on the image ID. This change is backwards compatible.

* providers/spotinst: user data decoded when returned from spotinst api, so that TF compares the two states properly, and does not update without cause.
This commit is contained in:
Liran Polak 2017-02-22 22:57:16 +02:00 committed by Paul Stack
parent 9d34612be0
commit f37800ae62
30 changed files with 6211 additions and 2 deletions

View File

@ -0,0 +1,12 @@
package main
import (
"github.com/hashicorp/terraform/builtin/providers/spotinst"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: spotinst.Provider,
})
}

View File

@ -0,0 +1,54 @@
package spotinst
import (
"fmt"
"log"
"github.com/spotinst/spotinst-sdk-go/spotinst"
)
type Config struct {
Email string
Password string
ClientID string
ClientSecret string
Token string
}
// Validate returns an error in case of invalid configuration.
func (c *Config) Validate() error {
msg := "%s\n\nNo valid credentials found for Spotinst Provider.\nPlease see https://www.terraform.io/docs/providers/spotinst/index.html\nfor more information on providing credentials for Spotinst Provider."
if c.Password != "" && c.Token != "" {
err := "ERR_CONFLICT: Both a password and a token were set, only one is required"
return fmt.Errorf(msg, err)
}
if c.Password != "" && (c.Email == "" || c.ClientID == "" || c.ClientSecret == "") {
err := "ERR_MISSING: A password was set without email, client_id or client_secret"
return fmt.Errorf(msg, err)
}
if c.Password == "" && c.Token == "" {
err := "ERR_MISSING: A token is required if not using password"
return fmt.Errorf(msg, err)
}
return nil
}
// Client returns a new client for accessing Spotinst.
func (c *Config) Client() (*spotinst.Client, error) {
var clientOpts []spotinst.ClientOptionFunc
if c.Token != "" {
clientOpts = append(clientOpts, spotinst.SetToken(c.Token))
} else {
clientOpts = append(clientOpts, spotinst.SetCredentials(c.Email, c.Password, c.ClientID, c.ClientSecret))
}
client, err := spotinst.NewClient(clientOpts...)
if err != nil {
return nil, fmt.Errorf("Error setting up client: %s", err)
}
log.Printf("[INFO] Spotinst client configured")
return client, nil
}

View File

@ -0,0 +1,70 @@
package spotinst
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{
"email": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SPOTINST_EMAIL", ""),
Description: "Spotinst Email",
},
"password": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SPOTINST_PASSWORD", ""),
Description: "Spotinst Password",
},
"client_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SPOTINST_CLIENT_ID", ""),
Description: "Spotinst OAuth Client ID",
},
"client_secret": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SPOTINST_CLIENT_SECRET", ""),
Description: "Spotinst OAuth Client Secret",
},
"token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SPOTINST_TOKEN", ""),
Description: "Spotinst Personal API Access Token",
},
},
ResourcesMap: map[string]*schema.Resource{
"spotinst_aws_group": resourceSpotinstAwsGroup(),
"spotinst_subscription": resourceSpotinstSubscription(),
"spotinst_healthcheck": resourceSpotinstHealthCheck(),
},
ConfigureFunc: providerConfigure,
}
}
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{
Email: d.Get("email").(string),
Password: d.Get("password").(string),
ClientID: d.Get("client_id").(string),
ClientSecret: d.Get("client_secret").(string),
Token: d.Get("token").(string),
}
if err := config.Validate(); err != nil {
return nil, err
}
return config.Client()
}

View File

@ -0,0 +1,48 @@
package spotinst
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{
"spotinst": 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) {
c := map[string]string{
"email": os.Getenv("SPOTINST_EMAIL"),
"password": os.Getenv("SPOTINST_PASSWORD"),
"client_id": os.Getenv("SPOTINST_CLIENT_ID"),
"client_secret": os.Getenv("SPOTINST_CLIENT_SECRET"),
"token": os.Getenv("SPOTINST_TOKEN"),
}
if c["password"] != "" && c["token"] != "" {
t.Fatalf("ERR_CONFLICT: Both a password and a token were set, only one is required")
}
if c["password"] != "" && (c["email"] == "" || c["client_id"] == "" || c["client_secret"] == "") {
t.Fatalf("ERR_MISSING: A password was set without email, client_id or client_secret")
}
if c["password"] == "" && c["token"] == "" {
t.Fatalf("ERR_MISSING: A token is required if not using password")
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,215 @@
package spotinst
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/spotinst/spotinst-sdk-go/spotinst"
)
func TestAccSpotinstGroup_Basic(t *testing.T) {
var group spotinst.AwsGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckSpotinstGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckSpotinstGroupConfigBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckSpotinstGroupExists("spotinst_aws_group.foo", &group),
testAccCheckSpotinstGroupAttributes(&group),
resource.TestCheckResourceAttr("spotinst_aws_group.foo", "name", "terraform"),
resource.TestCheckResourceAttr("spotinst_aws_group.foo", "description", "terraform"),
),
},
},
})
}
func TestAccSpotinstGroup_Updated(t *testing.T) {
var group spotinst.AwsGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckSpotinstGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckSpotinstGroupConfigBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckSpotinstGroupExists("spotinst_aws_group.foo", &group),
testAccCheckSpotinstGroupAttributes(&group),
resource.TestCheckResourceAttr("spotinst_aws_group.foo", "name", "terraform"),
resource.TestCheckResourceAttr("spotinst_aws_group.foo", "description", "terraform"),
),
},
{
Config: testAccCheckSpotinstGroupConfigNewValue,
Check: resource.ComposeTestCheckFunc(
testAccCheckSpotinstGroupExists("spotinst_aws_group.foo", &group),
testAccCheckSpotinstGroupAttributesUpdated(&group),
resource.TestCheckResourceAttr("spotinst_aws_group.foo", "name", "terraform_updated"),
resource.TestCheckResourceAttr("spotinst_aws_group.foo", "description", "terraform_updated"),
),
},
},
})
}
func testAccCheckSpotinstGroupDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*spotinst.Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "spotinst_aws_group" {
continue
}
input := &spotinst.ReadAwsGroupInput{ID: spotinst.String(rs.Primary.ID)}
resp, err := client.AwsGroupService.Read(input)
if err == nil && resp != nil && resp.Group != nil {
return fmt.Errorf("Group still exists")
}
}
return nil
}
func testAccCheckSpotinstGroupAttributes(group *spotinst.AwsGroup) resource.TestCheckFunc {
return func(s *terraform.State) error {
if spotinst.StringValue(group.Name) != "terraform" {
return fmt.Errorf("Bad content: %v", group.Name)
}
return nil
}
}
func testAccCheckSpotinstGroupAttributesUpdated(group *spotinst.AwsGroup) resource.TestCheckFunc {
return func(s *terraform.State) error {
if spotinst.StringValue(group.Name) != "terraform_updated" {
return fmt.Errorf("Bad content: %v", group.Name)
}
return nil
}
}
func testAccCheckSpotinstGroupExists(n string, group *spotinst.AwsGroup) 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 resource ID is set")
}
client := testAccProvider.Meta().(*spotinst.Client)
input := &spotinst.ReadAwsGroupInput{ID: spotinst.String(rs.Primary.ID)}
resp, err := client.AwsGroupService.Read(input)
if err != nil {
return err
}
if spotinst.StringValue(resp.Group.Name) != rs.Primary.Attributes["name"] {
return fmt.Errorf("Group not found: %+v,\n %+v\n", resp.Group, rs.Primary.Attributes)
}
*group = *resp.Group
return nil
}
}
const testAccCheckSpotinstGroupConfigBasic = `
resource "spotinst_aws_group" "foo" {
name = "terraform"
description = "terraform"
product = "Linux/UNIX"
capacity {
target = 0
minimum = 0
maximum = 5
}
strategy {
risk = 100
}
instance_types {
ondemand = "c3.large"
spot = ["c3.large", "m4.xlarge"]
}
availability_zone {
name = "us-west-2b"
}
launch_specification {
monitoring = false
image_id = "ami-f0091d91"
key_pair = "east"
security_group_ids = ["default"]
}
scaling_up_policy {
policy_name = "Scaling Policy 1"
metric_name = "CPUUtilization"
statistic = "average"
unit = "percent"
threshold = 80
adjustment = 1
namespace = "AWS/EC2"
operator = "gte"
period = 300
evaluation_periods = 2
cooldown = 300
dimensions {
env = "prod"
}
}
}`
const testAccCheckSpotinstGroupConfigNewValue = `
resource "spotinst_aws_group" "foo" {
name = "terraform_updated"
description = "terraform_updated"
product = "Linux/UNIX"
capacity {
target = 0
minimum = 0
maximum = 5
}
strategy {
risk = 100
}
instance_types {
ondemand = "c3.large"
spot = ["c3.large", "m4.xlarge"]
}
availability_zone {
name = "us-west-2b"
}
launch_specification {
monitoring = false
image_id = "ami-f0091d91"
key_pair = "east"
security_group_ids = ["default"]
}
scaling_up_policy {
policy_name = "Scaling Policy 2"
metric_name = "CPUUtilization"
statistic = "average"
unit = "percent"
threshold = 80
adjustment = 1
namespace = "AWS/EC2"
operator = "gte"
period = 300
evaluation_periods = 2
cooldown = 300
dimensions {
env = "dev"
}
}
}`

View File

@ -0,0 +1,333 @@
package spotinst
import (
"fmt"
"log"
"github.com/hashicorp/terraform/helper/schema"
"github.com/spotinst/spotinst-sdk-go/spotinst"
"github.com/spotinst/spotinst-sdk-go/spotinst/util/stringutil"
)
func resourceSpotinstHealthCheck() *schema.Resource {
return &schema.Resource{
Create: resourceSpotinstHealthCheckCreate,
Update: resourceSpotinstHealthCheckUpdate,
Read: resourceSpotinstHealthCheckRead,
Delete: resourceSpotinstHealthCheckDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"resource_id": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"check": &schema.Schema{
Type: schema.TypeSet,
Required: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"protocol": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"endpoint": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"port": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"interval": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"timeout": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
},
},
},
"threshold": &schema.Schema{
Type: schema.TypeSet,
Required: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"healthy": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"unhealthy": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
},
},
},
"proxy": &schema.Schema{
Type: schema.TypeSet,
Required: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"addr": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"port": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
},
},
},
},
}
}
func resourceSpotinstHealthCheckCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*spotinst.Client)
newHealthCheck, err := buildHealthCheckOpts(d, meta)
if err != nil {
return err
}
log.Printf("[DEBUG] HealthCheck create configuration: %#v\n", newHealthCheck)
input := &spotinst.CreateHealthCheckInput{HealthCheck: newHealthCheck}
resp, err := client.HealthCheckService.Create(input)
if err != nil {
return fmt.Errorf("Error creating health check: %s", err)
}
d.SetId(spotinst.StringValue(resp.HealthCheck.ID))
log.Printf("[INFO] HealthCheck created successfully: %s\n", d.Id())
return resourceSpotinstHealthCheckRead(d, meta)
}
func resourceSpotinstHealthCheckRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*spotinst.Client)
input := &spotinst.ReadHealthCheckInput{ID: spotinst.String(d.Id())}
resp, err := client.HealthCheckService.Read(input)
if err != nil {
return fmt.Errorf("Error retrieving health check: %s", err)
}
if hc := resp.HealthCheck; hc != nil {
d.Set("name", hc.Name)
d.Set("resource_id", hc.ResourceID)
// Set the check.
check := make([]map[string]interface{}, 0, 1)
check = append(check, map[string]interface{}{
"protocol": hc.Check.Protocol,
"endpoint": hc.Check.Endpoint,
"port": hc.Check.Port,
"interval": hc.Check.Interval,
"timeout": hc.Check.Timeout,
})
d.Set("check", check)
// Set the threshold.
threshold := make([]map[string]interface{}, 0, 1)
threshold = append(threshold, map[string]interface{}{
"healthy": hc.Check.Healthy,
"unhealthy": hc.Check.Unhealthy,
})
d.Set("threshold", threshold)
// Set the proxy.
proxy := make([]map[string]interface{}, 0, 1)
proxy = append(proxy, map[string]interface{}{
"addr": hc.Addr,
"port": hc.Port,
})
d.Set("proxy", proxy)
} else {
d.SetId("")
}
return nil
}
func resourceSpotinstHealthCheckUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*spotinst.Client)
healthCheck := &spotinst.HealthCheck{ID: spotinst.String(d.Id())}
update := false
if d.HasChange("name") {
healthCheck.Name = spotinst.String(d.Get("name").(string))
update = true
}
if d.HasChange("resource_id") {
healthCheck.ResourceID = spotinst.String(d.Get("resource_id").(string))
update = true
}
if d.HasChange("check") {
if v, ok := d.GetOk("check"); ok {
if check, err := expandHealthCheckConfig(v); err != nil {
return err
} else {
healthCheck.Check = check
update = true
}
}
}
if d.HasChange("threshold") {
if v, ok := d.GetOk("threshold"); ok {
if threshold, err := expandHealthCheckThreshold(v); err != nil {
return err
} else {
healthCheck.Check.HealthCheckThreshold = threshold
update = true
}
}
}
if d.HasChange("proxy") {
if v, ok := d.GetOk("proxy"); ok {
if proxy, err := expandHealthCheckProxy(v); err != nil {
return err
} else {
healthCheck.HealthCheckProxy = proxy
update = true
}
}
}
if update {
log.Printf("[DEBUG] HealthCheck update configuration: %s\n", stringutil.Stringify(healthCheck))
input := &spotinst.UpdateHealthCheckInput{HealthCheck: healthCheck}
if _, err := client.HealthCheckService.Update(input); err != nil {
return fmt.Errorf("Error updating health check %s: %s", d.Id(), err)
}
}
return resourceSpotinstHealthCheckRead(d, meta)
}
func resourceSpotinstHealthCheckDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*spotinst.Client)
log.Printf("[INFO] Deleting health check: %s\n", d.Id())
input := &spotinst.DeleteHealthCheckInput{ID: spotinst.String(d.Id())}
if _, err := client.HealthCheckService.Delete(input); err != nil {
return fmt.Errorf("Error deleting health check: %s", err)
}
d.SetId("")
return nil
}
// buildHealthCheckOpts builds the Spotinst HealthCheck options.
func buildHealthCheckOpts(d *schema.ResourceData, meta interface{}) (*spotinst.HealthCheck, error) {
healthCheck := &spotinst.HealthCheck{
Name: spotinst.String(d.Get("name").(string)),
ResourceID: spotinst.String(d.Get("resource_id").(string)),
}
if v, ok := d.GetOk("check"); ok {
if check, err := expandHealthCheckConfig(v); err != nil {
return nil, err
} else {
healthCheck.Check = check
}
}
if v, ok := d.GetOk("threshold"); ok {
if threshold, err := expandHealthCheckThreshold(v); err != nil {
return nil, err
} else {
healthCheck.Check.HealthCheckThreshold = threshold
}
}
if v, ok := d.GetOk("proxy"); ok {
if proxy, err := expandHealthCheckProxy(v); err != nil {
return nil, err
} else {
healthCheck.HealthCheckProxy = proxy
}
}
return healthCheck, nil
}
// expandHealthCheckConfig expands the Check block.
func expandHealthCheckConfig(data interface{}) (*spotinst.HealthCheckConfig, error) {
list := data.(*schema.Set).List()
m := list[0].(map[string]interface{})
check := &spotinst.HealthCheckConfig{}
if v, ok := m["protocol"].(string); ok && v != "" {
check.Protocol = spotinst.String(v)
}
if v, ok := m["endpoint"].(string); ok && v != "" {
check.Endpoint = spotinst.String(v)
}
if v, ok := m["port"].(int); ok && v >= 0 {
check.Port = spotinst.Int(v)
}
if v, ok := m["interval"].(int); ok && v >= 0 {
check.Interval = spotinst.Int(v)
}
if v, ok := m["timeout"].(int); ok && v >= 0 {
check.Timeout = spotinst.Int(v)
}
log.Printf("[DEBUG] HealthCheck check configuration: %s\n", stringutil.Stringify(check))
return check, nil
}
// expandHealthCheckThreshold expands the Threshold block.
func expandHealthCheckThreshold(data interface{}) (*spotinst.HealthCheckThreshold, error) {
list := data.(*schema.Set).List()
m := list[0].(map[string]interface{})
threshold := &spotinst.HealthCheckThreshold{}
if v, ok := m["healthy"].(int); ok && v >= 0 {
threshold.Healthy = spotinst.Int(v)
}
if v, ok := m["unhealthy"].(int); ok && v >= 0 {
threshold.Unhealthy = spotinst.Int(v)
}
log.Printf("[DEBUG] HealthCheck threshold configuration: %s\n", stringutil.Stringify(threshold))
return threshold, nil
}
// expandHealthCheckProxy expands the Proxy block.
func expandHealthCheckProxy(data interface{}) (*spotinst.HealthCheckProxy, error) {
list := data.(*schema.Set).List()
m := list[0].(map[string]interface{})
proxy := &spotinst.HealthCheckProxy{}
if v, ok := m["addr"].(string); ok && v != "" {
proxy.Addr = spotinst.String(v)
}
if v, ok := m["port"].(int); ok && v > 0 {
proxy.Port = spotinst.Int(v)
}
log.Printf("[DEBUG] HealthCheck proxy configuration: %s\n", stringutil.Stringify(proxy))
return proxy, nil
}

View File

@ -0,0 +1,160 @@
package spotinst
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/spotinst/spotinst-sdk-go/spotinst"
)
func TestAccSpotinstHealthCheck_Basic(t *testing.T) {
var healthCheck spotinst.HealthCheck
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckSpotinstHealthCheckDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckSpotinstHealthCheckConfigBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckSpotinstHealthCheckExists("spotinst_healthcheck.foo", &healthCheck),
testAccCheckSpotinstHealthCheckAttributes(&healthCheck),
resource.TestCheckResourceAttr("spotinst_healthcheck.foo", "name", "hc-foo"),
),
},
},
})
}
func TestAccSpotinstHealthCheck_Updated(t *testing.T) {
var healthCheck spotinst.HealthCheck
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckSpotinstHealthCheckDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckSpotinstHealthCheckConfigBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckSpotinstHealthCheckExists("spotinst_healthcheck.foo", &healthCheck),
testAccCheckSpotinstHealthCheckAttributes(&healthCheck),
resource.TestCheckResourceAttr("spotinst_healthcheck.foo", "name", "hc-foo"),
),
},
{
Config: testAccCheckSpotinstHealthCheckConfigNewValue,
Check: resource.ComposeTestCheckFunc(
testAccCheckSpotinstHealthCheckExists("spotinst_healthcheck.foo", &healthCheck),
testAccCheckSpotinstHealthCheckAttributesUpdated(&healthCheck),
resource.TestCheckResourceAttr("spotinst_healthcheck.foo", "name", "hc-bar"),
),
},
},
})
}
func testAccCheckSpotinstHealthCheckDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*spotinst.Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "spotinst_healthcheck" {
continue
}
input := &spotinst.ReadHealthCheckInput{ID: spotinst.String(rs.Primary.ID)}
resp, err := client.HealthCheckService.Read(input)
if err == nil && resp != nil && resp.HealthCheck != nil {
return fmt.Errorf("HealthCheck still exists")
}
}
return nil
}
func testAccCheckSpotinstHealthCheckAttributes(healthCheck *spotinst.HealthCheck) resource.TestCheckFunc {
return func(s *terraform.State) error {
if p := spotinst.StringValue(healthCheck.Check.Protocol); p != "http" {
return fmt.Errorf("Bad content: %s", p)
}
if e := spotinst.StringValue(healthCheck.Check.Endpoint); e != "http://endpoint.com" {
return fmt.Errorf("Bad content: %s", e)
}
return nil
}
}
func testAccCheckSpotinstHealthCheckAttributesUpdated(healthCheck *spotinst.HealthCheck) resource.TestCheckFunc {
return func(s *terraform.State) error {
if p := spotinst.StringValue(healthCheck.Check.Protocol); p != "https" {
return fmt.Errorf("Bad content: %s", p)
}
if e := spotinst.StringValue(healthCheck.Check.Endpoint); e != "https://endpoint.com" {
return fmt.Errorf("Bad content: %s", e)
}
return nil
}
}
func testAccCheckSpotinstHealthCheckExists(n string, healthCheck *spotinst.HealthCheck) 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 resource ID is set")
}
client := testAccProvider.Meta().(*spotinst.Client)
input := &spotinst.ReadHealthCheckInput{ID: spotinst.String(rs.Primary.ID)}
resp, err := client.HealthCheckService.Read(input)
if err != nil {
return err
}
if spotinst.StringValue(resp.HealthCheck.ID) != rs.Primary.Attributes["id"] {
return fmt.Errorf("HealthCheck not found: %+v,\n %+v\n", resp.HealthCheck, rs.Primary.Attributes)
}
*healthCheck = *resp.HealthCheck
return nil
}
}
const testAccCheckSpotinstHealthCheckConfigBasic = `
resource "spotinst_healthcheck" "foo" {
name = "hc-foo"
resource_id = "sig-foo"
check {
protocol = "http"
endpoint = "http://endpoint.com"
port = 1337
interval = 10
timeout = 10
}
threshold {
healthy = 1
unhealthy = 1
}
proxy {
addr = "http://proxy.com"
port = 80
}
}`
const testAccCheckSpotinstHealthCheckConfigNewValue = `
resource "spotinst_healthcheck" "foo" {
name = "hc-bar"
resource_id = "sig-foo"
check {
protocol = "https"
endpoint = "https://endpoint.com"
port = 3000
interval = 10
timeout = 10
}
threshold {
healthy = 2
unhealthy = 2
}
proxy {
addr = "http://proxy.com"
port = 8080
}
}`

View File

@ -0,0 +1,145 @@
package spotinst
import (
"fmt"
"log"
"strings"
"github.com/hashicorp/terraform/helper/schema"
"github.com/spotinst/spotinst-sdk-go/spotinst"
"github.com/spotinst/spotinst-sdk-go/spotinst/util/stringutil"
)
func resourceSpotinstSubscription() *schema.Resource {
return &schema.Resource{
Create: resourceSpotinstSubscriptionCreate,
Update: resourceSpotinstSubscriptionUpdate,
Read: resourceSpotinstSubscriptionRead,
Delete: resourceSpotinstSubscriptionDelete,
Schema: map[string]*schema.Schema{
"resource_id": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"event_type": &schema.Schema{
Type: schema.TypeString,
Required: true,
StateFunc: func(v interface{}) string {
value := v.(string)
return strings.ToUpper(value)
},
},
"protocol": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"endpoint": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"format": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
},
},
}
}
func resourceSpotinstSubscriptionCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*spotinst.Client)
newSubscription, err := buildSubscriptionOpts(d, meta)
if err != nil {
return err
}
log.Printf("[DEBUG] Subscription create configuration: %s\n", stringutil.Stringify(newSubscription))
input := &spotinst.CreateSubscriptionInput{Subscription: newSubscription}
resp, err := client.SubscriptionService.Create(input)
if err != nil {
return fmt.Errorf("Error creating subscription: %s", err)
}
d.SetId(spotinst.StringValue(resp.Subscription.ID))
log.Printf("[INFO] Subscription created successfully: %s\n", d.Id())
return resourceSpotinstSubscriptionRead(d, meta)
}
func resourceSpotinstSubscriptionRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*spotinst.Client)
input := &spotinst.ReadSubscriptionInput{ID: spotinst.String(d.Id())}
resp, err := client.SubscriptionService.Read(input)
if err != nil {
return fmt.Errorf("Error retrieving subscription: %s", err)
}
if s := resp.Subscription; s != nil {
d.Set("resource_id", s.ResourceID)
d.Set("event_type", s.EventType)
d.Set("protocol", s.Protocol)
d.Set("endpoint", s.Endpoint)
d.Set("format", s.Format)
} else {
d.SetId("")
}
return nil
}
func resourceSpotinstSubscriptionUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*spotinst.Client)
subscription := &spotinst.Subscription{ID: spotinst.String(d.Id())}
update := false
if d.HasChange("resource_id") {
subscription.ResourceID = spotinst.String(d.Get("resource_id").(string))
update = true
}
if d.HasChange("event_type") {
subscription.EventType = spotinst.String(d.Get("event_type").(string))
update = true
}
if d.HasChange("protocol") {
subscription.Protocol = spotinst.String(d.Get("protocol").(string))
update = true
}
if d.HasChange("endpoint") {
subscription.Endpoint = spotinst.String(d.Get("endpoint").(string))
update = true
}
if d.HasChange("format") {
subscription.Format = d.Get("format").(map[string]interface{})
update = true
}
if update {
log.Printf("[DEBUG] Subscription update configuration: %s\n", stringutil.Stringify(subscription))
input := &spotinst.UpdateSubscriptionInput{Subscription: subscription}
if _, err := client.SubscriptionService.Update(input); err != nil {
return fmt.Errorf("Error updating subscription %s: %s", d.Id(), err)
}
}
return resourceSpotinstSubscriptionRead(d, meta)
}
func resourceSpotinstSubscriptionDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}
// buildSubscriptionOpts builds the Spotinst Subscription options.
func buildSubscriptionOpts(d *schema.ResourceData, meta interface{}) (*spotinst.Subscription, error) {
subscription := &spotinst.Subscription{
ResourceID: spotinst.String(d.Get("resource_id").(string)),
EventType: spotinst.String(strings.ToUpper(d.Get("event_type").(string))),
Protocol: spotinst.String(d.Get("protocol").(string)),
Endpoint: spotinst.String(d.Get("endpoint").(string)),
Format: d.Get("format").(map[string]interface{}),
}
return subscription, nil
}

View File

@ -0,0 +1,144 @@
package spotinst
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/spotinst/spotinst-sdk-go/spotinst"
)
func TestAccSpotinstSubscription_Basic(t *testing.T) {
var subscription spotinst.Subscription
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
//CheckDestroy: testAccCheckSpotinstSubscriptionDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckSpotinstSubscriptionConfigBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckSpotinstSubscriptionExists("spotinst_subscription.foo", &subscription),
testAccCheckSpotinstSubscriptionAttributes(&subscription),
resource.TestCheckResourceAttr("spotinst_subscription.foo", "protocol", "http"),
resource.TestCheckResourceAttr("spotinst_subscription.foo", "endpoint", "http://endpoint.com"),
),
},
},
})
}
func TestAccSpotinstSubscription_Updated(t *testing.T) {
var subscription spotinst.Subscription
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
//CheckDestroy: testAccCheckSpotinstSubscriptionDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckSpotinstSubscriptionConfigBasic,
Check: resource.ComposeTestCheckFunc(
testAccCheckSpotinstSubscriptionExists("spotinst_subscription.foo", &subscription),
testAccCheckSpotinstSubscriptionAttributes(&subscription),
resource.TestCheckResourceAttr("spotinst_subscription.foo", "protocol", "http"),
resource.TestCheckResourceAttr("spotinst_subscription.foo", "endpoint", "http://endpoint.com"),
),
},
{
Config: testAccCheckSpotinstSubscriptionConfigNewValue,
Check: resource.ComposeTestCheckFunc(
testAccCheckSpotinstSubscriptionExists("spotinst_subscription.foo", &subscription),
testAccCheckSpotinstSubscriptionAttributesUpdated(&subscription),
resource.TestCheckResourceAttr("spotinst_subscription.foo", "protocol", "https"),
resource.TestCheckResourceAttr("spotinst_subscription.foo", "endpoint", "https://endpoint.com"),
),
},
},
})
}
func testAccCheckSpotinstSubscriptionDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*spotinst.Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "spotinst_subscription" {
continue
}
input := &spotinst.ReadSubscriptionInput{ID: spotinst.String(rs.Primary.ID)}
if _, err := client.SubscriptionService.Read(input); err == nil {
return fmt.Errorf("Subscription still exists")
}
}
return nil
}
func testAccCheckSpotinstSubscriptionAttributes(subscription *spotinst.Subscription) resource.TestCheckFunc {
return func(s *terraform.State) error {
if p := spotinst.StringValue(subscription.Protocol); p != "http" {
return fmt.Errorf("Bad content: %s", p)
}
if e := spotinst.StringValue(subscription.Endpoint); e != "http://endpoint.com" {
return fmt.Errorf("Bad content: %s", e)
}
return nil
}
}
func testAccCheckSpotinstSubscriptionAttributesUpdated(subscription *spotinst.Subscription) resource.TestCheckFunc {
return func(s *terraform.State) error {
if p := spotinst.StringValue(subscription.Protocol); p != "https" {
return fmt.Errorf("Bad content: %s", p)
}
if e := spotinst.StringValue(subscription.Endpoint); e != "https://endpoint.com" {
return fmt.Errorf("Bad content: %s", e)
}
return nil
}
}
func testAccCheckSpotinstSubscriptionExists(n string, subscription *spotinst.Subscription) 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 resource ID is set")
}
client := testAccProvider.Meta().(*spotinst.Client)
input := &spotinst.ReadSubscriptionInput{ID: spotinst.String(rs.Primary.ID)}
resp, err := client.SubscriptionService.Read(input)
if err != nil {
return err
}
if spotinst.StringValue(resp.Subscription.ID) != rs.Primary.Attributes["id"] {
return fmt.Errorf("Subscription not found: %+v,\n %+v\n", resp.Subscription, rs.Primary.Attributes)
}
*subscription = *resp.Subscription
return nil
}
}
const testAccCheckSpotinstSubscriptionConfigBasic = `
resource "spotinst_subscription" "foo" {
resource_id = "sig-foo"
event_type = "aws_ec2_instance_launch"
protocol = "http"
endpoint = "http://endpoint.com"
format = {
instance_id = "%instance-id%"
tags = "foo,baz,baz"
}
}`
const testAccCheckSpotinstSubscriptionConfigNewValue = `
resource "spotinst_subscription" "foo" {
resource_id = "sig-foo"
event_type = "aws_ec2_instance_launch"
protocol = "https"
endpoint = "https://endpoint.com"
format = {
instance_id = "%instance-id%"
tags = "foo,baz,baz"
}
}`

View File

@ -57,6 +57,7 @@ import (
rundeckprovider "github.com/hashicorp/terraform/builtin/providers/rundeck"
scalewayprovider "github.com/hashicorp/terraform/builtin/providers/scaleway"
softlayerprovider "github.com/hashicorp/terraform/builtin/providers/softlayer"
spotinstprovider "github.com/hashicorp/terraform/builtin/providers/spotinst"
statuscakeprovider "github.com/hashicorp/terraform/builtin/providers/statuscake"
templateprovider "github.com/hashicorp/terraform/builtin/providers/template"
terraformprovider "github.com/hashicorp/terraform/builtin/providers/terraform"
@ -130,6 +131,7 @@ var InternalProviders = map[string]plugin.ProviderFunc{
"rundeck": rundeckprovider.Provider,
"scaleway": scalewayprovider.Provider,
"softlayer": softlayerprovider.Provider,
"spotinst": spotinstprovider.Provider,
"statuscake": statuscakeprovider.Provider,
"template": templateprovider.Provider,
"terraform": terraformprovider.Provider,

View File

@ -0,0 +1,265 @@
package spotinst
import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"net/url"
"time"
)
// DefaultTransport returns a new http.Transport with the same default values
// as http.DefaultTransport, but with idle connections and KeepAlives disabled.
func defaultTransport() *http.Transport {
transport := defaultPooledTransport()
transport.DisableKeepAlives = true
transport.MaxIdleConnsPerHost = -1
return transport
}
// DefaultPooledTransport returns a new http.Transport with similar default
// values to http.DefaultTransport. Do not use this for transient transports as
// it can leak file descriptors over time. Only use this for transports that
// will be re-used for the same host(s).
func defaultPooledTransport() *http.Transport {
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
DisableKeepAlives: false,
MaxIdleConnsPerHost: 1,
}
return transport
}
// defaultHttpClient returns a new http.Client with similar default values to
// http.Client, but with a non-shared Transport, idle connections disabled, and
// KeepAlives disabled.
func defaultHttpClient() *http.Client {
return &http.Client{
Transport: defaultTransport(),
}
}
// defaultHttpPooledClient returns a new http.Client with the same default values
// as http.Client, but with a shared Transport. Do not use this function
// for transient clients as it can leak file descriptors over time. Only use
// this for clients that will be re-used for the same host(s).
func defaultHttpPooledClient() *http.Client {
return &http.Client{
Transport: defaultPooledTransport(),
}
}
// DefaultConfig returns a default configuration for the client. By default this
// will pool and reuse idle connections to API. If you have a long-lived
// client object, this is the desired behavior and should make the most efficient
// use of the connections to API. If you don't reuse a client object , which
// is not recommended, then you may notice idle connections building up over
// time. To avoid this, use the DefaultNonPooledConfig() instead.
func defaultPooledConfig() *clientConfig {
return defaultConfig(defaultPooledTransport)
}
// DefaultNonPooledConfig returns a default configuration for the client which
// does not pool connections. This isn't a recommended configuration because it
// will reconnect to API on every request, but this is useful to avoid the
// accumulation of idle connections if you make many client objects during the
// lifetime of your application.
func defaultNonPooledConfig() *clientConfig {
return defaultConfig(defaultTransport)
}
// defaultConfig returns the default configuration for the client, using the
// given function to make the transport.
func defaultConfig(transportFn func() *http.Transport) *clientConfig {
config := &clientConfig{
apiAddress: DefaultAPIAddress,
oauthAddress: DefaultOAuthAddress,
scheme: DefaultScheme,
userAgent: DefaultUserAgent,
contentType: DefaultContentType,
httpClient: &http.Client{
Transport: transportFn(),
},
}
return config
}
// Client provides a client to the API
type Client struct {
config *clientConfig
AwsGroupService AwsGroupService
HealthCheckService HealthCheckService
SubscriptionService SubscriptionService
}
// NewClient returns a new client
func NewClient(opts ...ClientOptionFunc) (*Client, error) {
config := defaultPooledConfig()
for _, o := range opts {
o(config)
}
client := &Client{config: config}
client.AwsGroupService = &AwsGroupServiceOp{client}
client.HealthCheckService = &HealthCheckServiceOp{client}
client.SubscriptionService = &SubscriptionServiceOp{client}
// Should we request a new access token/refresh token pair?
if creds := config.credentials; creds != nil && creds.Token == "" {
accessToken, _, err := client.obtainOAuthTokens(
creds.Email,
creds.Password,
creds.ClientID,
creds.ClientSecret,
)
if err != nil {
return nil, err
}
config.credentials.Token = accessToken
}
return client, nil
}
type oauthTokenPair struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
}
// obtainOAuthTokens obtains a new access token/refresh token pair.
func (c *Client) obtainOAuthTokens(username, password, clientID, clientSecret string) (string, string, error) {
u := url.URL{
Scheme: c.config.scheme,
Host: c.config.oauthAddress,
Path: "/token",
}
res, err := http.PostForm(u.String(), url.Values{
"grant_type": {"password"},
"username": {username},
"password": {password},
"client_id": {clientID},
"client_secret": {clientSecret},
})
_, resp, err := requireOK(0, res, err)
if err != nil {
return "", "", err
}
defer resp.Body.Close()
pair, err := oauthTokensFromHttpResponse(resp)
if err != nil {
return "", "", err
}
return pair.AccessToken, pair.RefreshToken, nil
}
func oauthTokensFromJSON(in []byte) (*oauthTokenPair, error) {
var rw responseWrapper
if err := json.Unmarshal(in, &rw); err != nil {
return nil, err
}
if len(rw.Response.Items) == 0 {
return nil, fmt.Errorf("invalid or malformed response")
}
out := new(oauthTokenPair)
for _, i := range rw.Response.Items {
if err := json.Unmarshal(i, out); err != nil {
return nil, err
}
}
return out, nil
}
func oauthTokensFromHttpResponse(resp *http.Response) (*oauthTokenPair, error) {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return oauthTokensFromJSON(body)
}
// newRequest is used to create a new request
func (c *Client) newRequest(method, path string) *request {
r := &request{
config: c.config,
method: method,
url: &url.URL{
Scheme: c.config.scheme,
Host: c.config.apiAddress,
Path: path,
},
params: make(map[string][]string),
header: make(http.Header),
}
if creds := c.config.credentials; creds != nil {
if token := creds.Token; token != "" {
r.header.Set("Authorization", "Bearer "+token)
}
}
return r
}
// doRequest runs a request with our client
func (c *Client) doRequest(r *request) (time.Duration, *http.Response, error) {
req, err := r.toHTTP()
if err != nil {
return 0, nil, err
}
c.dumpRequest(req)
start := time.Now()
resp, err := c.config.httpClient.Do(req)
diff := time.Now().Sub(start)
c.dumpResponse(resp)
return diff, resp, err
}
// errorf logs to the error log.
func (c *Client) errorf(format string, args ...interface{}) {
if c.config.errorlog != nil {
c.config.errorlog.Printf(format, args...)
}
}
// infof logs informational messages.
func (c *Client) infof(format string, args ...interface{}) {
if c.config.infolog != nil {
c.config.infolog.Printf(format, args...)
}
}
// tracef logs to the trace log.
func (c *Client) tracef(format string, args ...interface{}) {
if c.config.tracelog != nil {
c.config.tracelog.Printf(format, args...)
}
}
// dumpRequest dumps the given HTTP request to the trace log.
func (c *Client) dumpRequest(r *http.Request) {
if c.config.tracelog != nil && r != nil {
out, err := httputil.DumpRequestOut(r, true)
if err == nil {
c.tracef("%s\n", string(out))
}
}
}
// dumpResponse dumps the given HTTP response to the trace log.
func (c *Client) dumpResponse(resp *http.Response) {
if c.config.tracelog != nil && resp != nil {
out, err := httputil.DumpResponse(resp, true)
if err == nil {
c.tracef("%s\n", string(out))
}
}
}

View File

@ -0,0 +1,176 @@
package spotinst
import (
"fmt"
"net/http"
)
const (
// SDKVersion is the current version of the SDK.
SDKVersion = "2.0.2"
// SDKName is the name of the SDK.
SDKName = "spotinst-sdk-go"
// DefaultAPIAddress is the default address of the Spotinst API.
// It is used e.g. when initializing a new Client without a specific address.
DefaultAPIAddress = "api.spotinst.io"
// DefaultOAuthAddress is the default address of the Spotinst OAuth API.
// It is used e.g. when initializing a new Client without a specific address.
DefaultOAuthAddress = "oauth.spotinst.io"
// DefaultScheme is the default protocol scheme to use when making HTTP
// calls.
DefaultScheme = "https"
// DefaultContentType is the default content type to use when making HTTP
// calls.
DefaultContentType = "application/json"
// DefaultUserAgent is the default user agent to use when making HTTP
// calls.
DefaultUserAgent = SDKName + "/" + SDKVersion
// DefaultMaxRetries is the number of retries for a single request after
// the client will give up and return an error. It is zero by default, so
// retry is disabled by default.
DefaultMaxRetries = 0
// DefaultGzipEnabled specifies if gzip compression is enabled by default.
DefaultGzipEnabled = false
)
// clientConfig is used to configure the creation of a client.
type clientConfig struct {
// address is the address of the API server.
apiAddress string
// oauthAddress is the address of the OAuth server.
oauthAddress string
// scheme is the URI scheme for the API server.
scheme string
// httpClient is the client to use. Default will be
// used if not provided.
httpClient *http.Client
// credentials is used to provide a per-request authorization token.
credentials *credentials
// userAgent is the user agent to use when making HTTP calls.
userAgent string
// contentType is the content type to use when making HTTP calls.
contentType string
// errorf logs to the error log.
errorlog Logger
// infof logs informational messages.
infolog Logger
// tracef logs to the trace log.
tracelog Logger
}
// credentials is used to configure the credentials used by a client.
type credentials struct {
Email string `json:"username"`
Password string `json:"password"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Token string `json:"token"`
}
// ClientOptionFunc is a function that configures a Client.
// It is used in NewClient.
type ClientOptionFunc func(*clientConfig)
// SetAPIAddress defines the address of the Spotinst API.
func SetAPIAddress(addr string) ClientOptionFunc {
return func(c *clientConfig) {
c.apiAddress = addr
}
}
// SetOauthAddress defines the address of the Spotinst OAuth API.
func SetOauthAddress(addr string) ClientOptionFunc {
return func(c *clientConfig) {
c.oauthAddress = addr
}
}
// SetScheme defines the scheme for the address of the Spotinst API.
func SetScheme(scheme string) ClientOptionFunc {
return func(c *clientConfig) {
c.scheme = scheme
}
}
// SetHttpClient defines the HTTP client.
func SetHttpClient(client *http.Client) ClientOptionFunc {
return func(c *clientConfig) {
c.httpClient = client
}
}
// SetToken defines the authorization token.
func SetToken(token string) ClientOptionFunc {
return func(c *clientConfig) {
c.credentials = &credentials{
Token: token,
}
}
}
// SetCredentials defines the authorization credentials.
func SetCredentials(email, password, clientID, clientSecret string) ClientOptionFunc {
return func(c *clientConfig) {
c.credentials = &credentials{
Email: email,
Password: password,
ClientID: clientID,
ClientSecret: clientSecret,
}
}
}
// SetUserAgent defines the user agent.
func SetUserAgent(ua string) ClientOptionFunc {
return func(c *clientConfig) {
c.userAgent = fmt.Sprintf("%s+%s", ua, c.userAgent)
}
}
// SetContentType defines the content type.
func SetContentType(ct string) ClientOptionFunc {
return func(c *clientConfig) {
c.contentType = ct
}
}
// SetErrorLog sets the logger for critical messages like nodes joining
// or leaving the cluster or failing requests. It is nil by default.
func SetErrorLog(logger Logger) ClientOptionFunc {
return func(c *clientConfig) {
c.errorlog = logger
}
}
// SetInfoLog sets the logger for informational messages, e.g. requests
// and their response times. It is nil by default.
func SetInfoLog(logger Logger) ClientOptionFunc {
return func(c *clientConfig) {
c.infolog = logger
}
}
// SetTraceLog specifies the log.Logger to use for output of HTTP requests
// and responses which is helpful during debugging. It is nil by default.
func SetTraceLog(logger Logger) ClientOptionFunc {
return func(c *clientConfig) {
c.tracelog = logger
}
}

View File

@ -0,0 +1,6 @@
package spotinst
// Logger specifies the interface for all log operations.
type Logger interface {
Printf(format string, args ...interface{})
}

View File

@ -0,0 +1,50 @@
package spotinst
import (
"io"
"net/http"
"net/url"
)
// request is used to help build up a request
type request struct {
config *clientConfig
method string
url *url.URL
params url.Values
body io.Reader
header http.Header
obj interface{}
}
// toHTTP converts the request to an HTTP request
func (r *request) toHTTP() (*http.Request, error) {
// Encode the query parameters
r.url.RawQuery = r.params.Encode()
// Check if we should encode the body
if r.body == nil && r.obj != nil {
if b, err := encodeBody(r.obj); err != nil {
return nil, err
} else {
r.body = b
}
}
// Create the HTTP request
req, err := http.NewRequest(r.method, r.url.RequestURI(), r.body)
if err != nil {
return nil, err
}
req.URL.Host = r.url.Host
req.URL.Scheme = r.url.Scheme
req.Host = r.url.Host
req.Header = r.header
req.Header.Set("Content-Type", r.config.contentType)
req.Header.Add("Accept", r.config.contentType)
req.Header.Add("User-Agent", r.config.userAgent)
return req, nil
}

View File

@ -0,0 +1,141 @@
package spotinst
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strconv"
"time"
)
type responseWrapper struct {
Request struct {
ID string `json:"id"`
} `json:"request"`
Response struct {
Errors []responseError `json:"errors"`
Items []json.RawMessage `json:"items"`
} `json:"response"`
}
type responseError struct {
// Error code
Code string `json:"code"`
// Error message
Message string `json:"message"`
// Error field
Field string `json:"field"`
}
// An Error reports the error caused by an API request
type Error struct {
// HTTP response that caused this error
Response *http.Response `json:"-"`
// Error code
Code string `json:"code"`
// Error message
Message string `json:"message"`
// Error field
Field string `json:"field"`
// RequestID returned from the API, useful to contact support.
RequestID string `json:"requestId"`
}
func (e Error) Error() string {
msg := fmt.Sprintf("%v %v: %d (request: %q) %v: %v",
e.Response.Request.Method, e.Response.Request.URL,
e.Response.StatusCode, e.RequestID, e.Code, e.Message)
if e.Field != "" {
msg = fmt.Sprintf("%s (field: %v)", msg, e.Field)
}
return msg
}
type Errors []Error
func (es Errors) Error() string {
var stack string
for _, e := range es {
stack += e.Error() + "\n"
}
return stack
}
// decodeBody is used to JSON decode a body
func decodeBody(resp *http.Response, out interface{}) error {
dec := json.NewDecoder(resp.Body)
return dec.Decode(out)
}
// encodeBody is used to encode a request body
func encodeBody(obj interface{}) (io.Reader, error) {
buf := bytes.NewBuffer(nil)
enc := json.NewEncoder(buf)
if err := enc.Encode(obj); err != nil {
return nil, err
}
return buf, nil
}
// requireOK is used to verify response status code is a successful one (200 OK)
func requireOK(d time.Duration, resp *http.Response, err error) (time.Duration, *http.Response, error) {
if err != nil {
return d, nil, err
}
if resp.StatusCode != http.StatusOK {
err := extractError(resp)
return d, nil, err
}
return d, resp, nil
}
// extractError is used to extract the inner/logical errors from the response
func extractError(resp *http.Response) error {
b := bytes.NewBuffer(make([]byte, 0))
// TeeReader returns a Reader that writes to b
// what it reads from r.Body.
reader := io.TeeReader(resp.Body, b)
defer resp.Body.Close()
resp.Body = ioutil.NopCloser(b)
output := &responseWrapper{}
if err := json.NewDecoder(reader).Decode(output); err != nil {
return err
}
var errors Errors
if errs := output.Response.Errors; len(errs) > 0 {
for _, e := range errs {
err := Error{
Response: resp,
RequestID: output.Request.ID, // TODO(liran): Should be extracted from the X-Request-ID header
Code: e.Code,
Message: e.Message,
Field: e.Field,
}
errors = append(errors, err)
}
} else {
err := Error{
Response: resp,
RequestID: output.Request.ID, // TODO(liran): Should be extracted from the X-Request-ID header
Code: strconv.Itoa(resp.StatusCode),
Message: http.StatusText(resp.StatusCode),
}
errors = append(errors, err)
}
return errors
}

View File

@ -0,0 +1,422 @@
package spotinst
import (
"encoding/json"
"io/ioutil"
"net/http"
"github.com/spotinst/spotinst-sdk-go/spotinst/util/uritemplates"
)
// AwsGroupService is an interface for interfacing with the AwsGroup
// endpoints of the Spotinst API.
type AwsGroupService interface {
List(*ListAwsGroupInput) (*ListAwsGroupOutput, error)
Create(*CreateAwsGroupInput) (*CreateAwsGroupOutput, error)
Read(*ReadAwsGroupInput) (*ReadAwsGroupOutput, error)
Update(*UpdateAwsGroupInput) (*UpdateAwsGroupOutput, error)
Delete(*DeleteAwsGroupInput) (*DeleteAwsGroupOutput, error)
}
// AwsGroupServiceOp handles communication with the balancer related methods
// of the Spotinst API.
type AwsGroupServiceOp struct {
client *Client
}
var _ AwsGroupService = &AwsGroupServiceOp{}
type AwsGroup struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Capacity *AwsGroupCapacity `json:"capacity,omitempty"`
Compute *AwsGroupCompute `json:"compute,omitempty"`
Strategy *AwsGroupStrategy `json:"strategy,omitempty"`
Scaling *AwsGroupScaling `json:"scaling,omitempty"`
Scheduling *AwsGroupScheduling `json:"scheduling,omitempty"`
Integration *AwsGroupIntegration `json:"thirdPartiesIntegration,omitempty"`
}
type AwsGroupIntegration struct {
EC2ContainerService *AwsGroupEC2ContainerServiceIntegration `json:"ecs,omitempty"`
ElasticBeanstalk *AwsGroupElasticBeanstalkIntegration `json:"elasticBeanstalk,omitempty"`
Rancher *AwsGroupRancherIntegration `json:"rancher,omitempty"`
Kubernetes *AwsGroupKubernetesIntegration `json:"kubernetes,omitempty"`
Mesosphere *AwsGroupMesosphereIntegration `json:"mesosphere,omitempty"`
}
type AwsGroupRancherIntegration struct {
MasterHost *string `json:"masterHost,omitempty"`
AccessKey *string `json:"accessKey,omitempty"`
SecretKey *string `json:"secretKey,omitempty"`
}
type AwsGroupElasticBeanstalkIntegration struct {
EnvironmentID *string `json:"environmentId,omitempty"`
}
type AwsGroupEC2ContainerServiceIntegration struct {
ClusterName *string `json:"clusterName,omitempty"`
}
type AwsGroupKubernetesIntegration struct {
Server *string `json:"apiServer,omitempty"`
Token *string `json:"token,omitempty"`
}
type AwsGroupMesosphereIntegration struct {
Server *string `json:"apiServer,omitempty"`
}
type AwsGroupScheduling struct {
Tasks []*AwsGroupScheduledTask `json:"tasks,omitempty"`
}
type AwsGroupScheduledTask struct {
Frequency *string `json:"frequency,omitempty"`
CronExpression *string `json:"cronExpression,omitempty"`
TaskType *string `json:"taskType,omitempty"`
ScaleTargetCapacity *int `json:"scaleTargetCapacity,omitempty"`
ScaleMinCapacity *int `json:"scaleMinCapacity,omitempty"`
ScaleMaxCapacity *int `json:"scaleMaxCapacity,omitempty"`
BatchSizePercentage *int `json:"batchSizePercentage,omitempty"`
GracePeriod *int `json:"gracePeriod,omitempty"`
}
type AwsGroupScaling struct {
Up []*AwsGroupScalingPolicy `json:"up,omitempty"`
Down []*AwsGroupScalingPolicy `json:"down,omitempty"`
}
type AwsGroupScalingPolicy struct {
PolicyName *string `json:"policyName,omitempty"`
MetricName *string `json:"metricName,omitempty"`
Statistic *string `json:"statistic,omitempty"`
Unit *string `json:"unit,omitempty"`
Threshold *float64 `json:"threshold,omitempty"`
Adjustment *int `json:"adjustment,omitempty"`
MinTargetCapacity *int `json:"minTargetCapacity,omitempty"`
MaxTargetCapacity *int `json:"maxTargetCapacity,omitempty"`
Namespace *string `json:"namespace,omitempty"`
EvaluationPeriods *int `json:"evaluationPeriods,omitempty"`
Period *int `json:"period,omitempty"`
Cooldown *int `json:"cooldown,omitempty"`
Operator *string `json:"operator,omitempty"`
Dimensions []*AwsGroupScalingPolicyDimension `json:"dimensions,omitempty"`
}
type AwsGroupScalingPolicyDimension struct {
Name *string `json:"name,omitempty"`
Value *string `json:"value,omitempty"`
}
type AwsGroupStrategy struct {
Risk *float64 `json:"risk,omitempty"`
OnDemandCount *int `json:"onDemandCount,omitempty"`
DrainingTimeout *int `json:"drainingTimeout,omitempty"`
AvailabilityVsCost *string `json:"availabilityVsCost,omitempty"`
UtilizeReservedInstances *bool `json:"utilizeReservedInstances,omitempty"`
FallbackToOnDemand *bool `json:"fallbackToOd,omitempty"`
Signals []*AwsGroupStrategySignal `json:"signals"`
}
type AwsGroupStrategySignal struct {
Name *string `json:"name"`
}
type AwsGroupCapacity struct {
Minimum *int `json:"minimum,omitempty"`
Maximum *int `json:"maximum,omitempty"`
Target *int `json:"target,omitempty"`
Unit *string `json:"unit,omitempty"`
}
type AwsGroupCompute struct {
Product *string `json:"product,omitempty"`
InstanceTypes *AwsGroupComputeInstanceType `json:"instanceTypes,omitempty"`
LaunchSpecification *AwsGroupComputeLaunchSpecification `json:"launchSpecification,omitempty"`
AvailabilityZones []*AwsGroupComputeAvailabilityZone `json:"availabilityZones,omitempty"`
ElasticIPs []string `json:"elasticIps,omitempty"`
EBSVolumePool []*AwsGroupComputeEBSVolume `json:"ebsVolumePool,omitempty"`
}
type AwsGroupComputeEBSVolume struct {
DeviceName *string `json:"deviceName,omitempty"`
VolumeIDs []string `json:"volumeIds,omitempty"`
}
type AwsGroupComputeInstanceType struct {
OnDemand *string `json:"ondemand,omitempty"`
Spot []string `json:"spot,omitempty"`
Weights []*AwsGroupComputeInstanceTypeWeight `json:"weights,omitempty"`
}
type AwsGroupComputeInstanceTypeWeight struct {
InstanceType *string `json:"instanceType,omitempty"`
Weight *int `json:"weightedCapacity,omitempty"`
}
type AwsGroupComputeAvailabilityZone struct {
Name *string `json:"name,omitempty"`
SubnetID *string `json:"subnetId,omitempty"`
}
type AwsGroupComputeLaunchSpecification struct {
LoadBalancerNames []string `json:"loadBalancerNames,omitempty"`
LoadBalancersConfig *AwsGroupComputeLoadBalancersConfig `json:"loadBalancersConfig,omitempty"`
SecurityGroupIDs []string `json:"securityGroupIds,omitempty"`
HealthCheckType *string `json:"healthCheckType,omitempty"`
HealthCheckGracePeriod *int `json:"healthCheckGracePeriod,omitempty"`
ImageID *string `json:"imageId,omitempty"`
KeyPair *string `json:"keyPair,omitempty"`
UserData *string `json:"userData,omitempty"`
Tenancy *string `json:"tenancy,omitempty"`
Monitoring *bool `json:"monitoring,omitempty"`
EBSOptimized *bool `json:"ebsOptimized,omitempty"`
IamInstanceProfile *AwsGroupComputeIamInstanceProfile `json:"iamRole,omitempty"`
BlockDevices []*AwsGroupComputeBlockDevice `json:"blockDeviceMappings,omitempty"`
NetworkInterfaces []*AwsGroupComputeNetworkInterface `json:"networkInterfaces,omitempty"`
Tags []*AwsGroupComputeTag `json:"tags,omitempty"`
}
type AwsGroupComputeLoadBalancersConfig struct {
LoadBalancers []*AwsGroupComputeLoadBalancer `json:"loadBalancers,omitempty"`
}
type AwsGroupComputeLoadBalancer struct {
Name *string `json:"name,omitempty"`
Arn *string `json:"arn,omitempty"`
Type *string `json:"type,omitempty"`
}
type AwsGroupComputeNetworkInterface struct {
ID *string `json:"networkInterfaceId,omitempty"`
Description *string `json:"description,omitempty"`
DeviceIndex *int `json:"deviceIndex,omitempty"`
SecondaryPrivateIPAddressCount *int `json:"secondaryPrivateIpAddressCount,omitempty"`
AssociatePublicIPAddress *bool `json:"associatePublicIpAddress,omitempty"`
DeleteOnTermination *bool `json:"deleteOnTermination,omitempty"`
SecurityGroupsIDs []string `json:"groups,omitempty"`
PrivateIPAddress *string `json:"privateIpAddress,omitempty"`
SubnetID *string `json:"subnetId,omitempty"`
}
type AwsGroupComputeBlockDevice struct {
DeviceName *string `json:"deviceName,omitempty"`
VirtualName *string `json:"virtualName,omitempty"`
EBS *AwsGroupComputeEBS `json:"ebs,omitempty"`
}
type AwsGroupComputeEBS struct {
DeleteOnTermination *bool `json:"deleteOnTermination,omitempty"`
Encrypted *bool `json:"encrypted,omitempty"`
SnapshotID *string `json:"snapshotId,omitempty"`
VolumeType *string `json:"volumeType,omitempty"`
VolumeSize *int `json:"volumeSize,omitempty"`
IOPS *int `json:"iops,omitempty"`
}
type AwsGroupComputeIamInstanceProfile struct {
Name *string `json:"name,omitempty"`
Arn *string `json:"arn,omitempty"`
}
type AwsGroupComputeTag struct {
Key *string `json:"tagKey,omitempty"`
Value *string `json:"tagValue,omitempty"`
}
type ListAwsGroupInput struct{}
type ListAwsGroupOutput struct {
Groups []*AwsGroup `json:"groups,omitempty"`
}
type CreateAwsGroupInput struct {
Group *AwsGroup `json:"group,omitempty"`
}
type CreateAwsGroupOutput struct {
Group *AwsGroup `json:"group,omitempty"`
}
type ReadAwsGroupInput struct {
ID *string `json:"groupId,omitempty"`
}
type ReadAwsGroupOutput struct {
Group *AwsGroup `json:"group,omitempty"`
}
type UpdateAwsGroupInput struct {
Group *AwsGroup `json:"group,omitempty"`
}
type UpdateAwsGroupOutput struct {
Group *AwsGroup `json:"group,omitempty"`
}
type DeleteAwsGroupInput struct {
ID *string `json:"groupId,omitempty"`
}
type DeleteAwsGroupOutput struct{}
func awsGroupFromJSON(in []byte) (*AwsGroup, error) {
b := new(AwsGroup)
if err := json.Unmarshal(in, b); err != nil {
return nil, err
}
return b, nil
}
func awsGroupsFromJSON(in []byte) ([]*AwsGroup, error) {
var rw responseWrapper
if err := json.Unmarshal(in, &rw); err != nil {
return nil, err
}
out := make([]*AwsGroup, len(rw.Response.Items))
if len(out) == 0 {
return out, nil
}
for i, rb := range rw.Response.Items {
b, err := awsGroupFromJSON(rb)
if err != nil {
return nil, err
}
out[i] = b
}
return out, nil
}
func awsGroupsFromHttpResponse(resp *http.Response) ([]*AwsGroup, error) {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return awsGroupsFromJSON(body)
}
func (s *AwsGroupServiceOp) List(input *ListAwsGroupInput) (*ListAwsGroupOutput, error) {
r := s.client.newRequest("GET", "/aws/ec2/group")
_, resp, err := requireOK(s.client.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
gs, err := awsGroupsFromHttpResponse(resp)
if err != nil {
return nil, err
}
return &ListAwsGroupOutput{Groups: gs}, nil
}
func (s *AwsGroupServiceOp) Create(input *CreateAwsGroupInput) (*CreateAwsGroupOutput, error) {
r := s.client.newRequest("POST", "/aws/ec2/group")
r.obj = input
_, resp, err := requireOK(s.client.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
gs, err := awsGroupsFromHttpResponse(resp)
if err != nil {
return nil, err
}
output := new(CreateAwsGroupOutput)
if len(gs) > 0 {
output.Group = gs[0]
}
return output, nil
}
func (s *AwsGroupServiceOp) Read(input *ReadAwsGroupInput) (*ReadAwsGroupOutput, error) {
path, err := uritemplates.Expand("/aws/ec2/group/{groupId}", map[string]string{
"groupId": StringValue(input.ID),
})
if err != nil {
return nil, err
}
r := s.client.newRequest("GET", path)
r.obj = input
_, resp, err := requireOK(s.client.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
gs, err := awsGroupsFromHttpResponse(resp)
if err != nil {
return nil, err
}
output := new(ReadAwsGroupOutput)
if len(gs) > 0 {
output.Group = gs[0]
}
return output, nil
}
func (s *AwsGroupServiceOp) Update(input *UpdateAwsGroupInput) (*UpdateAwsGroupOutput, error) {
path, err := uritemplates.Expand("/aws/ec2/group/{groupId}", map[string]string{
"groupId": StringValue(input.Group.ID),
})
if err != nil {
return nil, err
}
// We do not need the ID anymore so let's drop it.
input.Group.ID = nil
r := s.client.newRequest("PUT", path)
r.obj = input
_, resp, err := requireOK(s.client.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
gs, err := awsGroupsFromHttpResponse(resp)
if err != nil {
return nil, err
}
output := new(UpdateAwsGroupOutput)
if len(gs) > 0 {
output.Group = gs[0]
}
return output, nil
}
func (s *AwsGroupServiceOp) Delete(input *DeleteAwsGroupInput) (*DeleteAwsGroupOutput, error) {
path, err := uritemplates.Expand("/aws/ec2/group/{groupId}", map[string]string{
"groupId": StringValue(input.ID),
})
if err != nil {
return nil, err
}
r := s.client.newRequest("DELETE", path)
r.obj = input
_, resp, err := requireOK(s.client.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
return &DeleteAwsGroupOutput{}, nil
}

View File

@ -0,0 +1,248 @@
package spotinst
import (
"encoding/json"
"io/ioutil"
"net/http"
"github.com/spotinst/spotinst-sdk-go/spotinst/util/uritemplates"
)
// HealthCheck is an interface for interfacing with the HealthCheck
// endpoints of the Spotinst API.
type HealthCheckService interface {
List(*ListHealthCheckInput) (*ListHealthCheckOutput, error)
Create(*CreateHealthCheckInput) (*CreateHealthCheckOutput, error)
Read(*ReadHealthCheckInput) (*ReadHealthCheckOutput, error)
Update(*UpdateHealthCheckInput) (*UpdateHealthCheckOutput, error)
Delete(*DeleteHealthCheckInput) (*DeleteHealthCheckOutput, error)
}
// HealthCheckServiceOp handles communication with the balancer related methods
// of the Spotinst API.
type HealthCheckServiceOp struct {
client *Client
}
var _ HealthCheckService = &HealthCheckServiceOp{}
type HealthCheck struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
ResourceID *string `json:"resourceId,omitempty"`
Check *HealthCheckConfig `json:"check,omitempty"`
*HealthCheckProxy
}
type HealthCheckProxy struct {
Addr *string `json:"proxyAddress,omitempty"`
Port *int `json:"proxyPort,omitempty"`
}
type HealthCheckConfig struct {
Protocol *string `json:"protocol,omitempty"`
Endpoint *string `json:"endpoint,omitempty"`
Port *int `json:"port,omitempty"`
Interval *int `json:"interval,omitempty"`
Timeout *int `json:"timeout,omitempty"`
*HealthCheckThreshold
}
type HealthCheckThreshold struct {
Healthy *int `json:"healthyThreshold,omitempty"`
Unhealthy *int `json:"unhealthyThreshold,omitempty"`
}
type ListHealthCheckInput struct{}
type ListHealthCheckOutput struct {
HealthChecks []*HealthCheck `json:"healthChecks,omitempty"`
}
type CreateHealthCheckInput struct {
HealthCheck *HealthCheck `json:"healthCheck,omitempty"`
}
type CreateHealthCheckOutput struct {
HealthCheck *HealthCheck `json:"healthCheck,omitempty"`
}
type ReadHealthCheckInput struct {
ID *string `json:"healthCheckId,omitempty"`
}
type ReadHealthCheckOutput struct {
HealthCheck *HealthCheck `json:"healthCheck,omitempty"`
}
type UpdateHealthCheckInput struct {
HealthCheck *HealthCheck `json:"healthCheck,omitempty"`
}
type UpdateHealthCheckOutput struct {
HealthCheck *HealthCheck `json:"healthCheck,omitempty"`
}
type DeleteHealthCheckInput struct {
ID *string `json:"healthCheckId,omitempty"`
}
type DeleteHealthCheckOutput struct{}
func healthCheckFromJSON(in []byte) (*HealthCheck, error) {
b := new(HealthCheck)
if err := json.Unmarshal(in, b); err != nil {
return nil, err
}
return b, nil
}
func healthChecksFromJSON(in []byte) ([]*HealthCheck, error) {
var rw responseWrapper
if err := json.Unmarshal(in, &rw); err != nil {
return nil, err
}
out := make([]*HealthCheck, len(rw.Response.Items))
if len(out) == 0 {
return out, nil
}
for i, rb := range rw.Response.Items {
b, err := healthCheckFromJSON(rb)
if err != nil {
return nil, err
}
out[i] = b
}
return out, nil
}
func healthChecksFromHttpResponse(resp *http.Response) ([]*HealthCheck, error) {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return healthChecksFromJSON(body)
}
func (s *HealthCheckServiceOp) List(input *ListHealthCheckInput) (*ListHealthCheckOutput, error) {
r := s.client.newRequest("GET", "/healthCheck")
_, resp, err := requireOK(s.client.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
hcs, err := healthChecksFromHttpResponse(resp)
if err != nil {
return nil, err
}
return &ListHealthCheckOutput{HealthChecks: hcs}, nil
}
func (s *HealthCheckServiceOp) Create(input *CreateHealthCheckInput) (*CreateHealthCheckOutput, error) {
r := s.client.newRequest("POST", "/healthCheck")
r.obj = input
_, resp, err := requireOK(s.client.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
hcs, err := healthChecksFromHttpResponse(resp)
if err != nil {
return nil, err
}
output := new(CreateHealthCheckOutput)
if len(hcs) > 0 {
output.HealthCheck = hcs[0]
}
return output, nil
}
func (s *HealthCheckServiceOp) Read(input *ReadHealthCheckInput) (*ReadHealthCheckOutput, error) {
path, err := uritemplates.Expand("/healthCheck/{healthCheckId}", map[string]string{
"healthCheckId": StringValue(input.ID),
})
if err != nil {
return nil, err
}
r := s.client.newRequest("GET", path)
r.obj = input
_, resp, err := requireOK(s.client.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
hcs, err := healthChecksFromHttpResponse(resp)
if err != nil {
return nil, err
}
output := new(ReadHealthCheckOutput)
if len(hcs) > 0 {
output.HealthCheck = hcs[0]
}
return output, nil
}
func (s *HealthCheckServiceOp) Update(input *UpdateHealthCheckInput) (*UpdateHealthCheckOutput, error) {
path, err := uritemplates.Expand("/healthCheck/{healthCheckId}", map[string]string{
"healthCheckId": StringValue(input.HealthCheck.ID),
})
if err != nil {
return nil, err
}
// We do not need the ID anymore so let's drop it.
input.HealthCheck.ID = nil
r := s.client.newRequest("PUT", path)
r.obj = input
_, resp, err := requireOK(s.client.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
hcs, err := healthChecksFromHttpResponse(resp)
if err != nil {
return nil, err
}
output := new(UpdateHealthCheckOutput)
if len(hcs) > 0 {
output.HealthCheck = hcs[0]
}
return output, nil
}
func (s *HealthCheckServiceOp) Delete(input *DeleteHealthCheckInput) (*DeleteHealthCheckOutput, error) {
path, err := uritemplates.Expand("/healthCheck/{healthCheckId}", map[string]string{
"healthCheckId": StringValue(input.ID),
})
if err != nil {
return nil, err
}
r := s.client.newRequest("DELETE", path)
r.obj = input
_, resp, err := requireOK(s.client.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
return &DeleteHealthCheckOutput{}, nil
}

View File

@ -0,0 +1,230 @@
package spotinst
import (
"encoding/json"
"io/ioutil"
"net/http"
"github.com/spotinst/spotinst-sdk-go/spotinst/util/uritemplates"
)
// Subscription is an interface for interfacing with the Subscription
// endpoints of the Spotinst API.
type SubscriptionService interface {
List(*ListSubscriptionInput) (*ListSubscriptionOutput, error)
Create(*CreateSubscriptionInput) (*CreateSubscriptionOutput, error)
Read(*ReadSubscriptionInput) (*ReadSubscriptionOutput, error)
Update(*UpdateSubscriptionInput) (*UpdateSubscriptionOutput, error)
Delete(*DeleteSubscriptionInput) (*DeleteSubscriptionOutput, error)
}
// SubscriptionServiceOp handles communication with the balancer related methods
// of the Spotinst API.
type SubscriptionServiceOp struct {
client *Client
}
var _ SubscriptionService = &SubscriptionServiceOp{}
type Subscription struct {
ID *string `json:"id,omitempty"`
ResourceID *string `json:"resourceId,omitempty"`
EventType *string `json:"eventType,omitempty"`
Protocol *string `json:"protocol,omitempty"`
Endpoint *string `json:"endpoint,omitempty"`
Format map[string]interface{} `json:"eventFormat,omitempty"`
}
type ListSubscriptionInput struct{}
type ListSubscriptionOutput struct {
Subscriptions []*Subscription `json:"subscriptions,omitempty"`
}
type CreateSubscriptionInput struct {
Subscription *Subscription `json:"subscription,omitempty"`
}
type CreateSubscriptionOutput struct {
Subscription *Subscription `json:"subscription,omitempty"`
}
type ReadSubscriptionInput struct {
ID *string `json:"subscriptionId,omitempty"`
}
type ReadSubscriptionOutput struct {
Subscription *Subscription `json:"subscription,omitempty"`
}
type UpdateSubscriptionInput struct {
Subscription *Subscription `json:"subscription,omitempty"`
}
type UpdateSubscriptionOutput struct {
Subscription *Subscription `json:"subscription,omitempty"`
}
type DeleteSubscriptionInput struct {
ID *string `json:"subscriptionId,omitempty"`
}
type DeleteSubscriptionOutput struct{}
func subscriptionFromJSON(in []byte) (*Subscription, error) {
b := new(Subscription)
if err := json.Unmarshal(in, b); err != nil {
return nil, err
}
return b, nil
}
func subscriptionsFromJSON(in []byte) ([]*Subscription, error) {
var rw responseWrapper
if err := json.Unmarshal(in, &rw); err != nil {
return nil, err
}
out := make([]*Subscription, len(rw.Response.Items))
if len(out) == 0 {
return out, nil
}
for i, rb := range rw.Response.Items {
b, err := subscriptionFromJSON(rb)
if err != nil {
return nil, err
}
out[i] = b
}
return out, nil
}
func subscriptionsFromHttpResponse(resp *http.Response) ([]*Subscription, error) {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return subscriptionsFromJSON(body)
}
func (s *SubscriptionServiceOp) List(input *ListSubscriptionInput) (*ListSubscriptionOutput, error) {
r := s.client.newRequest("GET", "/events/subscription")
_, resp, err := requireOK(s.client.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
gs, err := subscriptionsFromHttpResponse(resp)
if err != nil {
return nil, err
}
return &ListSubscriptionOutput{Subscriptions: gs}, nil
}
func (s *SubscriptionServiceOp) Create(input *CreateSubscriptionInput) (*CreateSubscriptionOutput, error) {
r := s.client.newRequest("POST", "/events/subscription")
r.obj = input
_, resp, err := requireOK(s.client.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
ss, err := subscriptionsFromHttpResponse(resp)
if err != nil {
return nil, err
}
output := new(CreateSubscriptionOutput)
if len(ss) > 0 {
output.Subscription = ss[0]
}
return output, nil
}
func (s *SubscriptionServiceOp) Read(input *ReadSubscriptionInput) (*ReadSubscriptionOutput, error) {
path, err := uritemplates.Expand("/events/subscription/{subscriptionId}", map[string]string{
"subscriptionId": StringValue(input.ID),
})
if err != nil {
return nil, err
}
r := s.client.newRequest("GET", path)
r.obj = input
_, resp, err := requireOK(s.client.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
ss, err := subscriptionsFromHttpResponse(resp)
if err != nil {
return nil, err
}
output := new(ReadSubscriptionOutput)
if len(ss) > 0 {
output.Subscription = ss[0]
}
return output, nil
}
func (s *SubscriptionServiceOp) Update(input *UpdateSubscriptionInput) (*UpdateSubscriptionOutput, error) {
path, err := uritemplates.Expand("/events/subscription/{subscriptionId}", map[string]string{
"subscriptionId": StringValue(input.Subscription.ID),
})
if err != nil {
return nil, err
}
// We do not need the ID anymore so let's drop it.
input.Subscription.ID = nil
r := s.client.newRequest("PUT", path)
r.obj = input
_, resp, err := requireOK(s.client.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
ss, err := subscriptionsFromHttpResponse(resp)
if err != nil {
return nil, err
}
output := new(UpdateSubscriptionOutput)
if len(ss) > 0 {
output.Subscription = ss[0]
}
return output, nil
}
func (s *SubscriptionServiceOp) Delete(input *DeleteSubscriptionInput) (*DeleteSubscriptionOutput, error) {
path, err := uritemplates.Expand("/events/subscription/{subscriptionId}", map[string]string{
"subscriptionId": StringValue(input.ID),
})
if err != nil {
return nil, err
}
r := s.client.newRequest("DELETE", path)
r.obj = input
_, resp, err := requireOK(s.client.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
return &DeleteSubscriptionOutput{}, nil
}

View File

@ -0,0 +1,357 @@
package spotinst
import "time"
// String returns a pointer to of the string value passed in.
func String(v string) *string {
return &v
}
// StringValue returns the value of the string pointer passed in or
// "" if the pointer is nil.
func StringValue(v *string) string {
if v != nil {
return *v
}
return ""
}
// StringSlice converts a slice of string values into a slice of
// string pointers
func StringSlice(src []string) []*string {
dst := make([]*string, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// StringValueSlice converts a slice of string pointers into a slice of
// string values
func StringValueSlice(src []*string) []string {
dst := make([]string, len(src))
for i := 0; i < len(src); i++ {
if src[i] != nil {
dst[i] = *(src[i])
}
}
return dst
}
// StringMap converts a string map of string values into a string
// map of string pointers
func StringMap(src map[string]string) map[string]*string {
dst := make(map[string]*string)
for k, val := range src {
v := val
dst[k] = &v
}
return dst
}
// StringValueMap converts a string map of string pointers into a string
// map of string values
func StringValueMap(src map[string]*string) map[string]string {
dst := make(map[string]string)
for k, val := range src {
if val != nil {
dst[k] = *val
}
}
return dst
}
// Bool returns a pointer to of the bool value passed in.
func Bool(v bool) *bool {
return &v
}
// BoolValue returns the value of the bool pointer passed in or
// false if the pointer is nil.
func BoolValue(v *bool) bool {
if v != nil {
return *v
}
return false
}
// BoolSlice converts a slice of bool values into a slice of
// bool pointers
func BoolSlice(src []bool) []*bool {
dst := make([]*bool, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// BoolValueSlice converts a slice of bool pointers into a slice of
// bool values
func BoolValueSlice(src []*bool) []bool {
dst := make([]bool, len(src))
for i := 0; i < len(src); i++ {
if src[i] != nil {
dst[i] = *(src[i])
}
}
return dst
}
// BoolMap converts a string map of bool values into a string
// map of bool pointers
func BoolMap(src map[string]bool) map[string]*bool {
dst := make(map[string]*bool)
for k, val := range src {
v := val
dst[k] = &v
}
return dst
}
// BoolValueMap converts a string map of bool pointers into a string
// map of bool values
func BoolValueMap(src map[string]*bool) map[string]bool {
dst := make(map[string]bool)
for k, val := range src {
if val != nil {
dst[k] = *val
}
}
return dst
}
// Int returns a pointer to of the int value passed in.
func Int(v int) *int {
return &v
}
// IntValue returns the value of the int pointer passed in or
// 0 if the pointer is nil.
func IntValue(v *int) int {
if v != nil {
return *v
}
return 0
}
// IntSlice converts a slice of int values into a slice of
// int pointers.
func IntSlice(src []int) []*int {
dst := make([]*int, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// IntValueSlice converts a slice of int pointers into a slice of
// int values.
func IntValueSlice(src []*int) []int {
dst := make([]int, len(src))
for i := 0; i < len(src); i++ {
if src[i] != nil {
dst[i] = *(src[i])
}
}
return dst
}
// IntMap converts a string map of int values into a string
// map of int pointers.
func IntMap(src map[string]int) map[string]*int {
dst := make(map[string]*int)
for k, val := range src {
v := val
dst[k] = &v
}
return dst
}
// IntValueMap converts a string map of int pointers into a string
// map of int values.
func IntValueMap(src map[string]*int) map[string]int {
dst := make(map[string]int)
for k, val := range src {
if val != nil {
dst[k] = *val
}
}
return dst
}
// Int64 returns a pointer to of the int64 value passed in.
func Int64(v int64) *int64 {
return &v
}
// Int64Value returns the value of the int64 pointer passed in or
// 0 if the pointer is nil.
func Int64Value(v *int64) int64 {
if v != nil {
return *v
}
return 0
}
// Int64Slice converts a slice of int64 values into a slice of
// int64 pointers.
func Int64Slice(src []int64) []*int64 {
dst := make([]*int64, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// Int64ValueSlice converts a slice of int64 pointers into a slice of
// int64 values.
func Int64ValueSlice(src []*int64) []int64 {
dst := make([]int64, len(src))
for i := 0; i < len(src); i++ {
if src[i] != nil {
dst[i] = *(src[i])
}
}
return dst
}
// Int64Map converts a string map of int64 values into a string
// map of int64 pointers.
func Int64Map(src map[string]int64) map[string]*int64 {
dst := make(map[string]*int64)
for k, val := range src {
v := val
dst[k] = &v
}
return dst
}
// Int64ValueMap converts a string map of int64 pointers into a string
// map of int64 values.
func Int64ValueMap(src map[string]*int64) map[string]int64 {
dst := make(map[string]int64)
for k, val := range src {
if val != nil {
dst[k] = *val
}
}
return dst
}
// Float64 returns a pointer to of the float64 value passed in.
func Float64(v float64) *float64 {
return &v
}
// Float64Value returns the value of the float64 pointer passed in or
// 0 if the pointer is nil.
func Float64Value(v *float64) float64 {
if v != nil {
return *v
}
return 0
}
// Float64Slice converts a slice of float64 values into a slice of
// float64 pointers.
func Float64Slice(src []float64) []*float64 {
dst := make([]*float64, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// Float64ValueSlice converts a slice of float64 pointers into a slice of
// float64 values.
func Float64ValueSlice(src []*float64) []float64 {
dst := make([]float64, len(src))
for i := 0; i < len(src); i++ {
if src[i] != nil {
dst[i] = *(src[i])
}
}
return dst
}
// Float64Map converts a string map of float64 values into a string
// map of float64 pointers.
func Float64Map(src map[string]float64) map[string]*float64 {
dst := make(map[string]*float64)
for k, val := range src {
v := val
dst[k] = &v
}
return dst
}
// Float64ValueMap converts a string map of float64 pointers into a string
// map of float64 values.
func Float64ValueMap(src map[string]*float64) map[string]float64 {
dst := make(map[string]float64)
for k, val := range src {
if val != nil {
dst[k] = *val
}
}
return dst
}
// Time returns a pointer to of the time.Time value passed in.
func Time(v time.Time) *time.Time {
return &v
}
// TimeValue returns the value of the time.Time pointer passed in or
// time.Time{} if the pointer is nil.
func TimeValue(v *time.Time) time.Time {
if v != nil {
return *v
}
return time.Time{}
}
// TimeSlice converts a slice of time.Time values into a slice of
// time.Time pointers.
func TimeSlice(src []time.Time) []*time.Time {
dst := make([]*time.Time, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// TimeValueSlice converts a slice of time.Time pointers into a slice of
// time.Time values.
func TimeValueSlice(src []*time.Time) []time.Time {
dst := make([]time.Time, len(src))
for i := 0; i < len(src); i++ {
if src[i] != nil {
dst[i] = *(src[i])
}
}
return dst
}
// TimeMap converts a string map of time.Time values into a string
// map of time.Time pointers.
func TimeMap(src map[string]time.Time) map[string]*time.Time {
dst := make(map[string]*time.Time)
for k, val := range src {
v := val
dst[k] = &v
}
return dst
}
// TimeValueMap converts a string map of time.Time pointers into a string
// map of time.Time values.
func TimeValueMap(src map[string]*time.Time) map[string]time.Time {
dst := make(map[string]time.Time)
for k, val := range src {
if val != nil {
dst[k] = *val
}
}
return dst
}

View File

@ -0,0 +1,69 @@
package stringutil
import (
"bytes"
"fmt"
"io"
"reflect"
)
// Stringify attempts to create a reasonable string representation of types.
// It does things like resolve pointers to their values and omits struct
// fields with nil values.
func Stringify(message interface{}) string {
var buf bytes.Buffer
v := reflect.ValueOf(message)
stringifyValue(&buf, v)
return buf.String()
}
// stringifyValue was heavily inspired by the goprotobuf library.
func stringifyValue(w io.Writer, val reflect.Value) {
if val.Kind() == reflect.Ptr && val.IsNil() {
w.Write([]byte("<nil>"))
return
}
v := reflect.Indirect(val)
switch v.Kind() {
case reflect.String:
fmt.Fprintf(w, `"%s"`, v)
case reflect.Slice:
w.Write([]byte{'['})
for i := 0; i < v.Len(); i++ {
if i > 0 {
w.Write([]byte{' '})
}
stringifyValue(w, v.Index(i))
}
w.Write([]byte{']'})
return
case reflect.Struct:
if v.Type().Name() != "" {
w.Write([]byte(v.Type().String()))
}
w.Write([]byte{'{'})
var sep bool
for i := 0; i < v.NumField(); i++ {
fv := v.Field(i)
if fv.Kind() == reflect.Ptr && fv.IsNil() {
continue
}
if fv.Kind() == reflect.Slice && fv.IsNil() {
continue
}
if sep {
w.Write([]byte(", "))
} else {
sep = true
}
w.Write([]byte(v.Type().Field(i).Name))
w.Write([]byte{':'})
stringifyValue(w, fv)
}
w.Write([]byte{'}'})
default:
if v.CanInterface() {
fmt.Fprint(w, v.Interface())
}
}
}

View File

@ -0,0 +1,18 @@
Copyright (c) 2013 Joshua Tacoma
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,359 @@
// Copyright 2013 Joshua Tacoma. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package uritemplates is a level 4 implementation of RFC 6570 (URI
// Template, http://tools.ietf.org/html/rfc6570).
//
// To use uritemplates, parse a template string and expand it with a value
// map:
//
// template, _ := uritemplates.Parse("https://api.github.com/repos{/user,repo}")
// values := make(map[string]interface{})
// values["user"] = "jtacoma"
// values["repo"] = "uritemplates"
// expanded, _ := template.ExpandString(values)
// fmt.Printf(expanded)
//
package uritemplates
import (
"bytes"
"errors"
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
)
var (
unreserved = regexp.MustCompile("[^A-Za-z0-9\\-._~]")
reserved = regexp.MustCompile("[^A-Za-z0-9\\-._~:/?#[\\]@!$&'()*+,;=]")
validname = regexp.MustCompile("^([A-Za-z0-9_\\.]|%[0-9A-Fa-f][0-9A-Fa-f])+$")
hex = []byte("0123456789ABCDEF")
)
func pctEncode(src []byte) []byte {
dst := make([]byte, len(src)*3)
for i, b := range src {
buf := dst[i*3 : i*3+3]
buf[0] = 0x25
buf[1] = hex[b/16]
buf[2] = hex[b%16]
}
return dst
}
func escape(s string, allowReserved bool) (escaped string) {
if allowReserved {
escaped = string(reserved.ReplaceAllFunc([]byte(s), pctEncode))
} else {
escaped = string(unreserved.ReplaceAllFunc([]byte(s), pctEncode))
}
return escaped
}
// A UriTemplate is a parsed representation of a URI template.
type UriTemplate struct {
raw string
parts []templatePart
}
// Parse parses a URI template string into a UriTemplate object.
func Parse(rawtemplate string) (template *UriTemplate, err error) {
template = new(UriTemplate)
template.raw = rawtemplate
split := strings.Split(rawtemplate, "{")
template.parts = make([]templatePart, len(split)*2-1)
for i, s := range split {
if i == 0 {
if strings.Contains(s, "}") {
err = errors.New("unexpected }")
break
}
template.parts[i].raw = s
} else {
subsplit := strings.Split(s, "}")
if len(subsplit) != 2 {
err = errors.New("malformed template")
break
}
expression := subsplit[0]
template.parts[i*2-1], err = parseExpression(expression)
if err != nil {
break
}
template.parts[i*2].raw = subsplit[1]
}
}
if err != nil {
template = nil
}
return template, err
}
type templatePart struct {
raw string
terms []templateTerm
first string
sep string
named bool
ifemp string
allowReserved bool
}
type templateTerm struct {
name string
explode bool
truncate int
}
func parseExpression(expression string) (result templatePart, err error) {
switch expression[0] {
case '+':
result.sep = ","
result.allowReserved = true
expression = expression[1:]
case '.':
result.first = "."
result.sep = "."
expression = expression[1:]
case '/':
result.first = "/"
result.sep = "/"
expression = expression[1:]
case ';':
result.first = ";"
result.sep = ";"
result.named = true
expression = expression[1:]
case '?':
result.first = "?"
result.sep = "&"
result.named = true
result.ifemp = "="
expression = expression[1:]
case '&':
result.first = "&"
result.sep = "&"
result.named = true
result.ifemp = "="
expression = expression[1:]
case '#':
result.first = "#"
result.sep = ","
result.allowReserved = true
expression = expression[1:]
default:
result.sep = ","
}
rawterms := strings.Split(expression, ",")
result.terms = make([]templateTerm, len(rawterms))
for i, raw := range rawterms {
result.terms[i], err = parseTerm(raw)
if err != nil {
break
}
}
return result, err
}
func parseTerm(term string) (result templateTerm, err error) {
if strings.HasSuffix(term, "*") {
result.explode = true
term = term[:len(term)-1]
}
split := strings.Split(term, ":")
if len(split) == 1 {
result.name = term
} else if len(split) == 2 {
result.name = split[0]
var parsed int64
parsed, err = strconv.ParseInt(split[1], 10, 0)
result.truncate = int(parsed)
} else {
err = errors.New("multiple colons in same term")
}
if !validname.MatchString(result.name) {
err = errors.New("not a valid name: " + result.name)
}
if result.explode && result.truncate > 0 {
err = errors.New("both explode and prefix modifers on same term")
}
return result, err
}
// Expand expands a URI template with a set of values to produce a string.
func (self *UriTemplate) Expand(value interface{}) (string, error) {
values, ismap := value.(map[string]interface{})
if !ismap {
if m, ismap := struct2map(value); !ismap {
return "", errors.New("expected map[string]interface{}, struct, or pointer to struct.")
} else {
return self.Expand(m)
}
}
var buf bytes.Buffer
for _, p := range self.parts {
err := p.expand(&buf, values)
if err != nil {
return "", err
}
}
return buf.String(), nil
}
func (self *templatePart) expand(buf *bytes.Buffer, values map[string]interface{}) error {
if len(self.raw) > 0 {
buf.WriteString(self.raw)
return nil
}
var zeroLen = buf.Len()
buf.WriteString(self.first)
var firstLen = buf.Len()
for _, term := range self.terms {
value, exists := values[term.name]
if !exists {
continue
}
if buf.Len() != firstLen {
buf.WriteString(self.sep)
}
switch v := value.(type) {
case string:
self.expandString(buf, term, v)
case []interface{}:
self.expandArray(buf, term, v)
case map[string]interface{}:
if term.truncate > 0 {
return errors.New("cannot truncate a map expansion")
}
self.expandMap(buf, term, v)
default:
if m, ismap := struct2map(value); ismap {
if term.truncate > 0 {
return errors.New("cannot truncate a map expansion")
}
self.expandMap(buf, term, m)
} else {
str := fmt.Sprintf("%v", value)
self.expandString(buf, term, str)
}
}
}
if buf.Len() == firstLen {
original := buf.Bytes()[:zeroLen]
buf.Reset()
buf.Write(original)
}
return nil
}
func (self *templatePart) expandName(buf *bytes.Buffer, name string, empty bool) {
if self.named {
buf.WriteString(name)
if empty {
buf.WriteString(self.ifemp)
} else {
buf.WriteString("=")
}
}
}
func (self *templatePart) expandString(buf *bytes.Buffer, t templateTerm, s string) {
if len(s) > t.truncate && t.truncate > 0 {
s = s[:t.truncate]
}
self.expandName(buf, t.name, len(s) == 0)
buf.WriteString(escape(s, self.allowReserved))
}
func (self *templatePart) expandArray(buf *bytes.Buffer, t templateTerm, a []interface{}) {
if len(a) == 0 {
return
} else if !t.explode {
self.expandName(buf, t.name, false)
}
for i, value := range a {
if t.explode && i > 0 {
buf.WriteString(self.sep)
} else if i > 0 {
buf.WriteString(",")
}
var s string
switch v := value.(type) {
case string:
s = v
default:
s = fmt.Sprintf("%v", v)
}
if len(s) > t.truncate && t.truncate > 0 {
s = s[:t.truncate]
}
if self.named && t.explode {
self.expandName(buf, t.name, len(s) == 0)
}
buf.WriteString(escape(s, self.allowReserved))
}
}
func (self *templatePart) expandMap(buf *bytes.Buffer, t templateTerm, m map[string]interface{}) {
if len(m) == 0 {
return
}
if !t.explode {
self.expandName(buf, t.name, len(m) == 0)
}
var firstLen = buf.Len()
for k, value := range m {
if firstLen != buf.Len() {
if t.explode {
buf.WriteString(self.sep)
} else {
buf.WriteString(",")
}
}
var s string
switch v := value.(type) {
case string:
s = v
default:
s = fmt.Sprintf("%v", v)
}
if t.explode {
buf.WriteString(escape(k, self.allowReserved))
buf.WriteRune('=')
buf.WriteString(escape(s, self.allowReserved))
} else {
buf.WriteString(escape(k, self.allowReserved))
buf.WriteRune(',')
buf.WriteString(escape(s, self.allowReserved))
}
}
}
func struct2map(v interface{}) (map[string]interface{}, bool) {
value := reflect.ValueOf(v)
switch value.Type().Kind() {
case reflect.Ptr:
return struct2map(value.Elem().Interface())
case reflect.Struct:
m := make(map[string]interface{})
for i := 0; i < value.NumField(); i++ {
tag := value.Type().Field(i).Tag
var name string
if strings.Contains(string(tag), ":") {
name = tag.Get("uri")
} else {
name = strings.TrimSpace(string(tag))
}
if len(name) == 0 {
name = value.Type().Field(i).Name
}
m[name] = value.Field(i).Interface()
}
return m, true
}
return nil, false
}

View File

@ -0,0 +1,13 @@
package uritemplates
func Expand(path string, expansions map[string]string) (string, error) {
template, err := Parse(path)
if err != nil {
return "", err
}
values := make(map[string]interface{})
for k, v := range expansions {
values[k] = v
}
return template.Expand(values)
}

19
vendor/vendor.json vendored
View File

@ -2515,7 +2515,24 @@
"revisionTime": "2015-01-08T08:49:52Z"
},
{
"checksumSHA1": "GQ9bu6PuydK3Yor1JgtVKUfEJm8=",
"checksumSHA1": "bxWb+Qr/L8+ju1G0KvPAjYHy8cM=",
"path": "github.com/spotinst/spotinst-sdk-go/spotinst",
"revision": "e9a0995904e38e1ed662fbd559760e11db8c2a0b",
"revisionTime": "2017-02-13T08:20:48Z"
},
{
"checksumSHA1": "/oApHzKZwKYrVFEosDhHV4y3sbQ=",
"path": "github.com/spotinst/spotinst-sdk-go/spotinst/util/stringutil",
"revision": "16e2840b013b6b365431813bcccd9800de350d79",
"revisionTime": "2017-01-31T14:42:13Z"
},
{
"checksumSHA1": "F8PDlBY72o5HHNLXpB4X1itP70w=",
"path": "github.com/spotinst/spotinst-sdk-go/spotinst/util/uritemplates",
"revision": "e9a0995904e38e1ed662fbd559760e11db8c2a0b",
"revisionTime": "2017-02-13T08:20:48Z"
},
{
"path": "github.com/tent/http-link-go",
"revision": "ac974c61c2f990f4115b119354b5e0b47550e888",
"revisionTime": "2013-07-02T22:55:49Z"

View File

@ -57,6 +57,7 @@ body.layout-rancher,
body.layout-random,
body.layout-rundeck,
body.layout-scaleway,
body.layout-spotinst,
body.layout-statuscake,
body.layout-softlayer,
body.layout-template,

View File

@ -0,0 +1,43 @@
---
layout: "spotinst"
page_title: "Provider: Spotinst"
sidebar_current: "docs-spotinst-index"
description: |-
The Spotinst provider is used to interact with the resources supported by Spotinst. The provider needs to be configured with the proper credentials before it can be used.
---
# Spotinst Provider
The Spotinst provider is used to interact with the
resources supported by Spotinst. The provider needs to be configured
with the proper credentials before it can be used.
Use the navigation to the left to read about the available resources.
## Example Usage
```
# Configure the Spotinst provider
provider "spotinst" {
email = "${var.spotinst_email}"
password = "${var.spotinst_password}"
client_id = "${var.spotinst_client_id}"
client_secret = "${var.spotinst_client_secret}"
token = "${var.spotinst_token}"
}
# Create an AWS group
resource "spotinst_aws_group" "foo" {
...
}
```
## Argument Reference
The following arguments are supported:
* `email` - (Required) The email registered in Spotinst. It must be provided, but it can also be sourced from the `SPOTINST_EMAIL` environment variable.
* `password` - (Optional; Required if not using `token`) The password associated with the username. It can be sourced from the `SPOTINST_PASSWORD` environment variable.
* `client_id` - (Optional; Required if not using `token`) The OAuth client ID associated with the username. It can be sourced from the `SPOTINST_CLIENT_ID` environment variable.
* `client_secret` - (Optional; Required if not using `token`) The OAuth client secret associated with the username. It can be sourced from the `SPOTINST_CLIENT_SECRET` environment variable.
* `token` - (Optional; Required if not using `password`) A Personal API Access Token issued by Spotinst. It can be sourced from the `SPOTINST_TOKEN` environment variable.

View File

@ -0,0 +1,245 @@
---
layout: "spotinst"
page_title: "Spotinst: aws_group"
sidebar_current: "docs-do-resource-aws_group"
description: |-
Provides a Spotinst AWS group resource.
---
# spotinst_aws_group
Provides a Spotinst AWS group resource.
## Example Usage
```
# Create an AWS group
resource "spotinst_aws_group" "workers" {
name = "workers-group"
description = "created by Terraform"
product = "Linux/UNIX"
capacity {
target = 50
minimum = 25
maximum = 100
}
strategy {
risk = 100
}
scheduled_task {
task_type = "scale"
cron_expression = "0 5 * * 0-4"
scale_target_capacity = 80
}
scheduled_task {
task_type = "backup_ami"
frequency = "hourly"
}
instance_types {
ondemand = "c3.large"
spot = ["m3.large", "m4.large", "c3.large", "c4.large"]
}
availability_zone {
name = "us-west-2b"
subnet_id = "subnet-7bbbf51e"
}
launch_specification {
monitoring = false
image_id = "ami-f0091d91"
key_pair = "pemfile"
security_group_ids = ["default", "allow-ssh"]
}
tags {
foo = "bar"
bar = "baz"
}
scaling_up_policy {
policy_name = "Scaling Policy 1"
metric_name = "CPUUtilization"
statistic = "average"
unit = "percent"
threshold = 80
adjustment = 1
namespace = "AWS/EC2"
period = 300
evaluation_periods = 2
cooldown = 300
}
scaling_down_policy {
policy_name = "Scaling Policy 2"
metric_name = "CPUUtilization"
statistic = "average"
unit = "percent"
threshold = 40
adjustment = 1
namespace = "AWS/EC2"
period = 300
evaluation_periods = 2
cooldown = 300
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Optional) The group description.
* `description` - (Optional) The group description.
* `product` - (Required) Operation system type.
* `capacity` - (Required) The group capacity. Only a single block is allowed.
* `target` - (Required) The desired number of instances the group should have at any time.
* `minimum` - (Optional; Required if using scaling policies) The minimum number of instances the group should have at any time.
* `maximum` - (Optional; Required if using scaling policies) The maximum number of instances the group should have at any time.
* `strategy` - (Required) This determines how your group request is fulfilled from the possible On-Demand and Spot pools selected for launch. Only a single block is allowed.
* `risk` - (Optional; Required if not using `ondemand_count`) The percentage of Spot instances that would spin up from the `capcity.target` number.
* `ondemand_count` - (Optional; Required if not using `risk`) Number of on demand instances to launch in the group. All other instances will be spot instances. When this parameter is set the "risk" parameter is being ignored.
* `availability_vs_cost` - (Optional) The percentage of Spot instances that would spin up from the `capcity.target` number.
* `draining_timeout` - (Optional) The time in seconds, the instance is allowed to run while detached from the ELB. This is to allow the instance time to be drained from incoming TCP connections before terminating it, during a scale down operation.
* `instance_types` - The type of instance determines your instance's CPU capacity, memory and storage (e.g., m1.small, c1.xlarge).
* `ondemand` - (Required) The base instance type.
* `spot` - (Required) One or more instance types.
* `launch_specification` - (Required) Describes the launch specification for an instance.
* `image_id` - (Required) The ID of the AMI used to launch the instance.
* `key_pair` - (Optional) The key name that should be used for the instance.
* `security_group_ids` - (Optional) A list of associated security group IDS.
* `monitoring` - (Optional) Indicates whether monitoring is enabled for the instance.
* `user_data` - (Optional) The user data to provide when launching the instance.
* `iam_instance_profile` - (Optional) The ARN of an IAM instance profile to associate with launched instances.
* `load_balancer_names` - (Optional) Registers each instance with the specified Elastic Load Balancers.
* `tags` - (Optional) A mapping of tags to assign to the resource.
* `elastic_ips` - (Optional) A list of [AWS Elastic IP](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html) allocation IDs to associate to the group instances.
<a id="availability-zone"></a>
## Availability Zone
Each `availability_zone` supports the following:
* `name` - The name of the availability zone.
* `subnet_id` - (Optional) A specific subnet ID within the given availability zone. If not specified, the default subnet will be used.
<a id="scheduled-task"></a>
## Scheduled Tasks
Each `scheduled_task` supports the following:
* `task_type` - (Required) The task type to run. Supported task types are `scale` and `backup_ami`.
* `cron_expression` - (Optional; Required if not using `frequency`) A valid cron expression. The cron is running in UTC time zone and is in [Unix cron format](https://en.wikipedia.org/wiki/Cron).
* `frequency` - (Optional; Required if not using `cron_expression`) The recurrence frequency to run this task. Supported values are `hourly`, `daily` and `weekly`.
* `scale_target_capcity` - (Optional) The desired number of instances the group should have.
* `scale_min_capcity` - (Optional) The minimum number of instances the group should have.
* `scale_max_capcity` - (Optional) The maximum number of instances the group should have.
<a id="scaling-policy"></a>
## Scaling Policies
Each `scaling_*_policy` supports the following:
* `namespace` - (Required) The namespace for the alarm's associated metric.
* `metric_name` - (Required) The name of the metric, with or without spaces.
* `threshold` - (Required) The value against which the specified statistic is compared.
* `policy_name` - (Optional) The name of the policy.
* `statistic` - (Optional) The metric statistics to return. For information about specific statistics go to [Statistics](http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/index.html?CHAP_TerminologyandKeyConcepts.html#Statistic) in the Amazon CloudWatch Developer Guide.
* `unit` - (Optional) The unit for the alarm's associated metric.
* `adjustment` - (Optional) The number of instances to add/remove to/from the target capacity when scale is needed.
* `period` - (Optional) The granularity, in seconds, of the returned datapoints. Period must be at least 60 seconds and must be a multiple of 60.
* `evaluation_periods` - (Optional) The number of periods over which data is compared to the specified threshold.
* `cooldown` - (Optional) The amount of time, in seconds, after a scaling activity completes and before the next scaling activity can start. If this parameter is not specified, the default cooldown period for the group applies.
* `dimensions` - (Optional) A mapping of dimensions describing qualities of the metric.
<a id="network-interface"></a>
## Network Interfaces
Each of the `network_interface` attributes controls a portion of the AWS
Instance's "Elastic Network Interfaces". It's a good idea to familiarize yourself with [AWS's Elastic Network
Interfaces docs](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html)
to understand the implications of using these attributes.
* `network_interface_id` - (Optional) The ID of the network interface.
* `device_index` - (Optional) The index of the device on the instance for the network interface attachment.
* `subnet_id` - (Optional) The ID of the subnet associated with the network string.
* `description` - (Optional) The description of the network interface.
* `private_ip_address` - (Optional) The private IP address of the network interface.
* `security_group_ids` - (Optional) The IDs of the security groups for the network interface.
* `delete_on_termination` - (Optional) If set to true, the interface is deleted when the instance is terminated.
* `secondary_private_ip_address_count` - (Optional) The number of secondary private IP addresses.
* `associate_public_ip_address` - (Optional) Indicates whether to assign a public IP address to an instance you launch in a VPC. The public IP address can only be assigned to a network interface for eth0, and can only be assigned to a new network interface, not an existing one.
<a id="block-devices"></a>
## Block Devices
Each of the `*_block_device` attributes controls a portion of the AWS
Instance's "Block Device Mapping". It's a good idea to familiarize yourself with [AWS's Block Device
Mapping docs](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html)
to understand the implications of using these attributes.
Each `ebs_block_device` supports the following:
* `device_name` - The name of the device to mount.
* `snapshot_id` - (Optional) The Snapshot ID to mount.
* `volume_type` - (Optional) The type of volume. Can be `"standard"`, `"gp2"`, or `"io1"`.
* `volume_size` - (Optional) The size of the volume in gigabytes.
* `iops` - (Optional) The amount of provisioned
[IOPS](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-io-characteristics.html).
This must be set with a `volume_type` of `"io1"`.
* `delete_on_termination` - (Optional) Whether the volume should be destroyed on instance termination.
* `encrypted` - (Optional) Enables [EBS encryption](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html) on the volume.
Modifying any `ebs_block_device` currently requires resource replacement.
Each `ephemeral_block_device` supports the following:
* `device_name` - The name of the block device to mount on the instance.
* `virtual_name` - The [Instance Store Device Name](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html#InstanceStoreDeviceNames)
(e.g. `"ephemeral0"`).
~> **NOTE:** Currently, changes to `*_block_device` configuration of _existing_
resources cannot be automatically detected by Terraform. After making updates
to block device configuration, resource recreation can be manually triggered by
using the [`taint` command](/docs/commands/taint.html).
<a id="third-party-integrations"></a>
## Third-Party Integrations
* `rancher_integration` - (Optional) Describes the [Rancher](http://rancherlabs.com/) integration.
* `master_host` - (Required) The URL of the Rancher Master host.
* `access_key` - (Required) The access key of the Rancher API.
* `secret_key` - (Required) The secret key of the Rancher API.
* `elastic_beanstalk_integration` - (Optional) Describes the [Elastic Beanstalk](https://aws.amazon.com/documentation/elastic-beanstalk/) integration.
* `environment_id` - (Required) The ID of the Elastic Beanstalk environment.
* `nirmata_integration` - (Optional) Describes the [Nirmata](http://nirmata.io/) integration.
* `api_key` - (Required) The API key of the Nirmata API.
## Attributes Reference
The following attributes are exported:
* `id` - The group ID.

View File

@ -383,6 +383,10 @@
<a href="/docs/providers/statuscake/index.html">StatusCake</a>
</li>
<li<%= sidebar_current("docs-providers-spotinst") %>>
<a href="/docs/providers/spotinst/index.html">Spotinst</a>
</li>
<li<%= sidebar_current("docs-providers-template") %>>
<a href="/docs/providers/template/index.html">Template</a>
</li>
@ -536,4 +540,4 @@
<% end %>
<%= yield %>
<% end %>
<% end %>

View File

@ -0,0 +1,26 @@
<% 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-spotinst-index") %>>
<a href="/docs/providers/spotinst/index.html">Spotinst Provider</a>
</li>
<li<%= sidebar_current(/^docs-spotinst-resource/) %>>
<a href="#">AWS Resources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-spotinst-resource-aws_group") %>>
<a href="/docs/providers/spotinst/r/aws_group.html">aws_group</a>
</li>
</ul>
</li>
</ul>
</div>
<% end %>
<%= yield %>
<% end %>