435c0d9fc8
This PR switches the Nomad repository from using govendor to Go modules for managing dependencies. Aspects of the Nomad workflow remain pretty much the same. The usual Makefile targets should continue to work as they always did. The API submodule simply defers to the parent Nomad version on the repository, keeping the semantics of API versioning that currently exists.
1179 lines
31 KiB
Go
1179 lines
31 KiB
Go
//
|
|
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
|
//
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
//
|
|
|
|
package compute
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/joyent/triton-go/client"
|
|
"github.com/joyent/triton-go/errors"
|
|
pkgerrors "github.com/pkg/errors"
|
|
)
|
|
|
|
type InstancesClient struct {
|
|
client *client.Client
|
|
}
|
|
|
|
const (
|
|
CNSTagDisable = "triton.cns.disable"
|
|
CNSTagReversePTR = "triton.cns.reverse_ptr"
|
|
CNSTagServices = "triton.cns.services"
|
|
)
|
|
|
|
// InstanceCNS is a container for the CNS-specific attributes. In the API these
|
|
// values are embedded within a Instance's Tags attribute, however they are
|
|
// exposed to the caller as their native types.
|
|
type InstanceCNS struct {
|
|
Disable bool
|
|
ReversePTR string
|
|
Services []string
|
|
}
|
|
|
|
type InstanceVolume struct {
|
|
Name string `json:"name,omitempty"`
|
|
Type string `json:"type,omitempty"`
|
|
Mode string `json:"mode,omitempty"`
|
|
Mountpoint string `json:"mountpoint,omitempty"`
|
|
}
|
|
|
|
type Instance struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Brand string `json:"brand"`
|
|
State string `json:"state"`
|
|
Image string `json:"image"`
|
|
Memory int `json:"memory"`
|
|
Disk int `json:"disk"`
|
|
Metadata map[string]string `json:"metadata"`
|
|
Tags map[string]interface{} `json:"tags"`
|
|
Created time.Time `json:"created"`
|
|
Updated time.Time `json:"updated"`
|
|
Docker bool `json:"docker"`
|
|
IPs []string `json:"ips"`
|
|
Networks []string `json:"networks"`
|
|
PrimaryIP string `json:"primaryIp"`
|
|
FirewallEnabled bool `json:"firewall_enabled"`
|
|
ComputeNode string `json:"compute_node"`
|
|
Package string `json:"package"`
|
|
DomainNames []string `json:"dns_names"`
|
|
DeletionProtection bool `json:"deletion_protection"`
|
|
CNS InstanceCNS
|
|
}
|
|
|
|
// _Instance is a private facade over Instance that handles the necessary API
|
|
// overrides from VMAPI's machine endpoint(s).
|
|
type _Instance struct {
|
|
Instance
|
|
Tags map[string]interface{} `json:"tags"`
|
|
}
|
|
|
|
type NIC struct {
|
|
IP string `json:"ip"`
|
|
MAC string `json:"mac"`
|
|
Primary bool `json:"primary"`
|
|
Netmask string `json:"netmask"`
|
|
Gateway string `json:"gateway"`
|
|
State string `json:"state"`
|
|
Network string `json:"network"`
|
|
}
|
|
|
|
type GetInstanceInput struct {
|
|
ID string
|
|
}
|
|
|
|
func (gmi *GetInstanceInput) Validate() error {
|
|
if gmi.ID == "" {
|
|
return fmt.Errorf("machine ID can not be empty")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *InstancesClient) Count(ctx context.Context, input *ListInstancesInput) (int, error) {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines")
|
|
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodHead,
|
|
Path: fullPath,
|
|
Query: buildQueryFilter(input),
|
|
}
|
|
|
|
response, err := c.client.ExecuteRequestRaw(ctx, reqInputs)
|
|
if err != nil {
|
|
return -1, pkgerrors.Wrap(err, "unable to get machines count")
|
|
}
|
|
|
|
if response == nil {
|
|
return -1, pkgerrors.New("request to get machines count has empty response")
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
var result int
|
|
|
|
if count := response.Header.Get("X-Resource-Count"); count != "" {
|
|
value, err := strconv.Atoi(count)
|
|
if err != nil {
|
|
return -1, pkgerrors.Wrap(err, "unable to decode machines count response")
|
|
}
|
|
result = value
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (c *InstancesClient) Get(ctx context.Context, input *GetInstanceInput) (*Instance, error) {
|
|
if err := input.Validate(); err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to get machine")
|
|
}
|
|
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID)
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodGet,
|
|
Path: fullPath,
|
|
}
|
|
response, err := c.client.ExecuteRequestRaw(ctx, reqInputs)
|
|
if response == nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to get machine")
|
|
}
|
|
if response.Body != nil {
|
|
defer response.Body.Close()
|
|
}
|
|
if response.StatusCode == http.StatusNotFound || response.StatusCode == http.StatusGone {
|
|
return nil, &errors.APIError{
|
|
StatusCode: response.StatusCode,
|
|
Code: "ResourceNotFound",
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to get machine")
|
|
}
|
|
|
|
var result *_Instance
|
|
decoder := json.NewDecoder(response.Body)
|
|
if err = decoder.Decode(&result); err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to decode get machine response")
|
|
}
|
|
|
|
native, err := result.toNative()
|
|
if err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to decode get machine response")
|
|
}
|
|
|
|
return native, nil
|
|
}
|
|
|
|
type ListInstancesInput struct {
|
|
Brand string
|
|
Alias string
|
|
Name string
|
|
Image string
|
|
State string
|
|
Memory uint16
|
|
Limit uint16
|
|
Offset uint16
|
|
Tags map[string]interface{} // query by arbitrary tags prefixed with "tag."
|
|
Tombstone bool
|
|
Docker bool
|
|
Credentials bool
|
|
}
|
|
|
|
func buildQueryFilter(input *ListInstancesInput) *url.Values {
|
|
query := &url.Values{}
|
|
if input.Brand != "" {
|
|
query.Set("brand", input.Brand)
|
|
}
|
|
if input.Name != "" {
|
|
query.Set("name", input.Name)
|
|
}
|
|
if input.Image != "" {
|
|
query.Set("image", input.Image)
|
|
}
|
|
if input.State != "" {
|
|
query.Set("state", input.State)
|
|
}
|
|
if input.Memory >= 1 {
|
|
query.Set("memory", fmt.Sprintf("%d", input.Memory))
|
|
}
|
|
if input.Limit >= 1 && input.Limit <= 1000 {
|
|
query.Set("limit", fmt.Sprintf("%d", input.Limit))
|
|
}
|
|
if input.Offset >= 0 {
|
|
query.Set("offset", fmt.Sprintf("%d", input.Offset))
|
|
}
|
|
if input.Tombstone {
|
|
query.Set("tombstone", "true")
|
|
}
|
|
if input.Docker {
|
|
query.Set("docker", "true")
|
|
}
|
|
if input.Credentials {
|
|
query.Set("credentials", "true")
|
|
}
|
|
if input.Tags != nil {
|
|
for k, v := range input.Tags {
|
|
query.Set(fmt.Sprintf("tag.%s", k), v.(string))
|
|
}
|
|
}
|
|
|
|
return query
|
|
}
|
|
|
|
func (c *InstancesClient) List(ctx context.Context, input *ListInstancesInput) ([]*Instance, error) {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines")
|
|
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodGet,
|
|
Path: fullPath,
|
|
Query: buildQueryFilter(input),
|
|
}
|
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to list machines")
|
|
}
|
|
|
|
var results []*_Instance
|
|
decoder := json.NewDecoder(respReader)
|
|
if err = decoder.Decode(&results); err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to decode list machines response")
|
|
}
|
|
|
|
machines := make([]*Instance, 0, len(results))
|
|
for _, machineAPI := range results {
|
|
native, err := machineAPI.toNative()
|
|
if err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to decode list machines response")
|
|
}
|
|
machines = append(machines, native)
|
|
}
|
|
|
|
return machines, nil
|
|
}
|
|
|
|
type CreateInstanceInput struct {
|
|
Name string
|
|
NamePrefix string
|
|
Package string
|
|
Image string
|
|
Networks []string
|
|
Affinity []string
|
|
LocalityStrict bool
|
|
LocalityNear []string
|
|
LocalityFar []string
|
|
Metadata map[string]string
|
|
Tags map[string]string //
|
|
FirewallEnabled bool //
|
|
CNS InstanceCNS
|
|
Volumes []InstanceVolume
|
|
}
|
|
|
|
func buildInstanceName(namePrefix string) string {
|
|
h := sha1.New()
|
|
io.WriteString(h, namePrefix+time.Now().UTC().String())
|
|
return fmt.Sprintf("%s%s", namePrefix, hex.EncodeToString(h.Sum(nil))[:8])
|
|
}
|
|
|
|
func (input *CreateInstanceInput) toAPI() (map[string]interface{}, error) {
|
|
const numExtraParams = 8
|
|
result := make(map[string]interface{}, numExtraParams+len(input.Metadata)+len(input.Tags))
|
|
|
|
result["firewall_enabled"] = input.FirewallEnabled
|
|
|
|
if input.Name != "" {
|
|
result["name"] = input.Name
|
|
} else if input.NamePrefix != "" {
|
|
result["name"] = buildInstanceName(input.NamePrefix)
|
|
}
|
|
|
|
if input.Package != "" {
|
|
result["package"] = input.Package
|
|
}
|
|
|
|
if input.Image != "" {
|
|
result["image"] = input.Image
|
|
}
|
|
|
|
if len(input.Networks) > 0 {
|
|
result["networks"] = input.Networks
|
|
}
|
|
|
|
if len(input.Volumes) > 0 {
|
|
result["volumes"] = input.Volumes
|
|
}
|
|
|
|
// validate that affinity and locality are not included together
|
|
hasAffinity := len(input.Affinity) > 0
|
|
hasLocality := len(input.LocalityNear) > 0 || len(input.LocalityFar) > 0
|
|
if hasAffinity && hasLocality {
|
|
return nil, fmt.Errorf("Cannot include both Affinity and Locality")
|
|
}
|
|
|
|
// affinity takes precedence over locality regardless
|
|
if len(input.Affinity) > 0 {
|
|
result["affinity"] = input.Affinity
|
|
} else {
|
|
locality := struct {
|
|
Strict bool `json:"strict"`
|
|
Near []string `json:"near,omitempty"`
|
|
Far []string `json:"far,omitempty"`
|
|
}{
|
|
Strict: input.LocalityStrict,
|
|
Near: input.LocalityNear,
|
|
Far: input.LocalityFar,
|
|
}
|
|
result["locality"] = locality
|
|
}
|
|
|
|
for key, value := range input.Tags {
|
|
result[fmt.Sprintf("tag.%s", key)] = value
|
|
}
|
|
|
|
// NOTE(justinwr): CNSTagServices needs to be a tag if available. No other
|
|
// CNS tags will be handled at this time.
|
|
input.CNS.toTags(result)
|
|
if val, found := result[CNSTagServices]; found {
|
|
result["tag."+CNSTagServices] = val
|
|
delete(result, CNSTagServices)
|
|
}
|
|
|
|
for key, value := range input.Metadata {
|
|
result[fmt.Sprintf("metadata.%s", key)] = value
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (c *InstancesClient) Create(ctx context.Context, input *CreateInstanceInput) (*Instance, error) {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines")
|
|
body, err := input.toAPI()
|
|
if err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to prepare for machine creation")
|
|
}
|
|
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodPost,
|
|
Path: fullPath,
|
|
Body: body,
|
|
}
|
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to create machine")
|
|
}
|
|
|
|
var result *Instance
|
|
decoder := json.NewDecoder(respReader)
|
|
if err = decoder.Decode(&result); err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to decode create machine response")
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
type DeleteInstanceInput struct {
|
|
ID string
|
|
}
|
|
|
|
func (c *InstancesClient) Delete(ctx context.Context, input *DeleteInstanceInput) error {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID)
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodDelete,
|
|
Path: fullPath,
|
|
}
|
|
response, err := c.client.ExecuteRequestRaw(ctx, reqInputs)
|
|
if response == nil {
|
|
return pkgerrors.Wrap(err, "unable to delete machine")
|
|
}
|
|
if response.Body != nil {
|
|
defer response.Body.Close()
|
|
}
|
|
if response.StatusCode == http.StatusNotFound || response.StatusCode == http.StatusGone {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to decode delete machine response")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type DeleteTagsInput struct {
|
|
ID string
|
|
}
|
|
|
|
func (c *InstancesClient) DeleteTags(ctx context.Context, input *DeleteTagsInput) error {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID, "tags")
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodDelete,
|
|
Path: fullPath,
|
|
}
|
|
response, err := c.client.ExecuteRequestRaw(ctx, reqInputs)
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to delete tags from machine")
|
|
}
|
|
if response == nil {
|
|
return fmt.Errorf("DeleteTags request has empty response")
|
|
}
|
|
if response.Body != nil {
|
|
defer response.Body.Close()
|
|
}
|
|
if response.StatusCode == http.StatusNotFound {
|
|
return nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type DeleteTagInput struct {
|
|
ID string
|
|
Key string
|
|
}
|
|
|
|
func (c *InstancesClient) DeleteTag(ctx context.Context, input *DeleteTagInput) error {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID, "tags", input.Key)
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodDelete,
|
|
Path: fullPath,
|
|
}
|
|
response, err := c.client.ExecuteRequestRaw(ctx, reqInputs)
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to delete tag from machine")
|
|
}
|
|
if response == nil {
|
|
return fmt.Errorf("DeleteTag request has empty response")
|
|
}
|
|
if response.Body != nil {
|
|
defer response.Body.Close()
|
|
}
|
|
if response.StatusCode == http.StatusNotFound {
|
|
return nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type RenameInstanceInput struct {
|
|
ID string
|
|
Name string
|
|
}
|
|
|
|
func (c *InstancesClient) Rename(ctx context.Context, input *RenameInstanceInput) error {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID)
|
|
|
|
params := &url.Values{}
|
|
params.Set("action", "rename")
|
|
params.Set("name", input.Name)
|
|
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodPost,
|
|
Path: fullPath,
|
|
Query: params,
|
|
}
|
|
respReader, err := c.client.ExecuteRequestURIParams(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to rename machine")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type ReplaceTagsInput struct {
|
|
ID string
|
|
Tags map[string]string
|
|
CNS InstanceCNS
|
|
}
|
|
|
|
// toAPI is used to join Tags and CNS tags into the same JSON object before
|
|
// sending an API request to the API gateway.
|
|
func (input ReplaceTagsInput) toAPI() map[string]interface{} {
|
|
result := map[string]interface{}{}
|
|
for key, value := range input.Tags {
|
|
result[key] = value
|
|
}
|
|
input.CNS.toTags(result)
|
|
return result
|
|
}
|
|
|
|
func (c *InstancesClient) ReplaceTags(ctx context.Context, input *ReplaceTagsInput) error {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID, "tags")
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodPut,
|
|
Path: fullPath,
|
|
Body: input.toAPI(),
|
|
}
|
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to replace machine tags")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type AddTagsInput struct {
|
|
ID string
|
|
Tags map[string]string
|
|
}
|
|
|
|
func (c *InstancesClient) AddTags(ctx context.Context, input *AddTagsInput) error {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID, "tags")
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodPost,
|
|
Path: fullPath,
|
|
Body: input.Tags,
|
|
}
|
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to add tags to machine")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type GetTagInput struct {
|
|
ID string
|
|
Key string
|
|
}
|
|
|
|
func (c *InstancesClient) GetTag(ctx context.Context, input *GetTagInput) (string, error) {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID, "tags", input.Key)
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodGet,
|
|
Path: fullPath,
|
|
}
|
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
|
if err != nil {
|
|
return "", pkgerrors.Wrap(err, "unable to get tag")
|
|
}
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
|
|
var result string
|
|
decoder := json.NewDecoder(respReader)
|
|
if err = decoder.Decode(&result); err != nil {
|
|
return "", pkgerrors.Wrap(err, "unable to decode get tag response")
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
type ListTagsInput struct {
|
|
ID string
|
|
}
|
|
|
|
func (c *InstancesClient) ListTags(ctx context.Context, input *ListTagsInput) (map[string]interface{}, error) {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID, "tags")
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodGet,
|
|
Path: fullPath,
|
|
}
|
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to list machine tags")
|
|
}
|
|
|
|
var result map[string]interface{}
|
|
decoder := json.NewDecoder(respReader)
|
|
if err = decoder.Decode(&result); err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable decode list machine tags response")
|
|
}
|
|
|
|
_, tags := TagsExtractMeta(result)
|
|
return tags, nil
|
|
}
|
|
|
|
type GetMetadataInput struct {
|
|
ID string
|
|
Key string
|
|
}
|
|
|
|
// GetMetadata returns a single metadata entry associated with an instance.
|
|
func (c *InstancesClient) GetMetadata(ctx context.Context, input *GetMetadataInput) (string, error) {
|
|
if input.Key == "" {
|
|
return "", fmt.Errorf("Missing metadata Key from input: %s", input.Key)
|
|
}
|
|
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID, "metadata", input.Key)
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodGet,
|
|
Path: fullPath,
|
|
}
|
|
response, err := c.client.ExecuteRequestRaw(ctx, reqInputs)
|
|
if err != nil {
|
|
return "", pkgerrors.Wrap(err, "unable to get machine metadata")
|
|
}
|
|
if response != nil {
|
|
defer response.Body.Close()
|
|
}
|
|
if response.StatusCode == http.StatusNotFound || response.StatusCode == http.StatusGone {
|
|
return "", &errors.APIError{
|
|
StatusCode: response.StatusCode,
|
|
Code: "ResourceNotFound",
|
|
}
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
if err != nil {
|
|
return "", pkgerrors.Wrap(err, "unable to decode get machine metadata response")
|
|
}
|
|
|
|
return fmt.Sprintf("%s", body), nil
|
|
}
|
|
|
|
type ListMetadataInput struct {
|
|
ID string
|
|
Credentials bool
|
|
}
|
|
|
|
func (c *InstancesClient) ListMetadata(ctx context.Context, input *ListMetadataInput) (map[string]string, error) {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID, "metadata")
|
|
|
|
query := &url.Values{}
|
|
if input.Credentials {
|
|
query.Set("credentials", "true")
|
|
}
|
|
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodGet,
|
|
Path: fullPath,
|
|
Query: query,
|
|
}
|
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to list machine metadata")
|
|
}
|
|
|
|
var result map[string]string
|
|
decoder := json.NewDecoder(respReader)
|
|
if err = decoder.Decode(&result); err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to decode list machine metadata response")
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
type UpdateMetadataInput struct {
|
|
ID string
|
|
Metadata map[string]string
|
|
}
|
|
|
|
func (c *InstancesClient) UpdateMetadata(ctx context.Context, input *UpdateMetadataInput) (map[string]string, error) {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID, "metadata")
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodPost,
|
|
Path: fullPath,
|
|
Body: input.Metadata,
|
|
}
|
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to update machine metadata")
|
|
}
|
|
|
|
var result map[string]string
|
|
decoder := json.NewDecoder(respReader)
|
|
if err = decoder.Decode(&result); err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to decode update machine metadata response")
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
type DeleteMetadataInput struct {
|
|
ID string
|
|
Key string
|
|
}
|
|
|
|
// DeleteMetadata deletes a single metadata key from an instance
|
|
func (c *InstancesClient) DeleteMetadata(ctx context.Context, input *DeleteMetadataInput) error {
|
|
if input.Key == "" {
|
|
return fmt.Errorf("Missing metadata Key from input: %s", input.Key)
|
|
}
|
|
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID, "metadata", input.Key)
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodDelete,
|
|
Path: fullPath,
|
|
}
|
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to delete machine metadata")
|
|
}
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type DeleteAllMetadataInput struct {
|
|
ID string
|
|
}
|
|
|
|
// DeleteAllMetadata deletes all metadata keys from this instance
|
|
func (c *InstancesClient) DeleteAllMetadata(ctx context.Context, input *DeleteAllMetadataInput) error {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID, "metadata")
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodDelete,
|
|
Path: fullPath,
|
|
}
|
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to delete all machine metadata")
|
|
}
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type ResizeInstanceInput struct {
|
|
ID string
|
|
Package string
|
|
}
|
|
|
|
func (c *InstancesClient) Resize(ctx context.Context, input *ResizeInstanceInput) error {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID)
|
|
|
|
params := &url.Values{}
|
|
params.Set("action", "resize")
|
|
params.Set("package", input.Package)
|
|
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodPost,
|
|
Path: fullPath,
|
|
Query: params,
|
|
}
|
|
respReader, err := c.client.ExecuteRequestURIParams(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to resize machine")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type EnableFirewallInput struct {
|
|
ID string
|
|
}
|
|
|
|
func (c *InstancesClient) EnableFirewall(ctx context.Context, input *EnableFirewallInput) error {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID)
|
|
|
|
params := &url.Values{}
|
|
params.Set("action", "enable_firewall")
|
|
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodPost,
|
|
Path: fullPath,
|
|
Query: params,
|
|
}
|
|
respReader, err := c.client.ExecuteRequestURIParams(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to enable machine firewall")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type DisableFirewallInput struct {
|
|
ID string
|
|
}
|
|
|
|
func (c *InstancesClient) DisableFirewall(ctx context.Context, input *DisableFirewallInput) error {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.ID)
|
|
|
|
params := &url.Values{}
|
|
params.Set("action", "disable_firewall")
|
|
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodPost,
|
|
Path: fullPath,
|
|
Query: params,
|
|
}
|
|
respReader, err := c.client.ExecuteRequestURIParams(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to disable machine firewall")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type ListNICsInput struct {
|
|
InstanceID string
|
|
}
|
|
|
|
func (c *InstancesClient) ListNICs(ctx context.Context, input *ListNICsInput) ([]*NIC, error) {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.InstanceID, "nics")
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodGet,
|
|
Path: fullPath,
|
|
}
|
|
respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to list machine NICs")
|
|
}
|
|
|
|
var result []*NIC
|
|
decoder := json.NewDecoder(respReader)
|
|
if err = decoder.Decode(&result); err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to decode list machine NICs response")
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
type GetNICInput struct {
|
|
InstanceID string
|
|
MAC string
|
|
}
|
|
|
|
func (c *InstancesClient) GetNIC(ctx context.Context, input *GetNICInput) (*NIC, error) {
|
|
mac := strings.Replace(input.MAC, ":", "", -1)
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.InstanceID, "nics", mac)
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodGet,
|
|
Path: fullPath,
|
|
}
|
|
response, err := c.client.ExecuteRequestRaw(ctx, reqInputs)
|
|
if err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to get machine NIC")
|
|
}
|
|
if response != nil {
|
|
defer response.Body.Close()
|
|
}
|
|
switch response.StatusCode {
|
|
case http.StatusNotFound:
|
|
return nil, &errors.APIError{
|
|
StatusCode: response.StatusCode,
|
|
Code: "ResourceNotFound",
|
|
}
|
|
}
|
|
|
|
var result *NIC
|
|
decoder := json.NewDecoder(response.Body)
|
|
if err = decoder.Decode(&result); err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to decode get machine NIC response")
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
type AddNICInput struct {
|
|
InstanceID string `json:"-"`
|
|
Network string `json:"network"`
|
|
}
|
|
|
|
// AddNIC asynchronously adds a NIC to a given instance. If a NIC for a given
|
|
// network already exists, a ResourceFound error will be returned. The status
|
|
// of the addition of a NIC can be polled by calling GetNIC()'s and testing NIC
|
|
// until its state is set to "running". Only one NIC per network may exist.
|
|
// Warning: this operation causes the instance to restart.
|
|
func (c *InstancesClient) AddNIC(ctx context.Context, input *AddNICInput) (*NIC, error) {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.InstanceID, "nics")
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodPost,
|
|
Path: fullPath,
|
|
Body: input,
|
|
}
|
|
response, err := c.client.ExecuteRequestRaw(ctx, reqInputs)
|
|
if err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to add NIC to machine")
|
|
}
|
|
if response != nil {
|
|
defer response.Body.Close()
|
|
}
|
|
switch response.StatusCode {
|
|
case http.StatusFound:
|
|
return nil, &errors.APIError{
|
|
StatusCode: response.StatusCode,
|
|
Code: "ResourceFound",
|
|
Message: response.Header.Get("Location"),
|
|
}
|
|
}
|
|
|
|
var result *NIC
|
|
decoder := json.NewDecoder(response.Body)
|
|
if err = decoder.Decode(&result); err != nil {
|
|
return nil, pkgerrors.Wrap(err, "unable to decode add NIC to machine response")
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
type RemoveNICInput struct {
|
|
InstanceID string
|
|
MAC string
|
|
}
|
|
|
|
// RemoveNIC removes a given NIC from a machine asynchronously. The status of
|
|
// the removal can be polled via GetNIC(). When GetNIC() returns a 404, the NIC
|
|
// has been removed from the instance. Warning: this operation causes the
|
|
// machine to restart.
|
|
func (c *InstancesClient) RemoveNIC(ctx context.Context, input *RemoveNICInput) error {
|
|
mac := strings.Replace(input.MAC, ":", "", -1)
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.InstanceID, "nics", mac)
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodDelete,
|
|
Path: fullPath,
|
|
}
|
|
response, err := c.client.ExecuteRequestRaw(ctx, reqInputs)
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to remove NIC from machine")
|
|
}
|
|
if response == nil {
|
|
return pkgerrors.Wrap(err, "unable to remove NIC from machine")
|
|
}
|
|
if response.Body != nil {
|
|
defer response.Body.Close()
|
|
}
|
|
switch response.StatusCode {
|
|
case http.StatusNotFound:
|
|
return &errors.APIError{
|
|
StatusCode: response.StatusCode,
|
|
Code: "ResourceNotFound",
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type StopInstanceInput struct {
|
|
InstanceID string
|
|
}
|
|
|
|
func (c *InstancesClient) Stop(ctx context.Context, input *StopInstanceInput) error {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.InstanceID)
|
|
|
|
params := &url.Values{}
|
|
params.Set("action", "stop")
|
|
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodPost,
|
|
Path: fullPath,
|
|
Query: params,
|
|
}
|
|
respReader, err := c.client.ExecuteRequestURIParams(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to stop machine")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type StartInstanceInput struct {
|
|
InstanceID string
|
|
}
|
|
|
|
func (c *InstancesClient) Start(ctx context.Context, input *StartInstanceInput) error {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.InstanceID)
|
|
|
|
params := &url.Values{}
|
|
params.Set("action", "start")
|
|
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodPost,
|
|
Path: fullPath,
|
|
Query: params,
|
|
}
|
|
respReader, err := c.client.ExecuteRequestURIParams(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to start machine")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type RebootInstanceInput struct {
|
|
InstanceID string
|
|
}
|
|
|
|
func (c *InstancesClient) Reboot(ctx context.Context, input *RebootInstanceInput) error {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.InstanceID)
|
|
|
|
params := &url.Values{}
|
|
params.Set("action", "reboot")
|
|
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodPost,
|
|
Path: fullPath,
|
|
Query: params,
|
|
}
|
|
respReader, err := c.client.ExecuteRequestURIParams(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to reboot machine")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type EnableDeletionProtectionInput struct {
|
|
InstanceID string
|
|
}
|
|
|
|
func (c *InstancesClient) EnableDeletionProtection(ctx context.Context, input *EnableDeletionProtectionInput) error {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.InstanceID)
|
|
|
|
params := &url.Values{}
|
|
params.Set("action", "enable_deletion_protection")
|
|
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodPost,
|
|
Path: fullPath,
|
|
Query: params,
|
|
}
|
|
respReader, err := c.client.ExecuteRequestURIParams(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to enable deletion protection")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type DisableDeletionProtectionInput struct {
|
|
InstanceID string
|
|
}
|
|
|
|
func (c *InstancesClient) DisableDeletionProtection(ctx context.Context, input *DisableDeletionProtectionInput) error {
|
|
fullPath := path.Join("/", c.client.AccountName, "machines", input.InstanceID)
|
|
|
|
params := &url.Values{}
|
|
params.Set("action", "disable_deletion_protection")
|
|
|
|
reqInputs := client.RequestInput{
|
|
Method: http.MethodPost,
|
|
Path: fullPath,
|
|
Query: params,
|
|
}
|
|
respReader, err := c.client.ExecuteRequestURIParams(ctx, reqInputs)
|
|
if respReader != nil {
|
|
defer respReader.Close()
|
|
}
|
|
if err != nil {
|
|
return pkgerrors.Wrap(err, "unable to disable deletion protection")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var reservedInstanceCNSTags = map[string]struct{}{
|
|
CNSTagDisable: {},
|
|
CNSTagReversePTR: {},
|
|
CNSTagServices: {},
|
|
}
|
|
|
|
// TagsExtractMeta extracts all of the misc parameters from Tags and returns a
|
|
// clean CNS and Tags struct.
|
|
func TagsExtractMeta(tags map[string]interface{}) (InstanceCNS, map[string]interface{}) {
|
|
nativeCNS := InstanceCNS{}
|
|
nativeTags := make(map[string]interface{}, len(tags))
|
|
for k, raw := range tags {
|
|
if _, found := reservedInstanceCNSTags[k]; found {
|
|
switch k {
|
|
case CNSTagDisable:
|
|
b := raw.(bool)
|
|
nativeCNS.Disable = b
|
|
case CNSTagReversePTR:
|
|
s := raw.(string)
|
|
nativeCNS.ReversePTR = s
|
|
case CNSTagServices:
|
|
nativeCNS.Services = strings.Split(raw.(string), ",")
|
|
default:
|
|
// TODO(seanc@): should assert, logic fail
|
|
}
|
|
} else {
|
|
nativeTags[k] = raw
|
|
}
|
|
}
|
|
|
|
return nativeCNS, nativeTags
|
|
}
|
|
|
|
// toNative() exports a given _Instance (API representation) to its native object
|
|
// format.
|
|
func (api *_Instance) toNative() (*Instance, error) {
|
|
m := Instance(api.Instance)
|
|
m.CNS, m.Tags = TagsExtractMeta(api.Tags)
|
|
return &m, nil
|
|
}
|
|
|
|
// toTags() injects its state information into a Tags map suitable for use to
|
|
// submit an API call to the vmapi machine endpoint
|
|
func (cns *InstanceCNS) toTags(m map[string]interface{}) {
|
|
if cns.Disable {
|
|
// NOTE(justinwr): The JSON encoder and API require the CNSTagDisable
|
|
// attribute to be an actual boolean, not a bool string.
|
|
m[CNSTagDisable] = cns.Disable
|
|
}
|
|
if cns.ReversePTR != "" {
|
|
m[CNSTagReversePTR] = cns.ReversePTR
|
|
}
|
|
if len(cns.Services) > 0 {
|
|
m[CNSTagServices] = strings.Join(cns.Services, ",")
|
|
}
|
|
}
|