open-nomad/vendor/github.com/joyent/triton-go/compute/instances.go
Seth Hoenig 435c0d9fc8 deps: Switch to Go modules for dependency management
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.
2020-06-02 14:30:36 -05:00

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