From 270110767c102f33a0936ef1396359cae01c03f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= <4281581+LKaemmerling@users.noreply.github.com> Date: Wed, 17 Oct 2018 12:15:47 +0200 Subject: [PATCH 1/4] Add hcloud Builder --- builder/hcloud/artifact.go | 47 ++ builder/hcloud/artifact_test.go | 29 + builder/hcloud/builder.go | 97 +++ builder/hcloud/builder_acc_test.go | 35 + builder/hcloud/config.go | 132 +++ builder/hcloud/step_create_server.go | 92 +++ builder/hcloud/step_create_snapshot.go | 53 ++ builder/hcloud/step_create_sshkey.go | 119 +++ builder/hcloud/step_shutdown_server.go | 49 ++ command/plugin.go | 2 + .../github.com/hetznercloud/hcloud-go/LICENSE | 21 + .../hetznercloud/hcloud-go/hcloud/action.go | 193 +++++ .../hetznercloud/hcloud-go/hcloud/client.go | 329 ++++++++ .../hcloud-go/hcloud/datacenter.go | 124 +++ .../hetznercloud/hcloud-go/hcloud/error.go | 52 ++ .../hcloud-go/hcloud/floating_ip.go | 335 ++++++++ .../hetznercloud/hcloud-go/hcloud/hcloud.go | 5 + .../hetznercloud/hcloud-go/hcloud/helper.go | 10 + .../hetznercloud/hcloud-go/hcloud/image.go | 243 ++++++ .../hetznercloud/hcloud-go/hcloud/iso.go | 135 +++ .../hetznercloud/hcloud-go/hcloud/location.go | 120 +++ .../hetznercloud/hcloud-go/hcloud/pricing.go | 80 ++ .../hetznercloud/hcloud-go/hcloud/schema.go | 399 +++++++++ .../hcloud-go/hcloud/schema/action.go | 39 + .../hcloud-go/hcloud/schema/datacenter.go | 23 + .../hcloud-go/hcloud/schema/error.go | 43 + .../hcloud-go/hcloud/schema/floating_ip.go | 112 +++ .../hcloud-go/hcloud/schema/image.go | 68 ++ .../hcloud-go/hcloud/schema/iso.go | 22 + .../hcloud-go/hcloud/schema/location.go | 22 + .../hcloud-go/hcloud/schema/meta.go | 23 + .../hcloud-go/hcloud/schema/pricing.go | 58 ++ .../hcloud-go/hcloud/schema/server.go | 300 +++++++ .../hcloud-go/hcloud/schema/server_type.go | 26 + .../hcloud-go/hcloud/schema/ssh_key.go | 47 ++ .../hcloud-go/hcloud/schema/volume.go | 105 +++ .../hetznercloud/hcloud-go/hcloud/server.go | 778 ++++++++++++++++++ .../hcloud-go/hcloud/server_type.go | 144 ++++ .../hetznercloud/hcloud-go/hcloud/ssh_key.go | 233 ++++++ .../hetznercloud/hcloud-go/hcloud/volume.go | 356 ++++++++ vendor/vendor.json | 6 + .../docs/builders/hetzner-cloud.html.md | 85 ++ website/source/index.html.erb | 2 +- 43 files changed, 5192 insertions(+), 1 deletion(-) create mode 100644 builder/hcloud/artifact.go create mode 100644 builder/hcloud/artifact_test.go create mode 100644 builder/hcloud/builder.go create mode 100644 builder/hcloud/builder_acc_test.go create mode 100644 builder/hcloud/config.go create mode 100644 builder/hcloud/step_create_server.go create mode 100644 builder/hcloud/step_create_snapshot.go create mode 100644 builder/hcloud/step_create_sshkey.go create mode 100644 builder/hcloud/step_shutdown_server.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/LICENSE create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/action.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/client.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/datacenter.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/error.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/floating_ip.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/hcloud.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/helper.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/image.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/iso.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/location.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/pricing.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/action.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/datacenter.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/error.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/floating_ip.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/image.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/iso.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/location.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/meta.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/pricing.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/server.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/server_type.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/ssh_key.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/schema/volume.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/server.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/server_type.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/ssh_key.go create mode 100644 vendor/github.com/hetznercloud/hcloud-go/hcloud/volume.go create mode 100644 website/source/docs/builders/hetzner-cloud.html.md 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..2b7862f46 --- /dev/null +++ b/builder/hcloud/builder.go @@ -0,0 +1,97 @@ +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), + }, + new(stepCreateServer), + &communicator.StepConnect{ + Config: &b.config.Comm, + Host: getServerIP, + SSHConfig: b.config.Comm.SSHConfigFunc(), + }, + new(common.StepProvision), + &common.StepCleanupTempKeys{ + Comm: &b.config.Comm, + }, + new(stepShutdownServer), + new(stepCreateSnapshot), + } + ui.Say("Steps OK") + // Run the steps + b.runner = common.NewRunner(steps, b.config.PackerConfig, ui) + b.runner.Run(state) + ui.Say("Run OK") + // 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 { + log.Println("Failed to find snapshot_name in state. Bug?") + 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..85f689700 --- /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("1Error 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..c0edbef8e --- /dev/null +++ b/builder/hcloud/step_create_sshkey.go @@ -0,0 +1,119 @@ +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 + priv_der := x509.MarshalPKCS1PrivateKey(priv) + priv_blk := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: priv_der, + } + + // Set the private key in the config for later + c.Comm.SSHPrivateKey = pem.EncodeToMemory(&priv_blk) + + // Marshal the public key into SSH compatible format + // TODO properly handle the public key error + pub, _ := ssh.NewPublicKey(&priv.PublicKey) + pub_sshformat := string(ssh.MarshalAuthorizedKey(pub)) + + // The name of the public key on DO + name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) + + // Create the key! + key, _, err := client.SSHKey.Create(context.TODO(), hcloud.SSHKeyCreateOpts{ + Name: name, + PublicKey: pub_sshformat, + }) + 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(&priv_blk)); 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 + } + } + } + ui.Say("SSH Key OK") + 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 6ffa53a75..f9e058563 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1114,6 +1114,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..6a7409398 --- /dev/null +++ b/website/source/docs/builders/hetzner-cloud.html.md @@ -0,0 +1,85 @@ +--- +description: | + The Hetzner Cloud Packer builder is able to create new images for use with + DigitalOcean. 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 location to create server in. + +- `location` (string) - The name of the location to launch the + droplet 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 DigitalOcean API compatible service. It can also be specified via + environment variable `HCLOUD_ENDPOINT`. + +- `server_name` (string) - The name assigned to the droplet. DigitalOcean + 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 Droplet. + +- `user_data_file` (string) - Path to a file that will be used for the user + data when launching the Droplet. + +- `tags` (list) - Tags to apply to the droplet when it is created + +## 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" +} +``` diff --git a/website/source/index.html.erb b/website/source/index.html.erb index 18222370b..a7ffc1066 100644 --- a/website/source/index.html.erb +++ b/website/source/index.html.erb @@ -80,7 +80,7 @@ description: |-
Out of the box Packer comes with support to build images for Amazon EC2, CloudStack, DigitalOcean, Docker, Google Compute - Engine, Microsoft Azure, QEMU, VirtualBox, VMware, and more. + Engine, Hetzner Cloud, Microsoft Azure, QEMU, VirtualBox, VMware, and more. Support for more platforms is on the way, and anyone can add new platforms via plugins.
From 16453173db02a51f79f2948026ca0d62541a4d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?=Out of the box Packer comes with support to build images for Amazon EC2, CloudStack, DigitalOcean, Docker, Google Compute - Engine, Hetzner Cloud, Microsoft Azure, QEMU, VirtualBox, VMware, and more. + Engine, Microsoft Azure, QEMU, VirtualBox, VMware, and more. Support for more platforms is on the way, and anyone can add new platforms via plugins.
From e4a670d2a26b3d318616ab28b5aad79c00109f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?=