diff --git a/builder/hcloud/artifact.go b/builder/hcloud/artifact.go new file mode 100644 index 000000000..52f2593f4 --- /dev/null +++ b/builder/hcloud/artifact.go @@ -0,0 +1,47 @@ +package hcloud + +import ( + "context" + "fmt" + "log" + "strconv" + + "github.com/hetznercloud/hcloud-go/hcloud" +) + +type Artifact struct { + // The name of the snapshot + snapshotName string + + // The ID of the image + snapshotId int + + // The hcloudClient for making API calls + hcloudClient *hcloud.Client +} + +func (*Artifact) BuilderId() string { + return BuilderId +} + +func (*Artifact) Files() []string { + return nil +} + +func (a *Artifact) Id() string { + return strconv.Itoa(a.snapshotId) +} + +func (a *Artifact) String() string { + return fmt.Sprintf("A snapshot was created: '%v' (ID: %v)", a.snapshotName, a.snapshotId) +} + +func (a *Artifact) State(name string) interface{} { + return nil +} + +func (a *Artifact) Destroy() error { + log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName) + _, err := a.hcloudClient.Image.Delete(context.TODO(), &hcloud.Image{ID: a.snapshotId}) + return err +} diff --git a/builder/hcloud/artifact_test.go b/builder/hcloud/artifact_test.go new file mode 100644 index 000000000..f6e00e436 --- /dev/null +++ b/builder/hcloud/artifact_test.go @@ -0,0 +1,29 @@ +package hcloud + +import ( + "testing" + + "github.com/hashicorp/packer/packer" +) + +func TestArtifact_Impl(t *testing.T) { + var _ packer.Artifact = (*Artifact)(nil) +} + +func TestArtifactId(t *testing.T) { + a := &Artifact{"packer-foobar", 42, nil} + expected := "42" + + if a.Id() != expected { + t.Fatalf("artifact ID should match: %v", expected) + } +} + +func TestArtifactString(t *testing.T) { + a := &Artifact{"packer-foobar", 42, nil} + expected := "A snapshot was created: 'packer-foobar' (ID: 42)" + + if a.String() != expected { + t.Fatalf("artifact string should match: %v", expected) + } +} diff --git a/builder/hcloud/builder.go b/builder/hcloud/builder.go new file mode 100644 index 000000000..af5812b63 --- /dev/null +++ b/builder/hcloud/builder.go @@ -0,0 +1,94 @@ +package hcloud + +import ( + "fmt" + "log" + + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/helper/communicator" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hetznercloud/hcloud-go/hcloud" +) + +// The unique id for the builder +const BuilderId = "hcloud.builder" + +type Builder struct { + config Config + runner multistep.Runner + hcloudClient *hcloud.Client +} + +var pluginVersion = "1.0.0" + +func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { + config, warnings, errs := NewConfig(raws...) + if errs != nil { + return warnings, errs + } + b.config = *config + return nil, nil +} + +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + opts := []hcloud.ClientOption{ + hcloud.WithToken(b.config.HCloudToken), + hcloud.WithEndpoint(b.config.Endpoint), + hcloud.WithPollInterval(b.config.PollInterval), + hcloud.WithApplication("hcloud-packer", pluginVersion), + } + b.hcloudClient = hcloud.NewClient(opts...) + // Set up the state + state := new(multistep.BasicStateBag) + state.Put("config", &b.config) + state.Put("hcloudClient", b.hcloudClient) + state.Put("hook", hook) + state.Put("ui", ui) + + // Build the steps + steps := []multistep.Step{ + &stepCreateSSHKey{ + Debug: b.config.PackerDebug, + DebugKeyPath: fmt.Sprintf("ssh_key_%s.pem", b.config.PackerBuildName), + }, + &stepCreateServer{}, + &communicator.StepConnect{ + Config: &b.config.Comm, + Host: getServerIP, + SSHConfig: b.config.Comm.SSHConfigFunc(), + }, + &common.StepProvision{}, + &common.StepCleanupTempKeys{ + Comm: &b.config.Comm, + }, + &stepShutdownServer{}, + &stepCreateSnapshot{}, + } + // Run the steps + b.runner = common.NewRunner(steps, b.config.PackerConfig, ui) + b.runner.Run(state) + // If there was an error, return that + if rawErr, ok := state.GetOk("error"); ok { + return nil, rawErr.(error) + } + + if _, ok := state.GetOk("snapshot_name"); !ok { + return nil, nil + } + + artifact := &Artifact{ + snapshotName: state.Get("snapshot_name").(string), + snapshotId: state.Get("snapshot_id").(int), + hcloudClient: b.hcloudClient, + } + + return artifact, nil +} + +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } +} diff --git a/builder/hcloud/builder_acc_test.go b/builder/hcloud/builder_acc_test.go new file mode 100644 index 000000000..86b0afd21 --- /dev/null +++ b/builder/hcloud/builder_acc_test.go @@ -0,0 +1,35 @@ +package hcloud + +import ( + "os" + "testing" + + builderT "github.com/hashicorp/packer/helper/builder/testing" +) + +func TestBuilderAcc_basic(t *testing.T) { + builderT.Test(t, builderT.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Builder: &Builder{}, + Template: testBuilderAccBasic, + }) +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("HCLOUD_TOKEN"); v == "" { + t.Fatal("HCLOUD_TOKEN must be set for acceptance tests") + } +} + +const testBuilderAccBasic = ` +{ + "builders": [{ + "type": "test", + "location": "nbg1", + "server_type": "cx11", + "image": "ubuntu-18.04", + "user_data": "", + "user_data_file": "" + }] +} +` diff --git a/builder/hcloud/config.go b/builder/hcloud/config.go new file mode 100644 index 000000000..ef34806c4 --- /dev/null +++ b/builder/hcloud/config.go @@ -0,0 +1,132 @@ +package hcloud + +import ( + "errors" + "fmt" + "os" + "time" + + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/common/uuid" + "github.com/hashicorp/packer/helper/communicator" + "github.com/hashicorp/packer/helper/config" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/mitchellh/mapstructure" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + Comm communicator.Config `mapstructure:",squash"` + + HCloudToken string `mapstructure:"token"` + Endpoint string `mapstructure:"endpoint"` + PollInterval time.Duration `mapstructure:"poll_interval"` + + ServerName string `mapstructure:"server_name"` + Location string `mapstructure:"location"` + ServerType string `mapstructure:"server_type"` + Image string `mapstructure:"image"` + + SnapshotName string `mapstructure:"snapshot_name"` + UserData string `mapstructure:"user_data"` + UserDataFile string `mapstructure:"user_data_file"` + + ctx interpolate.Context +} + +func NewConfig(raws ...interface{}) (*Config, []string, error) { + c := new(Config) + + var md mapstructure.Metadata + err := config.Decode(c, &config.DecodeOpts{ + Metadata: &md, + Interpolate: true, + InterpolateContext: &c.ctx, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{ + "run_command", + }, + }, + }, raws...) + if err != nil { + return nil, nil, err + } + + // Defaults + if c.HCloudToken == "" { + c.HCloudToken = os.Getenv("HCLOUD_TOKEN") + } + if c.Endpoint == "" { + if os.Getenv("HCLOUD_ENDPOINT") != "" { + c.Endpoint = os.Getenv("HCLOUD_ENDPOINT") + } else { + c.Endpoint = hcloud.Endpoint + } + } + if c.PollInterval == 0 { + c.PollInterval = 500 * time.Millisecond + } + + if c.SnapshotName == "" { + def, err := interpolate.Render("packer-{{timestamp}}", nil) + if err != nil { + panic(err) + } + // Default to packer-{{ unix timestamp (utc) }} + c.SnapshotName = def + } + + if c.ServerName == "" { + // Default to packer-[time-ordered-uuid] + c.ServerName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) + } + + var errs *packer.MultiError + if es := c.Comm.Prepare(&c.ctx); len(es) > 0 { + errs = packer.MultiErrorAppend(errs, es...) + } + if c.HCloudToken == "" { + // Required configurations that will display errors if not set + errs = packer.MultiErrorAppend( + errs, errors.New("token for auth must be specified")) + } + + if c.Location == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("location is required")) + } + + if c.ServerType == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("server type is required")) + } + + if c.Image == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("image is required")) + } + + if c.UserData != "" && c.UserDataFile != "" { + errs = packer.MultiErrorAppend( + errs, errors.New("only one of user_data or user_data_file can be specified")) + } else if c.UserDataFile != "" { + if _, err := os.Stat(c.UserDataFile); err != nil { + errs = packer.MultiErrorAppend( + errs, errors.New(fmt.Sprintf("user_data_file not found: %s", c.UserDataFile))) + } + } + + if errs != nil && len(errs.Errors) > 0 { + return nil, nil, errs + } + + packer.LogSecretFilter.Set(c.HCloudToken) + return c, nil, nil +} + +func getServerIP(state multistep.StateBag) (string, error) { + return state.Get("server_ip").(string), nil +} diff --git a/builder/hcloud/step_create_server.go b/builder/hcloud/step_create_server.go new file mode 100644 index 000000000..c705c7fad --- /dev/null +++ b/builder/hcloud/step_create_server.go @@ -0,0 +1,92 @@ +package hcloud + +import ( + "context" + "fmt" + + "io/ioutil" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hetznercloud/hcloud-go/hcloud" +) + +type stepCreateServer struct { + serverId int +} + +func (s *stepCreateServer) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + client := state.Get("hcloudClient").(*hcloud.Client) + ui := state.Get("ui").(packer.Ui) + c := state.Get("config").(*Config) + sshKeyId := state.Get("ssh_key_id").(int) + + // Create the server based on configuration + ui.Say("Creating server...") + + userData := c.UserData + if c.UserDataFile != "" { + contents, err := ioutil.ReadFile(c.UserDataFile) + if err != nil { + state.Put("error", fmt.Errorf("Problem reading user data file: %s", err)) + return multistep.ActionHalt + } + + userData = string(contents) + } + + serverCreateResult, _, err := client.Server.Create(context.TODO(), hcloud.ServerCreateOpts{ + Name: c.ServerName, + ServerType: &hcloud.ServerType{Name: c.ServerType}, + Image: &hcloud.Image{Name: c.Image}, + SSHKeys: []*hcloud.SSHKey{{ID: sshKeyId}}, + Location: &hcloud.Location{Name: c.Location}, + UserData: userData, + }) + if err != nil { + err := fmt.Errorf("Error creating server: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + state.Put("server_ip", serverCreateResult.Server.PublicNet.IPv4.IP.String()) + // We use this in cleanup + s.serverId = serverCreateResult.Server.ID + + // Store the server id for later + state.Put("server_id", serverCreateResult.Server.ID) + + _, errCh := client.Action.WatchProgress(context.TODO(), serverCreateResult.Action) + for { + select { + case err1 := <-errCh: + if err1 == nil { + return multistep.ActionContinue + } else { + err := fmt.Errorf("Error creating server: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + } + } +} + +func (s *stepCreateServer) Cleanup(state multistep.StateBag) { + // If the serverID isn't there, we probably never created it + if s.serverId == 0 { + return + } + + client := state.Get("hcloudClient").(*hcloud.Client) + ui := state.Get("ui").(packer.Ui) + + // Destroy the server we just created + ui.Say("Destroying server...") + _, err := client.Server.Delete(context.TODO(), &hcloud.Server{ID: s.serverId}) + if err != nil { + ui.Error(fmt.Sprintf( + "Error destroying server. Please destroy it manually: %s", err)) + } +} diff --git a/builder/hcloud/step_create_snapshot.go b/builder/hcloud/step_create_snapshot.go new file mode 100644 index 000000000..11eea9dfb --- /dev/null +++ b/builder/hcloud/step_create_snapshot.go @@ -0,0 +1,53 @@ +package hcloud + +import ( + "context" + "fmt" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hetznercloud/hcloud-go/hcloud" +) + +type stepCreateSnapshot struct{} + +func (s *stepCreateSnapshot) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + client := state.Get("hcloudClient").(*hcloud.Client) + ui := state.Get("ui").(packer.Ui) + c := state.Get("config").(*Config) + serverID := state.Get("server_id").(int) + + ui.Say("Creating snapshot ...") + ui.Say("This can take some time") + result, _, err := client.Server.CreateImage(context.TODO(), &hcloud.Server{ID: serverID}, &hcloud.ServerCreateImageOpts{ + Type: hcloud.ImageTypeSnapshot, + Description: hcloud.String(c.SnapshotName), + }) + if err != nil { + err := fmt.Errorf("Error creating snapshot: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + state.Put("snapshot_id", result.Image.ID) + state.Put("snapshot_name", c.SnapshotName) + _, errCh := client.Action.WatchProgress(context.TODO(), result.Action) + for { + select { + case err1 := <-errCh: + if err1 == nil { + return multistep.ActionContinue + } else { + err := fmt.Errorf("Error creating snapshot: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + } + } +} + +func (s *stepCreateSnapshot) Cleanup(state multistep.StateBag) { + // no cleanup +} diff --git a/builder/hcloud/step_create_sshkey.go b/builder/hcloud/step_create_sshkey.go new file mode 100644 index 000000000..222830927 --- /dev/null +++ b/builder/hcloud/step_create_sshkey.go @@ -0,0 +1,118 @@ +package hcloud + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "log" + "os" + "runtime" + + "github.com/hashicorp/packer/common/uuid" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hetznercloud/hcloud-go/hcloud" + "golang.org/x/crypto/ssh" +) + +type stepCreateSSHKey struct { + Debug bool + DebugKeyPath string + + keyId int +} + +func (s *stepCreateSSHKey) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + client := state.Get("hcloudClient").(*hcloud.Client) + ui := state.Get("ui").(packer.Ui) + c := state.Get("config").(*Config) + ui.Say("Creating temporary ssh key for server...") + + priv, err := rsa.GenerateKey(rand.Reader, 2014) + + // ASN.1 DER encoded form + privDER := x509.MarshalPKCS1PrivateKey(priv) + privBLK := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: privDER, + } + + // Set the private key in the config for later + c.Comm.SSHPrivateKey = pem.EncodeToMemory(&privBLK) + + // Marshal the public key into SSH compatible format + // TODO properly handle the public key error + pub, _ := ssh.NewPublicKey(&priv.PublicKey) + pubSSHFormat := string(ssh.MarshalAuthorizedKey(pub)) + + // The name of the public key on the Hetzner Cloud + name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) + + // Create the key! + key, _, err := client.SSHKey.Create(context.TODO(), hcloud.SSHKeyCreateOpts{ + Name: name, + PublicKey: pubSSHFormat, + }) + if err != nil { + err := fmt.Errorf("Error creating temporary SSH key: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // We use this to check cleanup + s.keyId = key.ID + + log.Printf("temporary ssh key name: %s", name) + + // Remember some state for the future + state.Put("ssh_key_id", key.ID) + + // If we're in debug mode, output the private key to the working directory. + if s.Debug { + ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath)) + f, err := os.Create(s.DebugKeyPath) + if err != nil { + state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) + return multistep.ActionHalt + } + defer f.Close() + + // Write the key out + if _, err := f.Write(pem.EncodeToMemory(&privBLK)); err != nil { + state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) + return multistep.ActionHalt + } + + // Chmod it so that it is SSH ready + if runtime.GOOS != "windows" { + if err := f.Chmod(0600); err != nil { + state.Put("error", fmt.Errorf("Error setting permissions of debug key: %s", err)) + return multistep.ActionHalt + } + } + } + return multistep.ActionContinue +} + +func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) { + // If no key id is set, then we never created it, so just return + if s.keyId == 0 { + return + } + + client := state.Get("hcloudClient").(*hcloud.Client) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Deleting temporary ssh key...") + _, err := client.SSHKey.Delete(context.TODO(), &hcloud.SSHKey{ID: s.keyId}) + if err != nil { + log.Printf("Error cleaning up ssh key: %s", err) + ui.Error(fmt.Sprintf( + "Error cleaning up ssh key. Please delete the key manually: %s", err)) + } +} diff --git a/builder/hcloud/step_shutdown_server.go b/builder/hcloud/step_shutdown_server.go new file mode 100644 index 000000000..60f488870 --- /dev/null +++ b/builder/hcloud/step_shutdown_server.go @@ -0,0 +1,49 @@ +package hcloud + +import ( + "context" + "fmt" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hetznercloud/hcloud-go/hcloud" +) + +type stepShutdownServer struct{} + +func (s *stepShutdownServer) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + client := state.Get("hcloudClient").(*hcloud.Client) + ui := state.Get("ui").(packer.Ui) + serverID := state.Get("server_id").(int) + + ui.Say("Shutting down server...") + + action, _, err := client.Server.Shutdown(context.TODO(), &hcloud.Server{ID: serverID}) + + if err != nil { + err := fmt.Errorf("Error stopping server: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + _, errCh := client.Action.WatchProgress(context.TODO(), action) + for { + select { + case err1 := <-errCh: + if err1 == nil { + return multistep.ActionContinue + } else { + err := fmt.Errorf("Error stopping server: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + } + } +} + +func (s *stepShutdownServer) Cleanup(state multistep.StateBag) { + // no cleanup +} diff --git a/command/plugin.go b/command/plugin.go index 9f537f344..71db5843e 100644 --- a/command/plugin.go +++ b/command/plugin.go @@ -25,6 +25,7 @@ import ( dockerbuilder "github.com/hashicorp/packer/builder/docker" filebuilder "github.com/hashicorp/packer/builder/file" googlecomputebuilder "github.com/hashicorp/packer/builder/googlecompute" + hcloudbuilder "github.com/hashicorp/packer/builder/hcloud" hypervisobuilder "github.com/hashicorp/packer/builder/hyperv/iso" hypervvmcxbuilder "github.com/hashicorp/packer/builder/hyperv/vmcx" lxcbuilder "github.com/hashicorp/packer/builder/lxc" @@ -95,6 +96,7 @@ var Builders = map[string]packer.Builder{ "docker": new(dockerbuilder.Builder), "file": new(filebuilder.Builder), "googlecompute": new(googlecomputebuilder.Builder), + "hcloud": new(hcloudbuilder.Builder), "hyperv-iso": new(hypervisobuilder.Builder), "hyperv-vmcx": new(hypervvmcxbuilder.Builder), "lxc": new(lxcbuilder.Builder), diff --git a/vendor/github.com/hetznercloud/hcloud-go/LICENSE b/vendor/github.com/hetznercloud/hcloud-go/LICENSE new file mode 100644 index 000000000..69ecbb496 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Hetzner Cloud GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/action.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/action.go new file mode 100644 index 000000000..c31af0ca4 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/action.go @@ -0,0 +1,193 @@ +package hcloud + +import ( + "context" + "fmt" + "time" + + "github.com/hetznercloud/hcloud-go/hcloud/schema" +) + +// Action represents an action in the Hetzner Cloud. +type Action struct { + ID int + Status ActionStatus + Command string + Progress int + Started time.Time + Finished time.Time + ErrorCode string + ErrorMessage string + Resources []*ActionResource +} + +// ActionStatus represents an action's status. +type ActionStatus string + +// List of action statuses. +const ( + ActionStatusRunning ActionStatus = "running" + ActionStatusSuccess ActionStatus = "success" + ActionStatusError ActionStatus = "error" +) + +// ActionResource references other resources from an action. +type ActionResource struct { + ID int + Type ActionResourceType +} + +// ActionResourceType represents an action's resource reference type. +type ActionResourceType string + +// List of action resource reference types. +const ( + ActionResourceTypeServer ActionResourceType = "server" + ActionResourceTypeImage ActionResourceType = "image" + ActionResourceTypeISO ActionResourceType = "iso" + ActionResourceTypeFloatingIP ActionResourceType = "floating_ip" + ActionResourceTypeVolume ActionResourceType = "volume" +) + +// ActionError is the error of an action. +type ActionError struct { + Code string + Message string +} + +func (e ActionError) Error() string { + return fmt.Sprintf("%s (%s)", e.Message, e.Code) +} + +func (a *Action) Error() error { + if a.ErrorCode != "" && a.ErrorMessage != "" { + return ActionError{ + Code: a.ErrorCode, + Message: a.ErrorMessage, + } + } + return nil +} + +// ActionClient is a client for the actions API. +type ActionClient struct { + client *Client +} + +// GetByID retrieves an action by its ID. +func (c *ActionClient) GetByID(ctx context.Context, id int) (*Action, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/actions/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.ActionGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return ActionFromSchema(body.Action), resp, nil +} + +// ActionListOpts specifies options for listing actions. +type ActionListOpts struct { + ListOpts +} + +// List returns a list of actions for a specific page. +func (c *ActionClient) List(ctx context.Context, opts ActionListOpts) ([]*Action, *Response, error) { + path := "/actions?" + valuesForListOpts(opts.ListOpts).Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.ActionListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + actions := make([]*Action, 0, len(body.Actions)) + for _, i := range body.Actions { + actions = append(actions, ActionFromSchema(i)) + } + return actions, resp, nil +} + +// All returns all actions. +func (c *ActionClient) All(ctx context.Context) ([]*Action, error) { + allActions := []*Action{} + + opts := ActionListOpts{} + opts.PerPage = 50 + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + actions, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allActions = append(allActions, actions...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allActions, nil +} + +// WatchProgress watches the action's progress until it completes with success or error. +func (c *ActionClient) WatchProgress(ctx context.Context, action *Action) (<-chan int, <-chan error) { + errCh := make(chan error, 1) + progressCh := make(chan int) + + go func() { + defer close(errCh) + defer close(progressCh) + + ticker := time.NewTicker(c.client.pollInterval) + sendProgress := func(p int) { + select { + case progressCh <- p: + break + default: + break + } + } + + for { + select { + case <-ctx.Done(): + errCh <- ctx.Err() + return + case <-ticker.C: + break + } + + a, _, err := c.GetByID(ctx, action.ID) + if err != nil { + errCh <- ctx.Err() + return + } + + switch a.Status { + case ActionStatusRunning: + sendProgress(a.Progress) + break + case ActionStatusSuccess: + sendProgress(100) + errCh <- nil + return + case ActionStatusError: + errCh <- a.Error() + return + } + } + }() + + return progressCh, errCh +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/client.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/client.go new file mode 100644 index 000000000..9a80d1713 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/client.go @@ -0,0 +1,329 @@ +package hcloud + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "math" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/hetznercloud/hcloud-go/hcloud/schema" +) + +// Endpoint is the base URL of the API. +const Endpoint = "https://api.hetzner.cloud/v1" + +// UserAgent is the value for the library part of the User-Agent header +// that is sent with each request. +const UserAgent = "hcloud-go/" + Version + +// A BackoffFunc returns the duration to wait before performing the +// next retry. The retries argument specifies how many retries have +// already been performed. When called for the first time, retries is 0. +type BackoffFunc func(retries int) time.Duration + +// ConstantBackoff returns a BackoffFunc which backs off for +// constant duration d. +func ConstantBackoff(d time.Duration) BackoffFunc { + return func(_ int) time.Duration { + return d + } +} + +// ExponentialBackoff returns a BackoffFunc which implements an exponential +// backoff using the formula: b^retries * d +func ExponentialBackoff(b float64, d time.Duration) BackoffFunc { + return func(retries int) time.Duration { + return time.Duration(math.Pow(b, float64(retries))) * d + } +} + +// Client is a client for the Hetzner Cloud API. +type Client struct { + endpoint string + token string + pollInterval time.Duration + backoffFunc BackoffFunc + httpClient *http.Client + applicationName string + applicationVersion string + userAgent string + + Action ActionClient + Datacenter DatacenterClient + FloatingIP FloatingIPClient + Image ImageClient + ISO ISOClient + Location LocationClient + Pricing PricingClient + Server ServerClient + ServerType ServerTypeClient + SSHKey SSHKeyClient + Volume VolumeClient +} + +// A ClientOption is used to configure a Client. +type ClientOption func(*Client) + +// WithEndpoint configures a Client to use the specified API endpoint. +func WithEndpoint(endpoint string) ClientOption { + return func(client *Client) { + client.endpoint = strings.TrimRight(endpoint, "/") + } +} + +// WithToken configures a Client to use the specified token for authentication. +func WithToken(token string) ClientOption { + return func(client *Client) { + client.token = token + } +} + +// WithPollInterval configures a Client to use the specified interval when polling +// from the API. +func WithPollInterval(pollInterval time.Duration) ClientOption { + return func(client *Client) { + client.pollInterval = pollInterval + } +} + +// WithBackoffFunc configures a Client to use the specified backoff function. +func WithBackoffFunc(f BackoffFunc) ClientOption { + return func(client *Client) { + client.backoffFunc = f + } +} + +// WithApplication configures a Client with the given application name and +// application version. The version may be blank. Programs are encouraged +// to at least set an application name. +func WithApplication(name, version string) ClientOption { + return func(client *Client) { + client.applicationName = name + client.applicationVersion = version + } +} + +// NewClient creates a new client. +func NewClient(options ...ClientOption) *Client { + client := &Client{ + endpoint: Endpoint, + httpClient: &http.Client{}, + backoffFunc: ExponentialBackoff(2, 500*time.Millisecond), + pollInterval: 500 * time.Millisecond, + } + + for _, option := range options { + option(client) + } + + client.buildUserAgent() + + client.Action = ActionClient{client: client} + client.Datacenter = DatacenterClient{client: client} + client.FloatingIP = FloatingIPClient{client: client} + client.Image = ImageClient{client: client} + client.ISO = ISOClient{client: client} + client.Location = LocationClient{client: client} + client.Pricing = PricingClient{client: client} + client.Server = ServerClient{client: client} + client.ServerType = ServerTypeClient{client: client} + client.SSHKey = SSHKeyClient{client: client} + client.Volume = VolumeClient{client: client} + + return client +} + +// NewRequest creates an HTTP request against the API. The returned request +// is assigned with ctx and has all necessary headers set (auth, user agent, etc.). +func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { + url := c.endpoint + path + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", c.userAgent) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + req = req.WithContext(ctx) + return req, nil +} + +// Do performs an HTTP request against the API. +func (c *Client) Do(r *http.Request, v interface{}) (*Response, error) { + var retries int + for { + resp, err := c.httpClient.Do(r) + if err != nil { + return nil, err + } + response := &Response{Response: resp} + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + resp.Body.Close() + return response, err + } + resp.Body.Close() + resp.Body = ioutil.NopCloser(bytes.NewReader(body)) + + if err = response.readMeta(body); err != nil { + return response, fmt.Errorf("hcloud: error reading response meta data: %s", err) + } + + if resp.StatusCode >= 400 && resp.StatusCode <= 599 { + err = errorFromResponse(resp, body) + if err == nil { + err = fmt.Errorf("hcloud: server responded with status code %d", resp.StatusCode) + } else { + if err, ok := err.(Error); ok && err.Code == ErrorCodeRateLimitExceeded { + c.backoff(retries) + retries++ + continue + } + } + return response, err + } + if v != nil { + if w, ok := v.(io.Writer); ok { + _, err = io.Copy(w, bytes.NewReader(body)) + } else { + err = json.Unmarshal(body, v) + } + } + + return response, err + } +} + +func (c *Client) backoff(retries int) { + time.Sleep(c.backoffFunc(retries)) +} + +func (c *Client) all(f func(int) (*Response, error)) (*Response, error) { + var ( + page = 1 + ) + for { + resp, err := f(page) + if err != nil { + return nil, err + } + if resp.Meta.Pagination == nil || resp.Meta.Pagination.NextPage == 0 { + return resp, nil + } + page = resp.Meta.Pagination.NextPage + } +} + +func (c *Client) buildUserAgent() { + switch { + case c.applicationName != "" && c.applicationVersion != "": + c.userAgent = c.applicationName + "/" + c.applicationVersion + " " + UserAgent + case c.applicationName != "" && c.applicationVersion == "": + c.userAgent = c.applicationName + " " + UserAgent + default: + c.userAgent = UserAgent + } +} + +func errorFromResponse(resp *http.Response, body []byte) error { + if !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { + return nil + } + + var respBody schema.ErrorResponse + if err := json.Unmarshal(body, &respBody); err != nil { + return nil + } + if respBody.Error.Code == "" && respBody.Error.Message == "" { + return nil + } + return ErrorFromSchema(respBody.Error) +} + +// Response represents a response from the API. It embeds http.Response. +type Response struct { + *http.Response + Meta Meta +} + +func (r *Response) readMeta(body []byte) error { + if h := r.Header.Get("RateLimit-Limit"); h != "" { + r.Meta.Ratelimit.Limit, _ = strconv.Atoi(h) + } + if h := r.Header.Get("RateLimit-Remaining"); h != "" { + r.Meta.Ratelimit.Remaining, _ = strconv.Atoi(h) + } + if h := r.Header.Get("RateLimit-Reset"); h != "" { + if ts, err := strconv.ParseInt(h, 10, 64); err == nil { + r.Meta.Ratelimit.Reset = time.Unix(ts, 0) + } + } + + if strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") { + var s schema.MetaResponse + if err := json.Unmarshal(body, &s); err != nil { + return err + } + if s.Meta.Pagination != nil { + p := PaginationFromSchema(*s.Meta.Pagination) + r.Meta.Pagination = &p + } + } + + return nil +} + +// Meta represents meta information included in an API response. +type Meta struct { + Pagination *Pagination + Ratelimit Ratelimit +} + +// Pagination represents pagination meta information. +type Pagination struct { + Page int + PerPage int + PreviousPage int + NextPage int + LastPage int + TotalEntries int +} + +// Ratelimit represents ratelimit information. +type Ratelimit struct { + Limit int + Remaining int + Reset time.Time +} + +// ListOpts specifies options for listing resources. +type ListOpts struct { + Page int // Page (starting at 1) + PerPage int // Items per page (0 means default) + LabelSelector string // Label selector for filtering by labels +} + +func valuesForListOpts(opts ListOpts) url.Values { + vals := url.Values{} + if opts.Page > 0 { + vals.Add("page", strconv.Itoa(opts.Page)) + } + if opts.PerPage > 0 { + vals.Add("per_page", strconv.Itoa(opts.PerPage)) + } + if len(opts.LabelSelector) > 0 { + vals.Add("label_selector", opts.LabelSelector) + } + return vals +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/datacenter.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/datacenter.go new file mode 100644 index 000000000..7b4429feb --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/datacenter.go @@ -0,0 +1,124 @@ +package hcloud + +import ( + "context" + "fmt" + "net/url" + "strconv" + + "github.com/hetznercloud/hcloud-go/hcloud/schema" +) + +// Datacenter represents a datacenter in the Hetzner Cloud. +type Datacenter struct { + ID int + Name string + Description string + Location *Location + ServerTypes DatacenterServerTypes +} + +// DatacenterServerTypes represents the server types available and supported in a datacenter. +type DatacenterServerTypes struct { + Supported []*ServerType + Available []*ServerType +} + +// DatacenterClient is a client for the datacenter API. +type DatacenterClient struct { + client *Client +} + +// GetByID retrieves a datacenter by its ID. +func (c *DatacenterClient) GetByID(ctx context.Context, id int) (*Datacenter, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/datacenters/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.DatacenterGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, resp, err + } + return DatacenterFromSchema(body.Datacenter), resp, nil +} + +// GetByName retrieves an datacenter by its name. +func (c *DatacenterClient) GetByName(ctx context.Context, name string) (*Datacenter, *Response, error) { + path := "/datacenters?name=" + url.QueryEscape(name) + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.DatacenterListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + + if len(body.Datacenters) == 0 { + return nil, resp, nil + } + return DatacenterFromSchema(body.Datacenters[0]), resp, nil +} + +// Get retrieves a datacenter by its ID if the input can be parsed as an integer, otherwise it retrieves a datacenter by its name. +func (c *DatacenterClient) Get(ctx context.Context, idOrName string) (*Datacenter, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// DatacenterListOpts specifies options for listing datacenters. +type DatacenterListOpts struct { + ListOpts +} + +// List returns a list of datacenters for a specific page. +func (c *DatacenterClient) List(ctx context.Context, opts DatacenterListOpts) ([]*Datacenter, *Response, error) { + path := "/datacenters?" + valuesForListOpts(opts.ListOpts).Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.DatacenterListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + datacenters := make([]*Datacenter, 0, len(body.Datacenters)) + for _, i := range body.Datacenters { + datacenters = append(datacenters, DatacenterFromSchema(i)) + } + return datacenters, resp, nil +} + +// All returns all datacenters. +func (c *DatacenterClient) All(ctx context.Context) ([]*Datacenter, error) { + allDatacenters := []*Datacenter{} + + opts := DatacenterListOpts{} + opts.PerPage = 50 + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + datacenters, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allDatacenters = append(allDatacenters, datacenters...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allDatacenters, nil +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/error.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/error.go new file mode 100644 index 000000000..6235f081d --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/error.go @@ -0,0 +1,52 @@ +package hcloud + +import "fmt" + +// ErrorCode represents an error code returned from the API. +type ErrorCode string + +// Error codes returned from the API. +const ( + ErrorCodeServiceError ErrorCode = "service_error" // Generic server error + ErrorCodeRateLimitExceeded ErrorCode = "rate_limit_exceeded" // Rate limit exceeded + ErrorCodeUnknownError ErrorCode = "unknown_error" // Unknown error + ErrorCodeNotFound ErrorCode = "not_found" // Resource not found + ErrorCodeInvalidInput ErrorCode = "invalid_input" // Validation error + + // Deprecated error codes + + // The actual value of this error code is limit_reached. The new error code + // rate_limit_exceeded for ratelimiting was introduced before Hetzner Cloud + // launched into the public. To make clients using the old error code still + // work as expected, we set the value of the old error code to that of the + // new error code. + ErrorCodeLimitReached = ErrorCodeRateLimitExceeded +) + +// Error is an error returned from the API. +type Error struct { + Code ErrorCode + Message string + Details interface{} +} + +func (e Error) Error() string { + return fmt.Sprintf("%s (%s)", e.Message, e.Code) +} + +// ErrorDetailsInvalidInput contains the details of an 'invalid_input' error. +type ErrorDetailsInvalidInput struct { + Fields []ErrorDetailsInvalidInputField +} + +// ErrorDetailsInvalidInputField contains the validation errors reported on a field. +type ErrorDetailsInvalidInputField struct { + Name string + Messages []string +} + +// IsError returns whether err is an API error with the given error code. +func IsError(err error, code ErrorCode) bool { + apiErr, ok := err.(Error) + return ok && apiErr.Code == code +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/floating_ip.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/floating_ip.go new file mode 100644 index 000000000..e1372db06 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/floating_ip.go @@ -0,0 +1,335 @@ +package hcloud + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net" + + "github.com/hetznercloud/hcloud-go/hcloud/schema" +) + +// FloatingIP represents a Floating IP in the Hetzner Cloud. +type FloatingIP struct { + ID int + Description string + IP net.IP + Network *net.IPNet + Type FloatingIPType + Server *Server + DNSPtr map[string]string + HomeLocation *Location + Blocked bool + Protection FloatingIPProtection + Labels map[string]string +} + +// DNSPtrForIP returns the reverse DNS pointer of the IP address. +func (f *FloatingIP) DNSPtrForIP(ip net.IP) string { + return f.DNSPtr[ip.String()] +} + +// FloatingIPProtection represents the protection level of a Floating IP. +type FloatingIPProtection struct { + Delete bool +} + +// FloatingIPType represents the type of a Floating IP. +type FloatingIPType string + +// Floating IP types. +const ( + FloatingIPTypeIPv4 FloatingIPType = "ipv4" + FloatingIPTypeIPv6 FloatingIPType = "ipv6" +) + +// FloatingIPClient is a client for the Floating IP API. +type FloatingIPClient struct { + client *Client +} + +// GetByID retrieves a Floating IP by its ID. +func (c *FloatingIPClient) GetByID(ctx context.Context, id int) (*FloatingIP, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/floating_ips/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.FloatingIPGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, resp, err + } + return FloatingIPFromSchema(body.FloatingIP), resp, nil +} + +// FloatingIPListOpts specifies options for listing Floating IPs. +type FloatingIPListOpts struct { + ListOpts +} + +// List returns a list of Floating IPs for a specific page. +func (c *FloatingIPClient) List(ctx context.Context, opts FloatingIPListOpts) ([]*FloatingIP, *Response, error) { + path := "/floating_ips?" + valuesForListOpts(opts.ListOpts).Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.FloatingIPListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + floatingIPs := make([]*FloatingIP, 0, len(body.FloatingIPs)) + for _, s := range body.FloatingIPs { + floatingIPs = append(floatingIPs, FloatingIPFromSchema(s)) + } + return floatingIPs, resp, nil +} + +// All returns all Floating IPs. +func (c *FloatingIPClient) All(ctx context.Context) ([]*FloatingIP, error) { + return c.AllWithOpts(ctx, FloatingIPListOpts{ListOpts{PerPage: 50}}) +} + +// AllWithOpts returns all Floating IPs for the given options. +func (c *FloatingIPClient) AllWithOpts(ctx context.Context, opts FloatingIPListOpts) ([]*FloatingIP, error) { + allFloatingIPs := []*FloatingIP{} + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + floatingIPs, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allFloatingIPs = append(allFloatingIPs, floatingIPs...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allFloatingIPs, nil +} + +// FloatingIPCreateOpts specifies options for creating a Floating IP. +type FloatingIPCreateOpts struct { + Type FloatingIPType + HomeLocation *Location + Server *Server + Description *string + Labels map[string]string +} + +// Validate checks if options are valid. +func (o FloatingIPCreateOpts) Validate() error { + switch o.Type { + case FloatingIPTypeIPv4, FloatingIPTypeIPv6: + break + default: + return errors.New("missing or invalid type") + } + if o.HomeLocation == nil && o.Server == nil { + return errors.New("one of home location or server is required") + } + return nil +} + +// FloatingIPCreateResult is the result of creating a Floating IP. +type FloatingIPCreateResult struct { + FloatingIP *FloatingIP + Action *Action +} + +// Create creates a Floating IP. +func (c *FloatingIPClient) Create(ctx context.Context, opts FloatingIPCreateOpts) (FloatingIPCreateResult, *Response, error) { + if err := opts.Validate(); err != nil { + return FloatingIPCreateResult{}, nil, err + } + + reqBody := schema.FloatingIPCreateRequest{ + Type: string(opts.Type), + Description: opts.Description, + } + if opts.HomeLocation != nil { + reqBody.HomeLocation = String(opts.HomeLocation.Name) + } + if opts.Server != nil { + reqBody.Server = Int(opts.Server.ID) + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return FloatingIPCreateResult{}, nil, err + } + + req, err := c.client.NewRequest(ctx, "POST", "/floating_ips", bytes.NewReader(reqBodyData)) + if err != nil { + return FloatingIPCreateResult{}, nil, err + } + + var respBody schema.FloatingIPCreateResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return FloatingIPCreateResult{}, resp, err + } + var action *Action + if respBody.Action != nil { + action = ActionFromSchema(*respBody.Action) + } + return FloatingIPCreateResult{ + FloatingIP: FloatingIPFromSchema(respBody.FloatingIP), + Action: action, + }, resp, nil +} + +// Delete deletes a Floating IP. +func (c *FloatingIPClient) Delete(ctx context.Context, floatingIP *FloatingIP) (*Response, error) { + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/floating_ips/%d", floatingIP.ID), nil) + if err != nil { + return nil, err + } + return c.client.Do(req, nil) +} + +// FloatingIPUpdateOpts specifies options for updating a Floating IP. +type FloatingIPUpdateOpts struct { + Description string + Labels map[string]string +} + +// Update updates a Floating IP. +func (c *FloatingIPClient) Update(ctx context.Context, floatingIP *FloatingIP, opts FloatingIPUpdateOpts) (*FloatingIP, *Response, error) { + reqBody := schema.FloatingIPUpdateRequest{ + Description: opts.Description, + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/floating_ips/%d", floatingIP.ID) + req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.FloatingIPUpdateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return FloatingIPFromSchema(respBody.FloatingIP), resp, nil +} + +// Assign assigns a Floating IP to a server. +func (c *FloatingIPClient) Assign(ctx context.Context, floatingIP *FloatingIP, server *Server) (*Action, *Response, error) { + reqBody := schema.FloatingIPActionAssignRequest{ + Server: server.ID, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/floating_ips/%d/actions/assign", floatingIP.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + var respBody schema.FloatingIPActionAssignResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// Unassign unassigns a Floating IP from the currently assigned server. +func (c *FloatingIPClient) Unassign(ctx context.Context, floatingIP *FloatingIP) (*Action, *Response, error) { + var reqBody schema.FloatingIPActionUnassignRequest + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/floating_ips/%d/actions/unassign", floatingIP.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + var respBody schema.FloatingIPActionUnassignResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// ChangeDNSPtr changes or resets the reverse DNS pointer for a Floating IP address. +// Pass a nil ptr to reset the reverse DNS pointer to its default value. +func (c *FloatingIPClient) ChangeDNSPtr(ctx context.Context, floatingIP *FloatingIP, ip string, ptr *string) (*Action, *Response, error) { + reqBody := schema.FloatingIPActionChangeDNSPtrRequest{ + IP: ip, + DNSPtr: ptr, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/floating_ips/%d/actions/change_dns_ptr", floatingIP.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.FloatingIPActionChangeDNSPtrResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// FloatingIPChangeProtectionOpts specifies options for changing the resource protection level of a Floating IP. +type FloatingIPChangeProtectionOpts struct { + Delete *bool +} + +// ChangeProtection changes the resource protection level of a Floating IP. +func (c *FloatingIPClient) ChangeProtection(ctx context.Context, floatingIP *FloatingIP, opts FloatingIPChangeProtectionOpts) (*Action, *Response, error) { + reqBody := schema.FloatingIPActionChangeProtectionRequest{ + Delete: opts.Delete, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/floating_ips/%d/actions/change_protection", floatingIP.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.FloatingIPActionChangeProtectionResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/hcloud.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/hcloud.go new file mode 100644 index 000000000..8d3b398a3 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/hcloud.go @@ -0,0 +1,5 @@ +// Package hcloud is a library for the Hetzner Cloud API. +package hcloud + +// Version is the library's version following Semantic Versioning. +const Version = "1.9.0" diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/helper.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/helper.go new file mode 100644 index 000000000..fad372547 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/helper.go @@ -0,0 +1,10 @@ +package hcloud + +// String returns a pointer to the passed string s. +func String(s string) *string { return &s } + +// Int returns a pointer to the passed integer i. +func Int(i int) *int { return &i } + +// Bool returns a pointer to the passed bool b. +func Bool(b bool) *bool { return &b } diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/image.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/image.go new file mode 100644 index 000000000..7089facd3 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/image.go @@ -0,0 +1,243 @@ +package hcloud + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/url" + "strconv" + "time" + + "github.com/hetznercloud/hcloud-go/hcloud/schema" +) + +// Image represents an Image in the Hetzner Cloud. +type Image struct { + ID int + Name string + Type ImageType + Status ImageStatus + Description string + ImageSize float32 + DiskSize float32 + Created time.Time + CreatedFrom *Server + BoundTo *Server + RapidDeploy bool + + OSFlavor string + OSVersion string + + Protection ImageProtection + Deprecated time.Time // The zero value denotes the image is not deprecated. + Labels map[string]string +} + +// IsDeprecated returns whether the image is deprecated. +func (image *Image) IsDeprecated() bool { + return !image.Deprecated.IsZero() +} + +// ImageProtection represents the protection level of an image. +type ImageProtection struct { + Delete bool +} + +// ImageType specifies the type of an image. +type ImageType string + +const ( + // ImageTypeSnapshot represents a snapshot image. + ImageTypeSnapshot ImageType = "snapshot" + // ImageTypeBackup represents a backup image. + ImageTypeBackup ImageType = "backup" + // ImageTypeSystem represents a system image. + ImageTypeSystem ImageType = "system" +) + +// ImageStatus specifies the status of an image. +type ImageStatus string + +const ( + // ImageStatusCreating is the status when an image is being created. + ImageStatusCreating ImageStatus = "creating" + // ImageStatusAvailable is the status when an image is available. + ImageStatusAvailable ImageStatus = "available" +) + +// ImageClient is a client for the image API. +type ImageClient struct { + client *Client +} + +// GetByID retrieves an image by its ID. +func (c *ImageClient) GetByID(ctx context.Context, id int) (*Image, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/images/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.ImageGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return ImageFromSchema(body.Image), resp, nil +} + +// GetByName retrieves an image by its name. +func (c *ImageClient) GetByName(ctx context.Context, name string) (*Image, *Response, error) { + path := "/images?name=" + url.QueryEscape(name) + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.ImageListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + + if len(body.Images) == 0 { + return nil, resp, nil + } + return ImageFromSchema(body.Images[0]), resp, nil +} + +// Get retrieves an image by its ID if the input can be parsed as an integer, otherwise it retrieves an image by its name. +func (c *ImageClient) Get(ctx context.Context, idOrName string) (*Image, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// ImageListOpts specifies options for listing images. +type ImageListOpts struct { + ListOpts +} + +// List returns a list of images for a specific page. +func (c *ImageClient) List(ctx context.Context, opts ImageListOpts) ([]*Image, *Response, error) { + path := "/images?" + valuesForListOpts(opts.ListOpts).Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.ImageListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + images := make([]*Image, 0, len(body.Images)) + for _, i := range body.Images { + images = append(images, ImageFromSchema(i)) + } + return images, resp, nil +} + +// All returns all images. +func (c *ImageClient) All(ctx context.Context) ([]*Image, error) { + return c.AllWithOpts(ctx, ImageListOpts{ListOpts{PerPage: 50}}) +} + +// AllWithOpts returns all images for the given options. +func (c *ImageClient) AllWithOpts(ctx context.Context, opts ImageListOpts) ([]*Image, error) { + allImages := []*Image{} + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + images, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allImages = append(allImages, images...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allImages, nil +} + +// Delete deletes an image. +func (c *ImageClient) Delete(ctx context.Context, image *Image) (*Response, error) { + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/images/%d", image.ID), nil) + if err != nil { + return nil, err + } + return c.client.Do(req, nil) +} + +// ImageUpdateOpts specifies options for updating an image. +type ImageUpdateOpts struct { + Description *string + Type ImageType + Labels map[string]string +} + +// Update updates an image. +func (c *ImageClient) Update(ctx context.Context, image *Image, opts ImageUpdateOpts) (*Image, *Response, error) { + reqBody := schema.ImageUpdateRequest{ + Description: opts.Description, + } + if opts.Type != "" { + reqBody.Type = String(string(opts.Type)) + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/images/%d", image.ID) + req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ImageUpdateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ImageFromSchema(respBody.Image), resp, nil +} + +// ImageChangeProtectionOpts specifies options for changing the resource protection level of an image. +type ImageChangeProtectionOpts struct { + Delete *bool +} + +// ChangeProtection changes the resource protection level of an image. +func (c *ImageClient) ChangeProtection(ctx context.Context, image *Image, opts ImageChangeProtectionOpts) (*Action, *Response, error) { + reqBody := schema.ImageActionChangeProtectionRequest{ + Delete: opts.Delete, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/images/%d/actions/change_protection", image.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ImageActionChangeProtectionResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/iso.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/iso.go new file mode 100644 index 000000000..852e69841 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/iso.go @@ -0,0 +1,135 @@ +package hcloud + +import ( + "context" + "fmt" + "net/url" + "strconv" + "time" + + "github.com/hetznercloud/hcloud-go/hcloud/schema" +) + +// ISO represents an ISO image in the Hetzner Cloud. +type ISO struct { + ID int + Name string + Description string + Type ISOType + Deprecated time.Time +} + +// IsDeprecated returns true if the ISO is deprecated +func (iso *ISO) IsDeprecated() bool { + return !iso.Deprecated.IsZero() +} + +// ISOType specifies the type of an ISO image. +type ISOType string + +const ( + // ISOTypePublic is the type of a public ISO image. + ISOTypePublic ISOType = "public" + + // ISOTypePrivate is the type of a private ISO image. + ISOTypePrivate ISOType = "private" +) + +// ISOClient is a client for the ISO API. +type ISOClient struct { + client *Client +} + +// GetByID retrieves an ISO by its ID. +func (c *ISOClient) GetByID(ctx context.Context, id int) (*ISO, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/isos/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.ISOGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, resp, err + } + return ISOFromSchema(body.ISO), resp, nil +} + +// GetByName retrieves an ISO by its name. +func (c *ISOClient) GetByName(ctx context.Context, name string) (*ISO, *Response, error) { + path := "/isos?name=" + url.QueryEscape(name) + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.ISOListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + + if len(body.ISOs) == 0 { + return nil, resp, nil + } + return ISOFromSchema(body.ISOs[0]), resp, nil +} + +// Get retrieves an ISO by its ID if the input can be parsed as an integer, otherwise it retrieves an ISO by its name. +func (c *ISOClient) Get(ctx context.Context, idOrName string) (*ISO, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// ISOListOpts specifies options for listing isos. +type ISOListOpts struct { + ListOpts +} + +// List returns a list of ISOs for a specific page. +func (c *ISOClient) List(ctx context.Context, opts ISOListOpts) ([]*ISO, *Response, error) { + path := "/isos?" + valuesForListOpts(opts.ListOpts).Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.ISOListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + isos := make([]*ISO, 0, len(body.ISOs)) + for _, i := range body.ISOs { + isos = append(isos, ISOFromSchema(i)) + } + return isos, resp, nil +} + +// All returns all ISOs. +func (c *ISOClient) All(ctx context.Context) ([]*ISO, error) { + allISOs := []*ISO{} + + opts := ISOListOpts{} + opts.PerPage = 50 + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + isos, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allISOs = append(allISOs, isos...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allISOs, nil +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/location.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/location.go new file mode 100644 index 000000000..4ab55ef11 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/location.go @@ -0,0 +1,120 @@ +package hcloud + +import ( + "context" + "fmt" + "net/url" + "strconv" + + "github.com/hetznercloud/hcloud-go/hcloud/schema" +) + +// Location represents a location in the Hetzner Cloud. +type Location struct { + ID int + Name string + Description string + Country string + City string + Latitude float64 + Longitude float64 +} + +// LocationClient is a client for the location API. +type LocationClient struct { + client *Client +} + +// GetByID retrieves a location by its ID. +func (c *LocationClient) GetByID(ctx context.Context, id int) (*Location, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/locations/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.LocationGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, resp, err + } + return LocationFromSchema(body.Location), resp, nil +} + +// GetByName retrieves an location by its name. +func (c *LocationClient) GetByName(ctx context.Context, name string) (*Location, *Response, error) { + path := "/locations?name=" + url.QueryEscape(name) + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.LocationListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + + if len(body.Locations) == 0 { + return nil, resp, nil + } + return LocationFromSchema(body.Locations[0]), resp, nil +} + +// Get retrieves a location by its ID if the input can be parsed as an integer, otherwise it retrieves a location by its name. +func (c *LocationClient) Get(ctx context.Context, idOrName string) (*Location, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// LocationListOpts specifies options for listing location. +type LocationListOpts struct { + ListOpts +} + +// List returns a list of locations for a specific page. +func (c *LocationClient) List(ctx context.Context, opts LocationListOpts) ([]*Location, *Response, error) { + path := "/locations?" + valuesForListOpts(opts.ListOpts).Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.LocationListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + locations := make([]*Location, 0, len(body.Locations)) + for _, i := range body.Locations { + locations = append(locations, LocationFromSchema(i)) + } + return locations, resp, nil +} + +// All returns all locations. +func (c *LocationClient) All(ctx context.Context) ([]*Location, error) { + allLocations := []*Location{} + + opts := LocationListOpts{} + opts.PerPage = 50 + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + locations, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allLocations = append(allLocations, locations...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allLocations, nil +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/pricing.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/pricing.go new file mode 100644 index 000000000..f2849a648 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/pricing.go @@ -0,0 +1,80 @@ +package hcloud + +import ( + "context" + + "github.com/hetznercloud/hcloud-go/hcloud/schema" +) + +// Pricing specifies pricing information for various resources. +type Pricing struct { + Image ImagePricing + FloatingIP FloatingIPPricing + Traffic TrafficPricing + ServerBackup ServerBackupPricing + ServerTypes []ServerTypePricing +} + +// Price represents a price. Net amount, gross amount, as well as VAT rate are +// specified as strings and it is the user's responsibility to convert them to +// appropriate types for calculations. +type Price struct { + Currency string + VATRate string + Net string + Gross string +} + +// ImagePricing provides pricing information for imaegs. +type ImagePricing struct { + PerGBMonth Price +} + +// FloatingIPPricing provides pricing information for Floating IPs. +type FloatingIPPricing struct { + Monthly Price +} + +// TrafficPricing provides pricing information for traffic. +type TrafficPricing struct { + PerTB Price +} + +// ServerBackupPricing provides pricing information for server backups. +type ServerBackupPricing struct { + Percentage string +} + +// ServerTypePricing provides pricing information for a server type. +type ServerTypePricing struct { + ServerType *ServerType + Pricings []ServerTypeLocationPricing +} + +// ServerTypeLocationPricing provides pricing information for a server type +// at a location. +type ServerTypeLocationPricing struct { + Location *Location + Hourly Price + Monthly Price +} + +// PricingClient is a client for the pricing API. +type PricingClient struct { + client *Client +} + +// Get retrieves pricing information. +func (c *PricingClient) Get(ctx context.Context) (Pricing, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", "/pricing", nil) + if err != nil { + return Pricing{}, nil, err + } + + var body schema.PricingGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return Pricing{}, nil, err + } + return PricingFromSchema(body.Pricing), resp, nil +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema.go new file mode 100644 index 000000000..828033c4d --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema.go @@ -0,0 +1,399 @@ +package hcloud + +import ( + "net" + + "github.com/hetznercloud/hcloud-go/hcloud/schema" +) + +// This file provides converter functions to convert models in the +// schema package to models in the hcloud package. + +// ActionFromSchema converts a schema.Action to an Action. +func ActionFromSchema(s schema.Action) *Action { + action := &Action{ + ID: s.ID, + Status: ActionStatus(s.Status), + Command: s.Command, + Progress: s.Progress, + Started: s.Started, + Resources: []*ActionResource{}, + } + if s.Finished != nil { + action.Finished = *s.Finished + } + if s.Error != nil { + action.ErrorCode = s.Error.Code + action.ErrorMessage = s.Error.Message + } + for _, r := range s.Resources { + action.Resources = append(action.Resources, &ActionResource{ + ID: r.ID, + Type: ActionResourceType(r.Type), + }) + } + return action +} + +// FloatingIPFromSchema converts a schema.FloatingIP to a FloatingIP. +func FloatingIPFromSchema(s schema.FloatingIP) *FloatingIP { + f := &FloatingIP{ + ID: s.ID, + Type: FloatingIPType(s.Type), + HomeLocation: LocationFromSchema(s.HomeLocation), + Blocked: s.Blocked, + Protection: FloatingIPProtection{ + Delete: s.Protection.Delete, + }, + } + if s.Description != nil { + f.Description = *s.Description + } + if s.Server != nil { + f.Server = &Server{ID: *s.Server} + } + if f.Type == FloatingIPTypeIPv4 { + f.IP = net.ParseIP(s.IP) + } else { + f.IP, f.Network, _ = net.ParseCIDR(s.IP) + } + f.DNSPtr = map[string]string{} + for _, entry := range s.DNSPtr { + f.DNSPtr[entry.IP] = entry.DNSPtr + } + f.Labels = map[string]string{} + for key, value := range s.Labels { + f.Labels[key] = value + } + return f +} + +// ISOFromSchema converts a schema.ISO to an ISO. +func ISOFromSchema(s schema.ISO) *ISO { + return &ISO{ + ID: s.ID, + Name: s.Name, + Description: s.Description, + Type: ISOType(s.Type), + Deprecated: s.Deprecated, + } +} + +// LocationFromSchema converts a schema.Location to a Location. +func LocationFromSchema(s schema.Location) *Location { + return &Location{ + ID: s.ID, + Name: s.Name, + Description: s.Description, + Country: s.Country, + City: s.City, + Latitude: s.Latitude, + Longitude: s.Longitude, + } +} + +// DatacenterFromSchema converts a schema.Datacenter to a Datacenter. +func DatacenterFromSchema(s schema.Datacenter) *Datacenter { + d := &Datacenter{ + ID: s.ID, + Name: s.Name, + Description: s.Description, + Location: LocationFromSchema(s.Location), + ServerTypes: DatacenterServerTypes{ + Available: []*ServerType{}, + Supported: []*ServerType{}, + }, + } + for _, t := range s.ServerTypes.Available { + d.ServerTypes.Available = append(d.ServerTypes.Available, &ServerType{ID: t}) + } + for _, t := range s.ServerTypes.Supported { + d.ServerTypes.Supported = append(d.ServerTypes.Supported, &ServerType{ID: t}) + } + return d +} + +// ServerFromSchema converts a schema.Server to a Server. +func ServerFromSchema(s schema.Server) *Server { + server := &Server{ + ID: s.ID, + Name: s.Name, + Status: ServerStatus(s.Status), + Created: s.Created, + PublicNet: ServerPublicNetFromSchema(s.PublicNet), + ServerType: ServerTypeFromSchema(s.ServerType), + IncludedTraffic: s.IncludedTraffic, + RescueEnabled: s.RescueEnabled, + Datacenter: DatacenterFromSchema(s.Datacenter), + Locked: s.Locked, + Protection: ServerProtection{ + Delete: s.Protection.Delete, + Rebuild: s.Protection.Rebuild, + }, + } + if s.Image != nil { + server.Image = ImageFromSchema(*s.Image) + } + if s.BackupWindow != nil { + server.BackupWindow = *s.BackupWindow + } + if s.OutgoingTraffic != nil { + server.OutgoingTraffic = *s.OutgoingTraffic + } + if s.IngoingTraffic != nil { + server.IngoingTraffic = *s.IngoingTraffic + } + if s.ISO != nil { + server.ISO = ISOFromSchema(*s.ISO) + } + server.Labels = map[string]string{} + for key, value := range s.Labels { + server.Labels[key] = value + } + for _, id := range s.Volumes { + server.Volumes = append(server.Volumes, &Volume{ID: id}) + } + return server +} + +// ServerPublicNetFromSchema converts a schema.ServerPublicNet to a ServerPublicNet. +func ServerPublicNetFromSchema(s schema.ServerPublicNet) ServerPublicNet { + publicNet := ServerPublicNet{ + IPv4: ServerPublicNetIPv4FromSchema(s.IPv4), + IPv6: ServerPublicNetIPv6FromSchema(s.IPv6), + } + for _, id := range s.FloatingIPs { + publicNet.FloatingIPs = append(publicNet.FloatingIPs, &FloatingIP{ID: id}) + } + return publicNet +} + +// ServerPublicNetIPv4FromSchema converts a schema.ServerPublicNetIPv4 to +// a ServerPublicNetIPv4. +func ServerPublicNetIPv4FromSchema(s schema.ServerPublicNetIPv4) ServerPublicNetIPv4 { + return ServerPublicNetIPv4{ + IP: net.ParseIP(s.IP), + Blocked: s.Blocked, + DNSPtr: s.DNSPtr, + } +} + +// ServerPublicNetIPv6FromSchema converts a schema.ServerPublicNetIPv6 to +// a ServerPublicNetIPv6. +func ServerPublicNetIPv6FromSchema(s schema.ServerPublicNetIPv6) ServerPublicNetIPv6 { + ipv6 := ServerPublicNetIPv6{ + Blocked: s.Blocked, + DNSPtr: map[string]string{}, + } + ipv6.IP, ipv6.Network, _ = net.ParseCIDR(s.IP) + + for _, dnsPtr := range s.DNSPtr { + ipv6.DNSPtr[dnsPtr.IP] = dnsPtr.DNSPtr + } + return ipv6 +} + +// ServerTypeFromSchema converts a schema.ServerType to a ServerType. +func ServerTypeFromSchema(s schema.ServerType) *ServerType { + st := &ServerType{ + ID: s.ID, + Name: s.Name, + Description: s.Description, + Cores: s.Cores, + Memory: s.Memory, + Disk: s.Disk, + StorageType: StorageType(s.StorageType), + CPUType: CPUType(s.CPUType), + } + for _, price := range s.Prices { + st.Pricings = append(st.Pricings, ServerTypeLocationPricing{ + Location: &Location{Name: price.Location}, + Hourly: Price{ + Net: price.PriceHourly.Net, + Gross: price.PriceHourly.Gross, + }, + Monthly: Price{ + Net: price.PriceMonthly.Net, + Gross: price.PriceMonthly.Gross, + }, + }) + } + return st +} + +// SSHKeyFromSchema converts a schema.SSHKey to a SSHKey. +func SSHKeyFromSchema(s schema.SSHKey) *SSHKey { + sshKey := &SSHKey{ + ID: s.ID, + Name: s.Name, + Fingerprint: s.Fingerprint, + PublicKey: s.PublicKey, + } + sshKey.Labels = map[string]string{} + for key, value := range s.Labels { + sshKey.Labels[key] = value + } + return sshKey +} + +// ImageFromSchema converts a schema.Image to an Image. +func ImageFromSchema(s schema.Image) *Image { + i := &Image{ + ID: s.ID, + Type: ImageType(s.Type), + Status: ImageStatus(s.Status), + Description: s.Description, + DiskSize: s.DiskSize, + Created: s.Created, + RapidDeploy: s.RapidDeploy, + OSFlavor: s.OSFlavor, + Protection: ImageProtection{ + Delete: s.Protection.Delete, + }, + Deprecated: s.Deprecated, + } + if s.Name != nil { + i.Name = *s.Name + } + if s.ImageSize != nil { + i.ImageSize = *s.ImageSize + } + if s.OSVersion != nil { + i.OSVersion = *s.OSVersion + } + if s.CreatedFrom != nil { + i.CreatedFrom = &Server{ + ID: s.CreatedFrom.ID, + Name: s.CreatedFrom.Name, + } + } + if s.BoundTo != nil { + i.BoundTo = &Server{ + ID: *s.BoundTo, + } + } + i.Labels = map[string]string{} + for key, value := range s.Labels { + i.Labels[key] = value + } + return i +} + +// VolumeFromSchema converts a schema.Volume to a Volume. +func VolumeFromSchema(s schema.Volume) *Volume { + v := &Volume{ + ID: s.ID, + Name: s.Name, + Location: LocationFromSchema(s.Location), + Size: s.Size, + LinuxDevice: s.LinuxDevice, + Protection: VolumeProtection{ + Delete: s.Protection.Delete, + }, + Created: s.Created, + } + if s.Server != nil { + v.Server = &Server{ID: *s.Server} + } + v.Labels = map[string]string{} + for key, value := range s.Labels { + v.Labels[key] = value + } + return v +} + +// PaginationFromSchema converts a schema.MetaPagination to a Pagination. +func PaginationFromSchema(s schema.MetaPagination) Pagination { + return Pagination{ + Page: s.Page, + PerPage: s.PerPage, + PreviousPage: s.PreviousPage, + NextPage: s.NextPage, + LastPage: s.LastPage, + TotalEntries: s.TotalEntries, + } +} + +// ErrorFromSchema converts a schema.Error to an Error. +func ErrorFromSchema(s schema.Error) Error { + e := Error{ + Code: ErrorCode(s.Code), + Message: s.Message, + } + + switch d := s.Details.(type) { + case schema.ErrorDetailsInvalidInput: + details := ErrorDetailsInvalidInput{ + Fields: []ErrorDetailsInvalidInputField{}, + } + for _, field := range d.Fields { + details.Fields = append(details.Fields, ErrorDetailsInvalidInputField{ + Name: field.Name, + Messages: field.Messages, + }) + } + e.Details = details + } + return e +} + +// PricingFromSchema converts a schema.Pricing to a Pricing. +func PricingFromSchema(s schema.Pricing) Pricing { + p := Pricing{ + Image: ImagePricing{ + PerGBMonth: Price{ + Currency: s.Currency, + VATRate: s.VATRate, + Net: s.Image.PricePerGBMonth.Net, + Gross: s.Image.PricePerGBMonth.Gross, + }, + }, + FloatingIP: FloatingIPPricing{ + Monthly: Price{ + Currency: s.Currency, + VATRate: s.VATRate, + Net: s.FloatingIP.PriceMonthly.Net, + Gross: s.FloatingIP.PriceMonthly.Gross, + }, + }, + Traffic: TrafficPricing{ + PerTB: Price{ + Currency: s.Currency, + VATRate: s.VATRate, + Net: s.Traffic.PricePerTB.Net, + Gross: s.Traffic.PricePerTB.Gross, + }, + }, + ServerBackup: ServerBackupPricing{ + Percentage: s.ServerBackup.Percentage, + }, + } + for _, serverType := range s.ServerTypes { + var pricings []ServerTypeLocationPricing + for _, price := range serverType.Prices { + pricings = append(pricings, ServerTypeLocationPricing{ + Location: &Location{Name: price.Location}, + Hourly: Price{ + Currency: s.Currency, + VATRate: s.VATRate, + Net: price.PriceHourly.Net, + Gross: price.PriceHourly.Gross, + }, + Monthly: Price{ + Currency: s.Currency, + VATRate: s.VATRate, + Net: price.PriceMonthly.Net, + Gross: price.PriceMonthly.Gross, + }, + }) + } + p.ServerTypes = append(p.ServerTypes, ServerTypePricing{ + ServerType: &ServerType{ + ID: serverType.ID, + Name: serverType.Name, + }, + Pricings: pricings, + }) + } + return p +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/action.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/action.go new file mode 100644 index 000000000..df4d7cf71 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/action.go @@ -0,0 +1,39 @@ +package schema + +import "time" + +// Action defines the schema of an action. +type Action struct { + ID int `json:"id"` + Status string `json:"status"` + Command string `json:"command"` + Progress int `json:"progress"` + Started time.Time `json:"started"` + Finished *time.Time `json:"finished"` + Error *ActionError `json:"error"` + Resources []ActionResourceReference `json:"resources"` +} + +// ActionResourceReference defines the schema of an action resource reference. +type ActionResourceReference struct { + ID int `json:"id"` + Type string `json:"type"` +} + +// ActionError defines the schema of an error embedded +// in an action. +type ActionError struct { + Code string `json:"code"` + Message string `json:"message"` +} + +// ActionGetResponse is the schema of the response when +// retrieving a single action. +type ActionGetResponse struct { + Action Action `json:"action"` +} + +// ActionListResponse defines the schema of the response when listing actions. +type ActionListResponse struct { + Actions []Action `json:"actions"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/datacenter.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/datacenter.go new file mode 100644 index 000000000..3e8178e89 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/datacenter.go @@ -0,0 +1,23 @@ +package schema + +// Datacenter defines the schema of a datacenter. +type Datacenter struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Location Location `json:"location"` + ServerTypes struct { + Supported []int `json:"supported"` + Available []int `json:"available"` + } `json:"server_types"` +} + +// DatacenterGetResponse defines the schema of the response when retrieving a single datacenter. +type DatacenterGetResponse struct { + Datacenter Datacenter `json:"datacenter"` +} + +// DatacenterListResponse defines the schema of the response when listing datacenters. +type DatacenterListResponse struct { + Datacenters []Datacenter `json:"datacenters"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/error.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/error.go new file mode 100644 index 000000000..6bcb6ecad --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/error.go @@ -0,0 +1,43 @@ +package schema + +import "encoding/json" + +// Error represents the schema of an error response. +type Error struct { + Code string `json:"code"` + Message string `json:"message"` + DetailsRaw json.RawMessage `json:"details"` + Details interface{} +} + +// UnmarshalJSON overrides default json unmarshalling. +func (e *Error) UnmarshalJSON(data []byte) (err error) { + type Alias Error + alias := (*Alias)(e) + if err = json.Unmarshal(data, alias); err != nil { + return + } + switch e.Code { + case "invalid_input": + details := ErrorDetailsInvalidInput{} + if err = json.Unmarshal(e.DetailsRaw, &details); err != nil { + return + } + alias.Details = details + } + return +} + +// ErrorResponse defines the schema of a response containing an error. +type ErrorResponse struct { + Error Error `json:"error"` +} + +// ErrorDetailsInvalidInput defines the schema of the Details field +// of an error with code 'invalid_input'. +type ErrorDetailsInvalidInput struct { + Fields []struct { + Name string `json:"name"` + Messages []string `json:"messages"` + } `json:"fields"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/floating_ip.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/floating_ip.go new file mode 100644 index 000000000..95ae2054b --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/floating_ip.go @@ -0,0 +1,112 @@ +package schema + +// FloatingIP defines the schema of a Floating IP. +type FloatingIP struct { + ID int `json:"id"` + Description *string `json:"description"` + IP string `json:"ip"` + Type string `json:"type"` + Server *int `json:"server"` + DNSPtr []FloatingIPDNSPtr `json:"dns_ptr"` + HomeLocation Location `json:"home_location"` + Blocked bool `json:"blocked"` + Protection FloatingIPProtection `json:"protection"` + Labels map[string]string `json:"labels"` +} + +// FloatingIPProtection represents the protection level of a Floating IP. +type FloatingIPProtection struct { + Delete bool `json:"delete"` +} + +// FloatingIPDNSPtr contains reverse DNS information for a +// IPv4 or IPv6 Floating IP. +type FloatingIPDNSPtr struct { + IP string `json:"ip"` + DNSPtr string `json:"dns_ptr"` +} + +// FloatingIPGetResponse defines the schema of the response when +// retrieving a single Floating IP. +type FloatingIPGetResponse struct { + FloatingIP FloatingIP `json:"floating_ip"` +} + +// FloatingIPUpdateRequest defines the schema of the request to update a Floating IP. +type FloatingIPUpdateRequest struct { + Description string `json:"description,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// FloatingIPUpdateResponse defines the schema of the response when updating a Floating IP. +type FloatingIPUpdateResponse struct { + FloatingIP FloatingIP `json:"floating_ip"` +} + +// FloatingIPListResponse defines the schema of the response when +// listing Floating IPs. +type FloatingIPListResponse struct { + FloatingIPs []FloatingIP `json:"floating_ips"` +} + +// FloatingIPCreateRequest defines the schema of the request to +// create a Floating IP. +type FloatingIPCreateRequest struct { + Type string `json:"type"` + HomeLocation *string `json:"home_location,omitempty"` + Server *int `json:"server,omitempty"` + Description *string `json:"description,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// FloatingIPCreateResponse defines the schema of the response +// when creating a Floating IP. +type FloatingIPCreateResponse struct { + FloatingIP FloatingIP `json:"floating_ip"` + Action *Action `json:"action"` +} + +// FloatingIPActionAssignRequest defines the schema of the request to +// create an assign Floating IP action. +type FloatingIPActionAssignRequest struct { + Server int `json:"server"` +} + +// FloatingIPActionAssignResponse defines the schema of the response when +// creating an assign action. +type FloatingIPActionAssignResponse struct { + Action Action `json:"action"` +} + +// FloatingIPActionUnassignRequest defines the schema of the request to +// create an unassign Floating IP action. +type FloatingIPActionUnassignRequest struct{} + +// FloatingIPActionUnassignResponse defines the schema of the response when +// creating an unassign action. +type FloatingIPActionUnassignResponse struct { + Action Action `json:"action"` +} + +// FloatingIPActionChangeDNSPtrRequest defines the schema for the request to +// change a Floating IP's reverse DNS pointer. +type FloatingIPActionChangeDNSPtrRequest struct { + IP string `json:"ip"` + DNSPtr *string `json:"dns_ptr"` +} + +// FloatingIPActionChangeDNSPtrResponse defines the schema of the response when +// creating a change_dns_ptr Floating IP action. +type FloatingIPActionChangeDNSPtrResponse struct { + Action Action `json:"action"` +} + +// FloatingIPActionChangeProtectionRequest defines the schema of the request to change the resource protection of a Floating IP. +type FloatingIPActionChangeProtectionRequest struct { + Delete *bool `json:"delete,omitempty"` +} + +// FloatingIPActionChangeProtectionResponse defines the schema of the response when changing the resource protection of a Floating IP. +type FloatingIPActionChangeProtectionResponse struct { + Action Action `json:"action"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/image.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/image.go new file mode 100644 index 000000000..c354d9ea9 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/image.go @@ -0,0 +1,68 @@ +package schema + +import "time" + +// Image defines the schema of an image. +type Image struct { + ID int `json:"id"` + Status string `json:"status"` + Type string `json:"type"` + Name *string `json:"name"` + Description string `json:"description"` + ImageSize *float32 `json:"image_size"` + DiskSize float32 `json:"disk_size"` + Created time.Time `json:"created"` + CreatedFrom *ImageCreatedFrom `json:"created_from"` + BoundTo *int `json:"bound_to"` + OSFlavor string `json:"os_flavor"` + OSVersion *string `json:"os_version"` + RapidDeploy bool `json:"rapid_deploy"` + Protection ImageProtection `json:"protection"` + Deprecated time.Time `json:"deprecated"` + Labels map[string]string `json:"labels"` +} + +// ImageProtection represents the protection level of a image. +type ImageProtection struct { + Delete bool `json:"delete"` +} + +// ImageCreatedFrom defines the schema of the images created from reference. +type ImageCreatedFrom struct { + ID int `json:"id"` + Name string `json:"name"` +} + +// ImageGetResponse defines the schema of the response when +// retrieving a single image. +type ImageGetResponse struct { + Image Image `json:"image"` +} + +// ImageListResponse defines the schema of the response when +// listing images. +type ImageListResponse struct { + Images []Image `json:"images"` +} + +// ImageUpdateRequest defines the schema of the request to update an image. +type ImageUpdateRequest struct { + Description *string `json:"description,omitempty"` + Type *string `json:"type,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// ImageUpdateResponse defines the schema of the response when updating an image. +type ImageUpdateResponse struct { + Image Image `json:"image"` +} + +// ImageActionChangeProtectionRequest defines the schema of the request to change the resource protection of an image. +type ImageActionChangeProtectionRequest struct { + Delete *bool `json:"delete,omitempty"` +} + +// ImageActionChangeProtectionResponse defines the schema of the response when changing the resource protection of an image. +type ImageActionChangeProtectionResponse struct { + Action Action `json:"action"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/iso.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/iso.go new file mode 100644 index 000000000..e41046896 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/iso.go @@ -0,0 +1,22 @@ +package schema + +import "time" + +// ISO defines the schema of an ISO image. +type ISO struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + Deprecated time.Time `json:"deprecated"` +} + +// ISOGetResponse defines the schema of the response when retrieving a single ISO. +type ISOGetResponse struct { + ISO ISO `json:"iso"` +} + +// ISOListResponse defines the schema of the response when listing ISOs. +type ISOListResponse struct { + ISOs []ISO `json:"isos"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/location.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/location.go new file mode 100644 index 000000000..1f030a9db --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/location.go @@ -0,0 +1,22 @@ +package schema + +// Location defines the schema of a location. +type Location struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Country string `json:"country"` + City string `json:"city"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + +// LocationGetResponse defines the schema of the response when retrieving a single location. +type LocationGetResponse struct { + Location Location `json:"location"` +} + +// LocationListResponse defines the schema of the response when listing locations. +type LocationListResponse struct { + Locations []Location `json:"locations"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/meta.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/meta.go new file mode 100644 index 000000000..9b06cda8c --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/meta.go @@ -0,0 +1,23 @@ +package schema + +// Meta defines the schema of meta information which may be included +// in responses. +type Meta struct { + Pagination *MetaPagination `json:"pagination"` +} + +// MetaPagination defines the schema of pagination information. +type MetaPagination struct { + Page int `json:"page"` + PerPage int `json:"per_page"` + PreviousPage int `json:"previous_page"` + NextPage int `json:"next_page"` + LastPage int `json:"last_page"` + TotalEntries int `json:"total_entries"` +} + +// MetaResponse defines the schema of a response containing +// meta information. +type MetaResponse struct { + Meta Meta `json:"meta"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/pricing.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/pricing.go new file mode 100644 index 000000000..05c2808f4 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/pricing.go @@ -0,0 +1,58 @@ +package schema + +// Pricing defines the schema for pricing information. +type Pricing struct { + Currency string `json:"currency"` + VATRate string `json:"vat_rate"` + Image PricingImage `json:"image"` + FloatingIP PricingFloatingIP `json:"floating_ip"` + Traffic PricingTraffic `json:"traffic"` + ServerBackup PricingServerBackup `json:"server_backup"` + ServerTypes []PricingServerType `json:"server_types"` +} + +// Price defines the schema of a single price with net and gross amount. +type Price struct { + Net string `json:"net"` + Gross string `json:"gross"` +} + +// PricingImage defines the schema of pricing information for an image. +type PricingImage struct { + PricePerGBMonth Price `json:"price_per_gb_month"` +} + +// PricingFloatingIP defines the schema of pricing information for a Floating IP. +type PricingFloatingIP struct { + PriceMonthly Price `json:"price_monthly"` +} + +// PricingTraffic defines the schema of pricing information for traffic. +type PricingTraffic struct { + PricePerTB Price `json:"price_per_tb"` +} + +// PricingServerBackup defines the schema of pricing information for server backups. +type PricingServerBackup struct { + Percentage string `json:"percentage"` +} + +// PricingServerType defines the schema of pricing information for a server type. +type PricingServerType struct { + ID int `json:"id"` + Name string `json:"name"` + Prices []PricingServerTypePrice `json:"prices"` +} + +// PricingServerTypePrice defines the schema of pricing information for a server +// type at a location. +type PricingServerTypePrice struct { + Location string `json:"location"` + PriceHourly Price `json:"price_hourly"` + PriceMonthly Price `json:"price_monthly"` +} + +// PricingGetResponse defines the schema of the response when retrieving pricing information. +type PricingGetResponse struct { + Pricing Pricing `json:"pricing"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/server.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/server.go new file mode 100644 index 000000000..f58e3b8ff --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/server.go @@ -0,0 +1,300 @@ +package schema + +import "time" + +// Server defines the schema of a server. +type Server struct { + ID int `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Created time.Time `json:"created"` + PublicNet ServerPublicNet `json:"public_net"` + ServerType ServerType `json:"server_type"` + IncludedTraffic uint64 `json:"included_traffic"` + OutgoingTraffic *uint64 `json:"outgoing_traffic"` + IngoingTraffic *uint64 `json:"ingoing_traffic"` + BackupWindow *string `json:"backup_window"` + RescueEnabled bool `json:"rescue_enabled"` + ISO *ISO `json:"iso"` + Locked bool `json:"locked"` + Datacenter Datacenter `json:"datacenter"` + Image *Image `json:"image"` + Protection ServerProtection `json:"protection"` + Labels map[string]string `json:"labels"` + Volumes []int `json:"volumes"` +} + +// ServerProtection defines the schema of a server's resource protection. +type ServerProtection struct { + Delete bool `json:"delete"` + Rebuild bool `json:"rebuild"` +} + +// ServerPublicNet defines the schema of a server's +// public network information. +type ServerPublicNet struct { + IPv4 ServerPublicNetIPv4 `json:"ipv4"` + IPv6 ServerPublicNetIPv6 `json:"ipv6"` + FloatingIPs []int `json:"floating_ips"` +} + +// ServerPublicNetIPv4 defines the schema of a server's public +// network information for an IPv4. +type ServerPublicNetIPv4 struct { + IP string `json:"ip"` + Blocked bool `json:"blocked"` + DNSPtr string `json:"dns_ptr"` +} + +// ServerPublicNetIPv6 defines the schema of a server's public +// network information for an IPv6. +type ServerPublicNetIPv6 struct { + IP string `json:"ip"` + Blocked bool `json:"blocked"` + DNSPtr []ServerPublicNetIPv6DNSPtr `json:"dns_ptr"` +} + +// ServerPublicNetIPv6DNSPtr defines the schema of a server's +// public network information for an IPv6 reverse DNS. +type ServerPublicNetIPv6DNSPtr struct { + IP string `json:"ip"` + DNSPtr string `json:"dns_ptr"` +} + +// ServerGetResponse defines the schema of the response when +// retrieving a single server. +type ServerGetResponse struct { + Server Server `json:"server"` +} + +// ServerListResponse defines the schema of the response when +// listing servers. +type ServerListResponse struct { + Servers []Server `json:"servers"` +} + +// ServerCreateRequest defines the schema for the request to +// create a server. +type ServerCreateRequest struct { + Name string `json:"name"` + ServerType interface{} `json:"server_type"` // int or string + Image interface{} `json:"image"` // int or string + SSHKeys []int `json:"ssh_keys,omitempty"` + Location string `json:"location,omitempty"` + Datacenter string `json:"datacenter,omitempty"` + UserData string `json:"user_data,omitempty"` + StartAfterCreate *bool `json:"start_after_create,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// ServerCreateResponse defines the schema of the response when +// creating a server. +type ServerCreateResponse struct { + Server Server `json:"server"` + Action Action `json:"action"` + RootPassword *string `json:"root_password"` +} + +// ServerUpdateRequest defines the schema of the request to update a server. +type ServerUpdateRequest struct { + Name string `json:"name,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// ServerUpdateResponse defines the schema of the response when updating a server. +type ServerUpdateResponse struct { + Server Server `json:"server"` +} + +// ServerActionPoweronRequest defines the schema for the request to +// create a poweron server action. +type ServerActionPoweronRequest struct{} + +// ServerActionPoweronResponse defines the schema of the response when +// creating a poweron server action. +type ServerActionPoweronResponse struct { + Action Action `json:"action"` +} + +// ServerActionPoweroffRequest defines the schema for the request to +// create a poweroff server action. +type ServerActionPoweroffRequest struct{} + +// ServerActionPoweroffResponse defines the schema of the response when +// creating a poweroff server action. +type ServerActionPoweroffResponse struct { + Action Action `json:"action"` +} + +// ServerActionRebootRequest defines the schema for the request to +// create a reboot server action. +type ServerActionRebootRequest struct{} + +// ServerActionRebootResponse defines the schema of the response when +// creating a reboot server action. +type ServerActionRebootResponse struct { + Action Action `json:"action"` +} + +// ServerActionResetRequest defines the schema for the request to +// create a reset server action. +type ServerActionResetRequest struct{} + +// ServerActionResetResponse defines the schema of the response when +// creating a reset server action. +type ServerActionResetResponse struct { + Action Action `json:"action"` +} + +// ServerActionShutdownRequest defines the schema for the request to +// create a shutdown server action. +type ServerActionShutdownRequest struct{} + +// ServerActionShutdownResponse defines the schema of the response when +// creating a shutdown server action. +type ServerActionShutdownResponse struct { + Action Action `json:"action"` +} + +// ServerActionResetPasswordRequest defines the schema for the request to +// create a reset_password server action. +type ServerActionResetPasswordRequest struct{} + +// ServerActionResetPasswordResponse defines the schema of the response when +// creating a reset_password server action. +type ServerActionResetPasswordResponse struct { + Action Action `json:"action"` + RootPassword string `json:"root_password"` +} + +// ServerActionCreateImageRequest defines the schema for the request to +// create a create_image server action. +type ServerActionCreateImageRequest struct { + Type *string `json:"type"` + Description *string `json:"description"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// ServerActionCreateImageResponse defines the schema of the response when +// creating a create_image server action. +type ServerActionCreateImageResponse struct { + Action Action `json:"action"` + Image Image `json:"image"` +} + +// ServerActionEnableRescueRequest defines the schema for the request to +// create a enable_rescue server action. +type ServerActionEnableRescueRequest struct { + Type *string `json:"type,omitempty"` + SSHKeys []int `json:"ssh_keys,omitempty"` +} + +// ServerActionEnableRescueResponse defines the schema of the response when +// creating a enable_rescue server action. +type ServerActionEnableRescueResponse struct { + Action Action `json:"action"` + RootPassword string `json:"root_password"` +} + +// ServerActionDisableRescueRequest defines the schema for the request to +// create a disable_rescue server action. +type ServerActionDisableRescueRequest struct{} + +// ServerActionDisableRescueResponse defines the schema of the response when +// creating a disable_rescue server action. +type ServerActionDisableRescueResponse struct { + Action Action `json:"action"` +} + +// ServerActionRebuildRequest defines the schema for the request to +// rebuild a server. +type ServerActionRebuildRequest struct { + Image interface{} `json:"image"` // int or string +} + +// ServerActionRebuildResponse defines the schema of the response when +// creating a rebuild server action. +type ServerActionRebuildResponse struct { + Action Action `json:"action"` +} + +// ServerActionAttachISORequest defines the schema for the request to +// attach an ISO to a server. +type ServerActionAttachISORequest struct { + ISO interface{} `json:"iso"` // int or string +} + +// ServerActionAttachISOResponse defines the schema of the response when +// creating a attach_iso server action. +type ServerActionAttachISOResponse struct { + Action Action `json:"action"` +} + +// ServerActionDetachISORequest defines the schema for the request to +// detach an ISO from a server. +type ServerActionDetachISORequest struct{} + +// ServerActionDetachISOResponse defines the schema of the response when +// creating a detach_iso server action. +type ServerActionDetachISOResponse struct { + Action Action `json:"action"` +} + +// ServerActionEnableBackupRequest defines the schema for the request to +// enable backup for a server. +type ServerActionEnableBackupRequest struct { + BackupWindow *string `json:"backup_window,omitempty"` +} + +// ServerActionEnableBackupResponse defines the schema of the response when +// creating a enable_backup server action. +type ServerActionEnableBackupResponse struct { + Action Action `json:"action"` +} + +// ServerActionDisableBackupRequest defines the schema for the request to +// disable backup for a server. +type ServerActionDisableBackupRequest struct{} + +// ServerActionDisableBackupResponse defines the schema of the response when +// creating a disable_backup server action. +type ServerActionDisableBackupResponse struct { + Action Action `json:"action"` +} + +// ServerActionChangeTypeRequest defines the schema for the request to +// change a server's type. +type ServerActionChangeTypeRequest struct { + ServerType interface{} `json:"server_type"` // int or string + UpgradeDisk bool `json:"upgrade_disk"` +} + +// ServerActionChangeTypeResponse defines the schema of the response when +// creating a change_type server action. +type ServerActionChangeTypeResponse struct { + Action Action `json:"action"` +} + +// ServerActionChangeDNSPtrRequest defines the schema for the request to +// change a server's reverse DNS pointer. +type ServerActionChangeDNSPtrRequest struct { + IP string `json:"ip"` + DNSPtr *string `json:"dns_ptr"` +} + +// ServerActionChangeDNSPtrResponse defines the schema of the response when +// creating a change_dns_ptr server action. +type ServerActionChangeDNSPtrResponse struct { + Action Action `json:"action"` +} + +// ServerActionChangeProtectionRequest defines the schema of the request to change the resource protection of a server. +type ServerActionChangeProtectionRequest struct { + Rebuild *bool `json:"rebuild,omitempty"` + Delete *bool `json:"delete,omitempty"` +} + +// ServerActionChangeProtectionResponse defines the schema of the response when changing the resource protection of a server. +type ServerActionChangeProtectionResponse struct { + Action Action `json:"action"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/server_type.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/server_type.go new file mode 100644 index 000000000..5d4f10b03 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/server_type.go @@ -0,0 +1,26 @@ +package schema + +// ServerType defines the schema of a server type. +type ServerType struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Cores int `json:"cores"` + Memory float32 `json:"memory"` + Disk int `json:"disk"` + StorageType string `json:"storage_type"` + CPUType string `json:"cpu_type"` + Prices []PricingServerTypePrice `json:"prices"` +} + +// ServerTypeListResponse defines the schema of the response when +// listing server types. +type ServerTypeListResponse struct { + ServerTypes []ServerType `json:"server_types"` +} + +// ServerTypeGetResponse defines the schema of the response when +// retrieving a single server type. +type ServerTypeGetResponse struct { + ServerType ServerType `json:"server_type"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/ssh_key.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/ssh_key.go new file mode 100644 index 000000000..2f80bac41 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/ssh_key.go @@ -0,0 +1,47 @@ +package schema + +// SSHKey defines the schema of a SSH key. +type SSHKey struct { + ID int `json:"id"` + Name string `json:"name"` + Fingerprint string `json:"fingerprint"` + PublicKey string `json:"public_key"` + Labels map[string]string `json:"labels"` +} + +// SSHKeyCreateRequest defines the schema of the request +// to create a SSH key. +type SSHKeyCreateRequest struct { + Name string `json:"name"` + PublicKey string `json:"public_key"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// SSHKeyCreateResponse defines the schema of the response +// when creating a SSH key. +type SSHKeyCreateResponse struct { + SSHKey SSHKey `json:"ssh_key"` +} + +// SSHKeyListResponse defines the schema of the response +// when listing SSH keys. +type SSHKeyListResponse struct { + SSHKeys []SSHKey `json:"ssh_keys"` +} + +// SSHKeyGetResponse defines the schema of the response +// when retrieving a single SSH key. +type SSHKeyGetResponse struct { + SSHKey SSHKey `json:"ssh_key"` +} + +// SSHKeyUpdateRequest defines the schema of the request to update a SSH key. +type SSHKeyUpdateRequest struct { + Name string `json:"name,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// SSHKeyUpdateResponse defines the schema of the response when updating a SSH key. +type SSHKeyUpdateResponse struct { + SSHKey SSHKey `json:"ssh_key"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/volume.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/volume.go new file mode 100644 index 000000000..aa6c699e9 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/volume.go @@ -0,0 +1,105 @@ +package schema + +import "time" + +// Volume defines the schema of a volume. +type Volume struct { + ID int `json:"id"` + Name string `json:"name"` + Server *int `json:"server"` + Location Location `json:"location"` + Size int `json:"size"` + Protection VolumeProtection `json:"protection"` + Labels map[string]string `json:"labels"` + LinuxDevice string `json:"linux_device"` + Created time.Time `json:"created"` +} + +// VolumeCreateRequest defines the schema of the request +// to create a volume. +type VolumeCreateRequest struct { + Name string `json:"name"` + Size int `json:"size"` + Server *int `json:"server,omitempty"` + Location interface{} `json:"location,omitempty"` // int, string, or nil + Labels *map[string]string `json:"labels,omitempty"` +} + +// VolumeCreateResponse defines the schema of the response +// when creating a volume. +type VolumeCreateResponse struct { + Volume Volume `json:"volume"` + Action *Action `json:"action"` +} + +// VolumeListResponse defines the schema of the response +// when listing volumes. +type VolumeListResponse struct { + Volumes []Volume `json:"volumes"` +} + +// VolumeGetResponse defines the schema of the response +// when retrieving a single volume. +type VolumeGetResponse struct { + Volume Volume `json:"volume"` +} + +// VolumeUpdateRequest defines the schema of the request to update a volume. +type VolumeUpdateRequest struct { + Name string `json:"name,omitempty"` + Labels *map[string]string `json:"labels,omitempty"` +} + +// VolumeUpdateResponse defines the schema of the response when updating a volume. +type VolumeUpdateResponse struct { + Volume Volume `json:"volume"` +} + +// VolumeProtection defines the schema of a volume's resource protection. +type VolumeProtection struct { + Delete bool `json:"delete"` +} + +// VolumeActionChangeProtectionRequest defines the schema of the request to +// change the resource protection of a volume. +type VolumeActionChangeProtectionRequest struct { + Delete *bool `json:"delete,omitempty"` +} + +// VolumeActionChangeProtectionResponse defines the schema of the response when +// changing the resource protection of a volume. +type VolumeActionChangeProtectionResponse struct { + Action Action `json:"action"` +} + +// VolumeActionAttachVolumeRequest defines the schema of the request to +// attach a volume to a server. +type VolumeActionAttachVolumeRequest struct { + Server int `json:"server"` +} + +// VolumeActionAttachVolumeResponse defines the schema of the response when +// attaching a volume to a server. +type VolumeActionAttachVolumeResponse struct { + Action Action `json:"action"` +} + +// VolumeActionDetachVolumeRequest defines the schema of the request to +// create an detach volume action. +type VolumeActionDetachVolumeRequest struct{} + +// VolumeActionDetachVolumeResponse defines the schema of the response when +// creating an detach volume action. +type VolumeActionDetachVolumeResponse struct { + Action Action `json:"action"` +} + +// VolumeActionResizeVolumeRequest defines the schema of the request to resize a volume. +type VolumeActionResizeVolumeRequest struct { + Size int `json:"size"` +} + +// VolumeActionResizeVolumeResponse defines the schema of the response when resizing a volume. +type VolumeActionResizeVolumeResponse struct { + Action Action `json:"action"` +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/server.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/server.go new file mode 100644 index 000000000..b85861fc3 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/server.go @@ -0,0 +1,778 @@ +package hcloud + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net" + "net/url" + "strconv" + "time" + + "github.com/hetznercloud/hcloud-go/hcloud/schema" +) + +// Server represents a server in the Hetzner Cloud. +type Server struct { + ID int + Name string + Status ServerStatus + Created time.Time + PublicNet ServerPublicNet + ServerType *ServerType + Datacenter *Datacenter + IncludedTraffic uint64 + OutgoingTraffic uint64 + IngoingTraffic uint64 + BackupWindow string + RescueEnabled bool + Locked bool + ISO *ISO + Image *Image + Protection ServerProtection + Labels map[string]string + Volumes []*Volume +} + +// ServerProtection represents the protection level of a server. +type ServerProtection struct { + Delete, Rebuild bool +} + +// ServerStatus specifies a server's status. +type ServerStatus string + +const ( + // ServerStatusInitializing is the status when a server is initializing. + ServerStatusInitializing ServerStatus = "initializing" + + // ServerStatusOff is the status when a server is off. + ServerStatusOff ServerStatus = "off" + + // ServerStatusRunning is the status when a server is running. + ServerStatusRunning ServerStatus = "running" +) + +// ServerPublicNet represents a server's public network. +type ServerPublicNet struct { + IPv4 ServerPublicNetIPv4 + IPv6 ServerPublicNetIPv6 + FloatingIPs []*FloatingIP +} + +// ServerPublicNetIPv4 represents a server's public IPv4 address. +type ServerPublicNetIPv4 struct { + IP net.IP + Blocked bool + DNSPtr string +} + +// ServerPublicNetIPv6 represents a server's public IPv6 network and address. +type ServerPublicNetIPv6 struct { + IP net.IP + Network *net.IPNet + Blocked bool + DNSPtr map[string]string +} + +// DNSPtrForIP returns the reverse dns pointer of the ip address. +func (s *ServerPublicNetIPv6) DNSPtrForIP(ip net.IP) string { + return s.DNSPtr[ip.String()] +} + +// ServerRescueType represents rescue types. +type ServerRescueType string + +// List of rescue types. +const ( + ServerRescueTypeLinux32 ServerRescueType = "linux32" + ServerRescueTypeLinux64 ServerRescueType = "linux64" + ServerRescueTypeFreeBSD64 ServerRescueType = "freebsd64" +) + +// ServerClient is a client for the servers API. +type ServerClient struct { + client *Client +} + +// GetByID retrieves a server by its ID. +func (c *ServerClient) GetByID(ctx context.Context, id int) (*Server, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/servers/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.ServerGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return ServerFromSchema(body.Server), resp, nil +} + +// GetByName retreives a server by its name. +func (c *ServerClient) GetByName(ctx context.Context, name string) (*Server, *Response, error) { + path := "/servers?name=" + url.QueryEscape(name) + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.ServerListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + + if len(body.Servers) == 0 { + return nil, resp, nil + } + return ServerFromSchema(body.Servers[0]), resp, nil +} + +// Get retrieves a server by its ID if the input can be parsed as an integer, otherwise it retrieves a server by its name. +func (c *ServerClient) Get(ctx context.Context, idOrName string) (*Server, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// ServerListOpts specifies options for listing servers. +type ServerListOpts struct { + ListOpts +} + +// List returns a list of servers for a specific page. +func (c *ServerClient) List(ctx context.Context, opts ServerListOpts) ([]*Server, *Response, error) { + path := "/servers?" + valuesForListOpts(opts.ListOpts).Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.ServerListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + servers := make([]*Server, 0, len(body.Servers)) + for _, s := range body.Servers { + servers = append(servers, ServerFromSchema(s)) + } + return servers, resp, nil +} + +// All returns all servers. +func (c *ServerClient) All(ctx context.Context) ([]*Server, error) { + return c.AllWithOpts(ctx, ServerListOpts{ListOpts{PerPage: 50}}) +} + +// AllWithOpts returns all servers for the given options. +func (c *ServerClient) AllWithOpts(ctx context.Context, opts ServerListOpts) ([]*Server, error) { + allServers := []*Server{} + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + servers, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allServers = append(allServers, servers...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allServers, nil +} + +// ServerCreateOpts specifies options for creating a new server. +type ServerCreateOpts struct { + Name string + ServerType *ServerType + Image *Image + SSHKeys []*SSHKey + Location *Location + Datacenter *Datacenter + UserData string + StartAfterCreate *bool + Labels map[string]string +} + +// Validate checks if options are valid. +func (o ServerCreateOpts) Validate() error { + if o.Name == "" { + return errors.New("missing name") + } + if o.ServerType == nil || (o.ServerType.ID == 0 && o.ServerType.Name == "") { + return errors.New("missing server type") + } + if o.Image == nil || (o.Image.ID == 0 && o.Image.Name == "") { + return errors.New("missing image") + } + if o.Location != nil && o.Datacenter != nil { + return errors.New("location and datacenter are mutually exclusive") + } + return nil +} + +// ServerCreateResult is the result of a create server call. +type ServerCreateResult struct { + Server *Server + Action *Action + RootPassword string +} + +// Create creates a new server. +func (c *ServerClient) Create(ctx context.Context, opts ServerCreateOpts) (ServerCreateResult, *Response, error) { + if err := opts.Validate(); err != nil { + return ServerCreateResult{}, nil, err + } + + var reqBody schema.ServerCreateRequest + reqBody.UserData = opts.UserData + reqBody.Name = opts.Name + reqBody.StartAfterCreate = opts.StartAfterCreate + if opts.ServerType.ID != 0 { + reqBody.ServerType = opts.ServerType.ID + } else if opts.ServerType.Name != "" { + reqBody.ServerType = opts.ServerType.Name + } + if opts.Image.ID != 0 { + reqBody.Image = opts.Image.ID + } else if opts.Image.Name != "" { + reqBody.Image = opts.Image.Name + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + for _, sshKey := range opts.SSHKeys { + reqBody.SSHKeys = append(reqBody.SSHKeys, sshKey.ID) + } + if opts.Location != nil { + if opts.Location.ID != 0 { + reqBody.Location = strconv.Itoa(opts.Location.ID) + } else { + reqBody.Location = opts.Location.Name + } + } + if opts.Datacenter != nil { + if opts.Datacenter.ID != 0 { + reqBody.Datacenter = strconv.Itoa(opts.Datacenter.ID) + } else { + reqBody.Datacenter = opts.Datacenter.Name + } + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return ServerCreateResult{}, nil, err + } + + req, err := c.client.NewRequest(ctx, "POST", "/servers", bytes.NewReader(reqBodyData)) + if err != nil { + return ServerCreateResult{}, nil, err + } + + var respBody schema.ServerCreateResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return ServerCreateResult{}, resp, err + } + result := ServerCreateResult{ + Server: ServerFromSchema(respBody.Server), + Action: ActionFromSchema(respBody.Action), + } + if respBody.RootPassword != nil { + result.RootPassword = *respBody.RootPassword + } + return result, resp, nil +} + +// Delete deletes a server. +func (c *ServerClient) Delete(ctx context.Context, server *Server) (*Response, error) { + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/servers/%d", server.ID), nil) + if err != nil { + return nil, err + } + return c.client.Do(req, nil) +} + +// ServerUpdateOpts specifies options for updating a server. +type ServerUpdateOpts struct { + Name string + Labels map[string]string +} + +// Update updates a server. +func (c *ServerClient) Update(ctx context.Context, server *Server, opts ServerUpdateOpts) (*Server, *Response, error) { + reqBody := schema.ServerUpdateRequest{ + Name: opts.Name, + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/servers/%d", server.ID) + req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerUpdateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ServerFromSchema(respBody.Server), resp, nil +} + +// Poweron starts a server. +func (c *ServerClient) Poweron(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/poweron", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionPoweronResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// Reboot reboots a server. +func (c *ServerClient) Reboot(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/reboot", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionRebootResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// Reset resets a server. +func (c *ServerClient) Reset(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/reset", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionResetResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// Shutdown shuts down a server. +func (c *ServerClient) Shutdown(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/shutdown", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionShutdownResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// Poweroff stops a server. +func (c *ServerClient) Poweroff(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/poweroff", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionPoweroffResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// ServerResetPasswordResult is the result of resetting a server's password. +type ServerResetPasswordResult struct { + Action *Action + RootPassword string +} + +// ResetPassword resets a server's password. +func (c *ServerClient) ResetPassword(ctx context.Context, server *Server) (ServerResetPasswordResult, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/reset_password", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return ServerResetPasswordResult{}, nil, err + } + + respBody := schema.ServerActionResetPasswordResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return ServerResetPasswordResult{}, resp, err + } + return ServerResetPasswordResult{ + Action: ActionFromSchema(respBody.Action), + RootPassword: respBody.RootPassword, + }, resp, nil +} + +// ServerCreateImageOpts specifies options for creating an image from a server. +type ServerCreateImageOpts struct { + Type ImageType + Description *string + Labels map[string]string +} + +// Validate checks if options are valid. +func (o ServerCreateImageOpts) Validate() error { + switch o.Type { + case ImageTypeSnapshot, ImageTypeBackup: + break + case "": + break + default: + return errors.New("invalid type") + } + + return nil +} + +// ServerCreateImageResult is the result of creating an image from a server. +type ServerCreateImageResult struct { + Action *Action + Image *Image +} + +// CreateImage creates an image from a server. +func (c *ServerClient) CreateImage(ctx context.Context, server *Server, opts *ServerCreateImageOpts) (ServerCreateImageResult, *Response, error) { + var reqBody schema.ServerActionCreateImageRequest + if opts != nil { + if err := opts.Validate(); err != nil { + return ServerCreateImageResult{}, nil, fmt.Errorf("invalid options: %s", err) + } + if opts.Description != nil { + reqBody.Description = opts.Description + } + if opts.Type != "" { + reqBody.Type = String(string(opts.Type)) + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return ServerCreateImageResult{}, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/create_image", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return ServerCreateImageResult{}, nil, err + } + + respBody := schema.ServerActionCreateImageResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return ServerCreateImageResult{}, resp, err + } + return ServerCreateImageResult{ + Action: ActionFromSchema(respBody.Action), + Image: ImageFromSchema(respBody.Image), + }, resp, nil +} + +// ServerEnableRescueOpts specifies options for enabling rescue mode for a server. +type ServerEnableRescueOpts struct { + Type ServerRescueType + SSHKeys []*SSHKey +} + +// ServerEnableRescueResult is the result of enabling rescue mode for a server. +type ServerEnableRescueResult struct { + Action *Action + RootPassword string +} + +// EnableRescue enables rescue mode for a server. +func (c *ServerClient) EnableRescue(ctx context.Context, server *Server, opts ServerEnableRescueOpts) (ServerEnableRescueResult, *Response, error) { + reqBody := schema.ServerActionEnableRescueRequest{ + Type: String(string(opts.Type)), + } + for _, sshKey := range opts.SSHKeys { + reqBody.SSHKeys = append(reqBody.SSHKeys, sshKey.ID) + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return ServerEnableRescueResult{}, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/enable_rescue", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return ServerEnableRescueResult{}, nil, err + } + + respBody := schema.ServerActionEnableRescueResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return ServerEnableRescueResult{}, resp, err + } + result := ServerEnableRescueResult{ + Action: ActionFromSchema(respBody.Action), + RootPassword: respBody.RootPassword, + } + return result, resp, nil +} + +// DisableRescue disables rescue mode for a server. +func (c *ServerClient) DisableRescue(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/disable_rescue", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionDisableRescueResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// ServerRebuildOpts specifies options for rebuilding a server. +type ServerRebuildOpts struct { + Image *Image +} + +// Rebuild rebuilds a server. +func (c *ServerClient) Rebuild(ctx context.Context, server *Server, opts ServerRebuildOpts) (*Action, *Response, error) { + reqBody := schema.ServerActionRebuildRequest{} + if opts.Image.ID != 0 { + reqBody.Image = opts.Image.ID + } else { + reqBody.Image = opts.Image.Name + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/rebuild", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionRebuildResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// AttachISO attaches an ISO to a server. +func (c *ServerClient) AttachISO(ctx context.Context, server *Server, iso *ISO) (*Action, *Response, error) { + reqBody := schema.ServerActionAttachISORequest{} + if iso.ID != 0 { + reqBody.ISO = iso.ID + } else { + reqBody.ISO = iso.Name + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/attach_iso", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionAttachISOResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// DetachISO detaches the currently attached ISO from a server. +func (c *ServerClient) DetachISO(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/detach_iso", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionDetachISOResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// EnableBackup enables backup for a server. Pass in an empty backup window to let the +// API pick a window for you. See the API documentation at docs.hetzner.cloud for a list +// of valid backup windows. +func (c *ServerClient) EnableBackup(ctx context.Context, server *Server, window string) (*Action, *Response, error) { + reqBody := schema.ServerActionEnableBackupRequest{} + if window != "" { + reqBody.BackupWindow = String(window) + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/enable_backup", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionEnableBackupResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// DisableBackup disables backup for a server. +func (c *ServerClient) DisableBackup(ctx context.Context, server *Server) (*Action, *Response, error) { + path := fmt.Sprintf("/servers/%d/actions/disable_backup", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, nil) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionDisableBackupResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// ServerChangeTypeOpts specifies options for changing a server's type. +type ServerChangeTypeOpts struct { + ServerType *ServerType // new server type + UpgradeDisk bool // whether disk should be upgraded +} + +// ChangeType changes a server's type. +func (c *ServerClient) ChangeType(ctx context.Context, server *Server, opts ServerChangeTypeOpts) (*Action, *Response, error) { + reqBody := schema.ServerActionChangeTypeRequest{ + UpgradeDisk: opts.UpgradeDisk, + } + if opts.ServerType.ID != 0 { + reqBody.ServerType = opts.ServerType.ID + } else { + reqBody.ServerType = opts.ServerType.Name + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/change_type", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionChangeTypeResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// ChangeDNSPtr changes or resets the reverse DNS pointer for a server IP address. +// Pass a nil ptr to reset the reverse DNS pointer to its default value. +func (c *ServerClient) ChangeDNSPtr(ctx context.Context, server *Server, ip string, ptr *string) (*Action, *Response, error) { + reqBody := schema.ServerActionChangeDNSPtrRequest{ + IP: ip, + DNSPtr: ptr, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/change_dns_ptr", server.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionChangeDNSPtrResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// ServerChangeProtectionOpts specifies options for changing the resource protection level of a server. +type ServerChangeProtectionOpts struct { + Rebuild *bool + Delete *bool +} + +// ChangeProtection changes the resource protection level of a server. +func (c *ServerClient) ChangeProtection(ctx context.Context, image *Server, opts ServerChangeProtectionOpts) (*Action, *Response, error) { + reqBody := schema.ServerActionChangeProtectionRequest{ + Rebuild: opts.Rebuild, + Delete: opts.Delete, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/servers/%d/actions/change_protection", image.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.ServerActionChangeProtectionResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/server_type.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/server_type.go new file mode 100644 index 000000000..8f4166c17 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/server_type.go @@ -0,0 +1,144 @@ +package hcloud + +import ( + "context" + "fmt" + "net/url" + "strconv" + + "github.com/hetznercloud/hcloud-go/hcloud/schema" +) + +// ServerType represents a server type in the Hetzner Cloud. +type ServerType struct { + ID int + Name string + Description string + Cores int + Memory float32 + Disk int + StorageType StorageType + CPUType CPUType + Pricings []ServerTypeLocationPricing +} + +// StorageType specifies the type of storage. +type StorageType string + +const ( + // StorageTypeLocal is the type for local storage. + StorageTypeLocal StorageType = "local" + + // StorageTypeCeph is the type for remote storage. + StorageTypeCeph StorageType = "ceph" +) + +// CPUType specifies the type of the CPU. +type CPUType string + +const ( + // CPUTypeShared is the type for shared CPU. + CPUTypeShared CPUType = "shared" + + //CPUTypeDedicated is the type for dedicated CPU. + CPUTypeDedicated CPUType = "dedicated" +) + +// ServerTypeClient is a client for the server types API. +type ServerTypeClient struct { + client *Client +} + +// GetByID retrieves a server type by its ID. +func (c *ServerTypeClient) GetByID(ctx context.Context, id int) (*ServerType, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/server_types/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.ServerTypeGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return ServerTypeFromSchema(body.ServerType), resp, nil +} + +// GetByName retrieves a server type by its name. +func (c *ServerTypeClient) GetByName(ctx context.Context, name string) (*ServerType, *Response, error) { + path := "/server_types?name=" + url.QueryEscape(name) + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.ServerTypeListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + + if len(body.ServerTypes) == 0 { + return nil, resp, nil + } + return ServerTypeFromSchema(body.ServerTypes[0]), resp, nil +} + +// Get retrieves a server type by its ID if the input can be parsed as an integer, otherwise it retrieves a server type by its name. +func (c *ServerTypeClient) Get(ctx context.Context, idOrName string) (*ServerType, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// ServerTypeListOpts specifies options for listing server types. +type ServerTypeListOpts struct { + ListOpts +} + +// List returns a list of server types for a specific page. +func (c *ServerTypeClient) List(ctx context.Context, opts ServerTypeListOpts) ([]*ServerType, *Response, error) { + path := "/server_types?" + valuesForListOpts(opts.ListOpts).Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.ServerTypeListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + serverTypes := make([]*ServerType, 0, len(body.ServerTypes)) + for _, s := range body.ServerTypes { + serverTypes = append(serverTypes, ServerTypeFromSchema(s)) + } + return serverTypes, resp, nil +} + +// All returns all server types. +func (c *ServerTypeClient) All(ctx context.Context) ([]*ServerType, error) { + allServerTypes := []*ServerType{} + + opts := ServerTypeListOpts{} + opts.PerPage = 50 + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + serverTypes, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allServerTypes = append(allServerTypes, serverTypes...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allServerTypes, nil +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/ssh_key.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/ssh_key.go new file mode 100644 index 000000000..2ab55bff6 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/ssh_key.go @@ -0,0 +1,233 @@ +package hcloud + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/url" + "strconv" + + "github.com/hetznercloud/hcloud-go/hcloud/schema" +) + +// SSHKey represents a SSH key in the Hetzner Cloud. +type SSHKey struct { + ID int + Name string + Fingerprint string + PublicKey string + Labels map[string]string +} + +// SSHKeyClient is a client for the SSH keys API. +type SSHKeyClient struct { + client *Client +} + +// GetByID retrieves a SSH key by its ID. +func (c *SSHKeyClient) GetByID(ctx context.Context, id int) (*SSHKey, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/ssh_keys/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.SSHKeyGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return SSHKeyFromSchema(body.SSHKey), resp, nil +} + +// GetByName retrieves a SSH key by its name. +func (c *SSHKeyClient) GetByName(ctx context.Context, name string) (*SSHKey, *Response, error) { + path := "/ssh_keys?name=" + url.QueryEscape(name) + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.SSHKeyListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + + if len(body.SSHKeys) == 0 { + return nil, resp, nil + } + return SSHKeyFromSchema(body.SSHKeys[0]), resp, nil +} + +// GetByFingerprint retreives a SSH key by its fingerprint. +func (c *SSHKeyClient) GetByFingerprint(ctx context.Context, fingerprint string) (*SSHKey, *Response, error) { + path := "/ssh_keys?fingerprint=" + url.QueryEscape(fingerprint) + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.SSHKeyListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + + if len(body.SSHKeys) == 0 { + return nil, resp, nil + } + return SSHKeyFromSchema(body.SSHKeys[0]), resp, nil +} + +// Get retrieves a SSH key by its ID if the input can be parsed as an integer, otherwise it retrieves a SSH key by its name. +func (c *SSHKeyClient) Get(ctx context.Context, idOrName string) (*SSHKey, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// SSHKeyListOpts specifies options for listing SSH keys. +type SSHKeyListOpts struct { + ListOpts +} + +// List returns a list of SSH keys for a specific page. +func (c *SSHKeyClient) List(ctx context.Context, opts SSHKeyListOpts) ([]*SSHKey, *Response, error) { + path := "/ssh_keys?" + valuesForListOpts(opts.ListOpts).Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.SSHKeyListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + sshKeys := make([]*SSHKey, 0, len(body.SSHKeys)) + for _, s := range body.SSHKeys { + sshKeys = append(sshKeys, SSHKeyFromSchema(s)) + } + return sshKeys, resp, nil +} + +// All returns all SSH keys. +func (c *SSHKeyClient) All(ctx context.Context) ([]*SSHKey, error) { + return c.AllWithOpts(ctx, SSHKeyListOpts{ListOpts{PerPage: 50}}) +} + +// AllWithOpts returns all SSH keys with the given options. +func (c *SSHKeyClient) AllWithOpts(ctx context.Context, opts SSHKeyListOpts) ([]*SSHKey, error) { + allSSHKeys := []*SSHKey{} + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + sshKeys, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allSSHKeys = append(allSSHKeys, sshKeys...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allSSHKeys, nil +} + +// SSHKeyCreateOpts specifies parameters for creating a SSH key. +type SSHKeyCreateOpts struct { + Name string + PublicKey string + Labels map[string]string +} + +// Validate checks if options are valid. +func (o SSHKeyCreateOpts) Validate() error { + if o.Name == "" { + return errors.New("missing name") + } + if o.PublicKey == "" { + return errors.New("missing public key") + } + return nil +} + +// Create creates a new SSH key with the given options. +func (c *SSHKeyClient) Create(ctx context.Context, opts SSHKeyCreateOpts) (*SSHKey, *Response, error) { + if err := opts.Validate(); err != nil { + return nil, nil, err + } + reqBody := schema.SSHKeyCreateRequest{ + Name: opts.Name, + PublicKey: opts.PublicKey, + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + req, err := c.client.NewRequest(ctx, "POST", "/ssh_keys", bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + var respBody schema.SSHKeyCreateResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return SSHKeyFromSchema(respBody.SSHKey), resp, nil +} + +// Delete deletes a SSH key. +func (c *SSHKeyClient) Delete(ctx context.Context, sshKey *SSHKey) (*Response, error) { + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/ssh_keys/%d", sshKey.ID), nil) + if err != nil { + return nil, err + } + return c.client.Do(req, nil) +} + +// SSHKeyUpdateOpts specifies options for updating a SSH key. +type SSHKeyUpdateOpts struct { + Name string + Labels map[string]string +} + +// Update updates a SSH key. +func (c *SSHKeyClient) Update(ctx context.Context, sshKey *SSHKey, opts SSHKeyUpdateOpts) (*SSHKey, *Response, error) { + reqBody := schema.SSHKeyUpdateRequest{ + Name: opts.Name, + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/ssh_keys/%d", sshKey.ID) + req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.SSHKeyUpdateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return SSHKeyFromSchema(respBody.SSHKey), resp, nil +} diff --git a/vendor/github.com/hetznercloud/hcloud-go/hcloud/volume.go b/vendor/github.com/hetznercloud/hcloud-go/hcloud/volume.go new file mode 100644 index 000000000..4e5f4d0a9 --- /dev/null +++ b/vendor/github.com/hetznercloud/hcloud-go/hcloud/volume.go @@ -0,0 +1,356 @@ +package hcloud + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/url" + "strconv" + "time" + + "github.com/hetznercloud/hcloud-go/hcloud/schema" +) + +// Volume represents a volume in the Hetzner Cloud. +type Volume struct { + ID int + Name string + Server *Server + Location *Location + Size int + Protection VolumeProtection + Labels map[string]string + LinuxDevice string + Created time.Time +} + +// VolumeProtection represents the protection level of a volume. +type VolumeProtection struct { + Delete bool +} + +// VolumeClient is a client for the volume API. +type VolumeClient struct { + client *Client +} + +// GetByID retrieves a volume by its ID. +func (c *VolumeClient) GetByID(ctx context.Context, id int) (*Volume, *Response, error) { + req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/volumes/%d", id), nil) + if err != nil { + return nil, nil, err + } + + var body schema.VolumeGetResponse + resp, err := c.client.Do(req, &body) + if err != nil { + if IsError(err, ErrorCodeNotFound) { + return nil, resp, nil + } + return nil, nil, err + } + return VolumeFromSchema(body.Volume), resp, nil +} + +// GetByName retrieves a volume by its name. +func (c *VolumeClient) GetByName(ctx context.Context, name string) (*Volume, *Response, error) { + path := "/volumes?name=" + url.QueryEscape(name) + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.VolumeListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + + if len(body.Volumes) == 0 { + return nil, resp, nil + } + return VolumeFromSchema(body.Volumes[0]), resp, nil +} + +// Get retrieves a volume by its ID if the input can be parsed as an integer, +// otherwise it retrieves a volume by its name. +func (c *VolumeClient) Get(ctx context.Context, idOrName string) (*Volume, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, int(id)) + } + return c.GetByName(ctx, idOrName) +} + +// VolumeListOpts specifies options for listing volumes. +type VolumeListOpts struct { + ListOpts +} + +// List returns a list of volumes for a specific page. +func (c *VolumeClient) List(ctx context.Context, opts VolumeListOpts) ([]*Volume, *Response, error) { + path := "/volumes?" + valuesForListOpts(opts.ListOpts).Encode() + req, err := c.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + var body schema.VolumeListResponse + resp, err := c.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + volumes := make([]*Volume, 0, len(body.Volumes)) + for _, s := range body.Volumes { + volumes = append(volumes, VolumeFromSchema(s)) + } + return volumes, resp, nil +} + +// All returns all volumes. +func (c *VolumeClient) All(ctx context.Context) ([]*Volume, error) { + return c.AllWithOpts(ctx, VolumeListOpts{ListOpts{PerPage: 50}}) +} + +// AllWithOpts returns all volumes with the given options. +func (c *VolumeClient) AllWithOpts(ctx context.Context, opts VolumeListOpts) ([]*Volume, error) { + allVolumes := []*Volume{} + + _, err := c.client.all(func(page int) (*Response, error) { + opts.Page = page + volumes, resp, err := c.List(ctx, opts) + if err != nil { + return resp, err + } + allVolumes = append(allVolumes, volumes...) + return resp, nil + }) + if err != nil { + return nil, err + } + + return allVolumes, nil +} + +// VolumeCreateOpts specifies parameters for creating a volume. +type VolumeCreateOpts struct { + Name string + Size int + Server *Server + Location *Location + Labels map[string]string +} + +// Validate checks if options are valid. +func (o VolumeCreateOpts) Validate() error { + if o.Name == "" { + return errors.New("missing name") + } + if o.Size <= 0 { + return errors.New("size must be greater than 0") + } + if o.Server == nil && o.Location == nil { + return errors.New("one of server or location must be provided") + } + if o.Server != nil && o.Location != nil { + return errors.New("only one of server or location must be provided") + } + return nil +} + +// VolumeCreateResult is the result of creating a volume. +type VolumeCreateResult struct { + Volume *Volume + Action *Action +} + +// Create creates a new volume with the given options. +func (c *VolumeClient) Create(ctx context.Context, opts VolumeCreateOpts) (VolumeCreateResult, *Response, error) { + if err := opts.Validate(); err != nil { + return VolumeCreateResult{}, nil, err + } + reqBody := schema.VolumeCreateRequest{ + Name: opts.Name, + Size: opts.Size, + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + if opts.Server != nil { + reqBody.Server = Int(opts.Server.ID) + } + if opts.Location != nil { + if opts.Location.ID != 0 { + reqBody.Location = opts.Location.ID + } else { + reqBody.Location = opts.Location.Name + } + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return VolumeCreateResult{}, nil, err + } + + req, err := c.client.NewRequest(ctx, "POST", "/volumes", bytes.NewReader(reqBodyData)) + if err != nil { + return VolumeCreateResult{}, nil, err + } + + var respBody schema.VolumeCreateResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return VolumeCreateResult{}, resp, err + } + + var action *Action + if respBody.Action != nil { + action = ActionFromSchema(*respBody.Action) + } + + return VolumeCreateResult{ + Volume: VolumeFromSchema(respBody.Volume), + Action: action, + }, resp, nil +} + +// Delete deletes a volume. +func (c *VolumeClient) Delete(ctx context.Context, volume *Volume) (*Response, error) { + req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/volumes/%d", volume.ID), nil) + if err != nil { + return nil, err + } + return c.client.Do(req, nil) +} + +// VolumeUpdateOpts specifies options for updating a volume. +type VolumeUpdateOpts struct { + Name string + Labels map[string]string +} + +// Update updates a volume. +func (c *VolumeClient) Update(ctx context.Context, volume *Volume, opts VolumeUpdateOpts) (*Volume, *Response, error) { + reqBody := schema.VolumeUpdateRequest{ + Name: opts.Name, + } + if opts.Labels != nil { + reqBody.Labels = &opts.Labels + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/volumes/%d", volume.ID) + req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.VolumeUpdateResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return VolumeFromSchema(respBody.Volume), resp, nil +} + +// Attach attaches a volume to a server. +func (c *VolumeClient) Attach(ctx context.Context, volume *Volume, server *Server) (*Action, *Response, error) { + reqBody := schema.VolumeActionAttachVolumeRequest{ + Server: server.ID, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/volumes/%d/actions/attach", volume.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + var respBody schema.VolumeActionAttachVolumeResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// Detach detaches a volume from a server. +func (c *VolumeClient) Detach(ctx context.Context, volume *Volume) (*Action, *Response, error) { + var reqBody schema.VolumeActionDetachVolumeRequest + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/volumes/%d/actions/detach", volume.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + var respBody schema.VolumeActionDetachVolumeResponse + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, nil +} + +// VolumeChangeProtectionOpts specifies options for changing the resource protection level of a volume. +type VolumeChangeProtectionOpts struct { + Delete *bool +} + +// ChangeProtection changes the resource protection level of a volume. +func (c *VolumeClient) ChangeProtection(ctx context.Context, volume *Volume, opts VolumeChangeProtectionOpts) (*Action, *Response, error) { + reqBody := schema.VolumeActionChangeProtectionRequest{ + Delete: opts.Delete, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/volumes/%d/actions/change_protection", volume.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.VolumeActionChangeProtectionResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} + +// Resize changes the size of a volume. +func (c *VolumeClient) Resize(ctx context.Context, volume *Volume, size int) (*Action, *Response, error) { + reqBody := schema.VolumeActionResizeVolumeRequest{ + Size: size, + } + reqBodyData, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("/volumes/%d/actions/resize", volume.ID) + req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData)) + if err != nil { + return nil, nil, err + } + + respBody := schema.VolumeActionResizeVolumeResponse{} + resp, err := c.client.Do(req, &respBody) + if err != nil { + return nil, resp, err + } + return ActionFromSchema(respBody.Action), resp, err +} diff --git a/vendor/vendor.json b/vendor/vendor.json index f1304234b..9a2135b5f 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1171,6 +1171,12 @@ "path": "github.com/hashicorp/yamux", "revision": "df949784da9ed028ee76df44652e42d37a09d7e4" }, + { + "checksumSHA1": "lMvR+qhQWll2pXPY3GmJbKqgkUc=", + "path": "github.com/hetznercloud/hcloud-go/hcloud", + "revision": "eaf050e4f37028d2ca5f99a2fb38ead2c9b293d3", + "revisionTime": "2018-10-16T09:48:19Z" + }, { "checksumSHA1": "3/Bhy+ua/DCv2ElMD5GzOYSGN6g=", "comment": "0.2.2-2-gc01cf91", diff --git a/website/source/docs/builders/hetzner-cloud.html.md b/website/source/docs/builders/hetzner-cloud.html.md new file mode 100644 index 000000000..e09f9e8e5 --- /dev/null +++ b/website/source/docs/builders/hetzner-cloud.html.md @@ -0,0 +1,82 @@ +--- +description: | + The Hetzner Cloud Packer builder is able to create new images for use with the + Hetzner Cloud. The builder takes a source image, runs any provisioning + necessary on the image after launching it, then snapshots it into a reusable + image. This reusable image can then be used as the foundation of new servers + that are launched within the Hetzner Cloud. +layout: docs +page_title: 'Hetzner Cloud - Builders' +sidebar_current: 'docs-builders-hetzner-cloud' +--- + +# Hetzner Cloud Builder + +Type: `hcloud` + +The `hcloud` Packer builder is able to create new images for use with +[Hetzner Cloud](https://www.hetzner.cloud). The builder takes a source image, +runs any provisioning necessary on the image after launching it, then snapshots +it into a reusable image. This reusable image can then be used as the foundation +of new servers that are launched within the Hetzner Cloud. + +The builder does *not* manage images. Once it creates an image, it is up to you +to use it or delete it. + +## Configuration Reference + +There are many configuration options available for the builder. They are +segmented below into two categories: required and optional parameters. Within +each category, the available configuration keys are alphabetized. + +In addition to the options listed here, a +[communicator](/docs/templates/communicator.html) can be configured for this +builder. + +### Required: + +- `token` (string) - The client TOKEN to use to access your account. It + can also be specified via environment variable `HCLOUD_TOKEN`, + if set. + +- `image` (string) - ID or name of image to launch server from. + +- `location` (string) - The name of the location to launch the server in. + +- `server_type` (string) - ID or name of the server type this server should be created with. + +### Optional: + +- `endpoint` (string) - Non standard api endpoint URL. Set this if you are + using a Hetzner Cloud API compatible service. It can also be specified via + environment variable `HCLOUD_ENDPOINT`. + +- `server_name` (string) - The name assigned to the server. The Hetzner Cloud + sets the hostname of the machine to this value. + +- `snapshot_name` (string) - The name of the resulting snapshot that will + appear in your account. Defaults to "packer-{{timestamp}}" (see + [configuration templates](/docs/templates/engine.html) for more info). + +- `poll_interval` (string) - Configures the interval in which actions are polled by the client. Default `500ms`. Increase this interval if you run into rate limiting errors. + +- `user_data` (string) - User data to launch with the server. + +- `user_data_file` (string) - Path to a file that will be used for the user + data when launching the server. + +## Basic Example + +Here is a basic example. It is completely valid as soon as you enter your own +access tokens: + +``` json +{ + "type": "hcloud", + "token": "YOUR API KEY", + "image": "ubuntu-18.04", + "location": "nbg1", + "server_type": "cx11", + "ssh_username": "root" +} +```