diff --git a/hcloud/architecture.go b/hcloud/architecture.go new file mode 100644 index 00000000..8e0c1df5 --- /dev/null +++ b/hcloud/architecture.go @@ -0,0 +1,12 @@ +package hcloud + +// Architecture specifies the architecture of the CPU. +type Architecture string + +const ( + // ArchitectureX86 is the architecture for Intel/AMD x86 CPUs. + ArchitectureX86 Architecture = "x86" + + // ArchitectureARM is the architecture for ARM CPUs. + ArchitectureARM Architecture = "arm" +) diff --git a/hcloud/image.go b/hcloud/image.go index 6a718982..04d61b50 100644 --- a/hcloud/image.go +++ b/hcloud/image.go @@ -26,8 +26,9 @@ type Image struct { BoundTo *Server RapidDeploy bool - OSFlavor string - OSVersion string + OSFlavor string + OSVersion string + Architecture Architecture Protection ImageProtection Deprecated time.Time // The zero value denotes the image is not deprecated. @@ -98,6 +99,8 @@ func (c *ImageClient) GetByID(ctx context.Context, id int) (*Image, *Response, e } // GetByName retrieves an image by its name. If the image does not exist, nil is returned. +// +// Deprecated: Use [ImageClient.GetByNameAndArchitecture] instead. func (c *ImageClient) GetByName(ctx context.Context, name string) (*Image, *Response, error) { if name == "" { return nil, nil, nil @@ -109,15 +112,44 @@ func (c *ImageClient) GetByName(ctx context.Context, name string) (*Image, *Resp return images[0], response, err } +// GetByNameAndArchitecture retrieves an image by its name and architecture. If the image does not exist, +// nil is returned. +// In contrast to [ImageClient.Get], this method also returns deprecated images. Depending on your needs you should +// check for this in your calling method. +func (c *ImageClient) GetByNameAndArchitecture(ctx context.Context, name string, architecture Architecture) (*Image, *Response, error) { + if name == "" { + return nil, nil, nil + } + images, response, err := c.List(ctx, ImageListOpts{Name: name, Architecture: []Architecture{architecture}, IncludeDeprecated: true}) + if len(images) == 0 { + return nil, response, err + } + return images[0], response, err +} + // Get retrieves an image by its ID if the input can be parsed as an integer, otherwise it // retrieves an image by its name. If the image does not exist, nil is returned. +// +// Deprecated: Use [ImageClient.GetForArchitecture] instead. 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.GetByID(ctx, id) } return c.GetByName(ctx, idOrName) } +// GetForArchitecture retrieves an image by its ID if the input can be parsed as an integer, otherwise it +// retrieves an image by its name and architecture. If the image does not exist, nil is returned. +// +// In contrast to [ImageClient.Get], this method also returns deprecated images. Depending on your needs you should +// check for this in your calling method. +func (c *ImageClient) GetForArchitecture(ctx context.Context, idOrName string, architecture Architecture) (*Image, *Response, error) { + if id, err := strconv.Atoi(idOrName); err == nil { + return c.GetByID(ctx, id) + } + return c.GetByNameAndArchitecture(ctx, idOrName, architecture) +} + // ImageListOpts specifies options for listing images. type ImageListOpts struct { ListOpts @@ -127,6 +159,7 @@ type ImageListOpts struct { Sort []string Status []ImageStatus IncludeDeprecated bool + Architecture []Architecture } func (l ImageListOpts) values() url.Values { @@ -149,6 +182,9 @@ func (l ImageListOpts) values() url.Values { for _, status := range l.Status { vals.Add("status", string(status)) } + for _, arch := range l.Architecture { + vals.Add("architecture", string(arch)) + } return vals } diff --git a/hcloud/image_test.go b/hcloud/image_test.go index 28fd0cde..745552ba 100644 --- a/hcloud/image_test.go +++ b/hcloud/image_test.go @@ -171,6 +171,86 @@ func TestImageClient(t *testing.T) { } }) + t.Run("GetByNameAndArchitecture", func(t *testing.T) { + env := newTestEnv() + defer env.Teardown() + + env.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { + if r.URL.RawQuery != "architecture=arm&include_deprecated=true&name=my+image" { + t.Fatal("unexpected query parameter") + } + json.NewEncoder(w).Encode(schema.ImageListResponse{ + Images: []schema.Image{ + { + ID: 1, + }, + }, + }) + }) + + ctx := context.Background() + image, _, err := env.Client.Image.GetByNameAndArchitecture(ctx, "my image", ArchitectureARM) + if err != nil { + t.Fatal(err) + } + if image == nil { + t.Fatal("no image") + } + if image.ID != 1 { + t.Errorf("unexpected image ID: %v", image.ID) + } + + t.Run("via GetForArchitecture", func(t *testing.T) { + image, _, err := env.Client.Image.GetForArchitecture(ctx, "my image", ArchitectureARM) + if err != nil { + t.Fatal(err) + } + if image == nil { + t.Fatal("no image") + } + if image.ID != 1 { + t.Errorf("unexpected image ID: %v", image.ID) + } + }) + }) + + t.Run("GetByNameAndArchitecture (not found)", func(t *testing.T) { + env := newTestEnv() + defer env.Teardown() + + env.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) { + if r.URL.RawQuery != "architecture=arm&include_deprecated=true&name=my+image" { + t.Fatal("unexpected query parameter") + } + json.NewEncoder(w).Encode(schema.ImageListResponse{ + Images: []schema.Image{}, + }) + }) + + ctx := context.Background() + image, _, err := env.Client.Image.GetByNameAndArchitecture(ctx, "my image", ArchitectureARM) + if err != nil { + t.Fatal(err) + } + if image != nil { + t.Fatal("unexpected image") + } + }) + + t.Run("GetByNameAndArchitecture (empty)", func(t *testing.T) { + env := newTestEnv() + defer env.Teardown() + + ctx := context.Background() + image, _, err := env.Client.Image.GetByNameAndArchitecture(ctx, "", ArchitectureARM) + if err != nil { + t.Fatal(err) + } + if image != nil { + t.Fatal("unexpected image") + } + }) + t.Run("List", func(t *testing.T) { env := newTestEnv() defer env.Teardown() diff --git a/hcloud/iso.go b/hcloud/iso.go index ed2825ba..d5814cb8 100644 --- a/hcloud/iso.go +++ b/hcloud/iso.go @@ -12,11 +12,12 @@ import ( // ISO represents an ISO image in the Hetzner Cloud. type ISO struct { - ID int - Name string - Description string - Type ISOType - Deprecated time.Time + ID int + Name string + Description string + Type ISOType + Architecture *Architecture + Deprecated time.Time } // IsDeprecated returns true if the ISO is deprecated. @@ -83,6 +84,12 @@ type ISOListOpts struct { ListOpts Name string Sort []string + // Architecture filters the ISOs by Architecture. Note that custom ISOs do not have any architecture set, and you + // must use IncludeWildcardArchitecture to include them. + Architecture []Architecture + // IncludeWildcardArchitecture must be set to also return custom ISOs that have no architecture set, if you are + // also setting the Architecture field. + IncludeWildcardArchitecture bool } func (l ISOListOpts) values() url.Values { @@ -93,6 +100,12 @@ func (l ISOListOpts) values() url.Values { for _, sort := range l.Sort { vals.Add("sort", sort) } + for _, arch := range l.Architecture { + vals.Add("architecture", string(arch)) + } + if l.IncludeWildcardArchitecture { + vals.Add("include_architecture_wildcard", "true") + } return vals } diff --git a/hcloud/schema.go b/hcloud/schema.go index 091426bc..b72db88d 100644 --- a/hcloud/schema.go +++ b/hcloud/schema.go @@ -118,13 +118,17 @@ func PrimaryIPFromSchema(s schema.PrimaryIP) *PrimaryIP { // ISOFromSchema converts a schema.ISO to an ISO. func ISOFromSchema(s schema.ISO) *ISO { - return &ISO{ + iso := &ISO{ ID: s.ID, Name: s.Name, Description: s.Description, Type: ISOType(s.Type), Deprecated: s.Deprecated, } + if s.Architecture != nil { + iso.Architecture = Ptr(Architecture(*s.Architecture)) + } + return iso } // LocationFromSchema converts a schema.Location to a Location. @@ -274,14 +278,15 @@ func ServerPrivateNetFromSchema(s schema.ServerPrivateNet) ServerPrivateNet { // 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), + 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), + Architecture: Architecture(s.Architecture), } for _, price := range s.Prices { st.Pricings = append(st.Pricings, ServerTypeLocationPricing{ @@ -318,14 +323,15 @@ func SSHKeyFromSchema(s schema.SSHKey) *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, + 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, + Architecture: Architecture(s.Architecture), Protection: ImageProtection{ Delete: s.Protection.Delete, }, diff --git a/hcloud/schema/image.go b/hcloud/schema/image.go index 7a3be887..76775b13 100644 --- a/hcloud/schema/image.go +++ b/hcloud/schema/image.go @@ -4,23 +4,24 @@ 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"` - Deleted time.Time `json:"deleted"` - Labels map[string]string `json:"labels"` + 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"` + Architecture string `json:"architecture"` + RapidDeploy bool `json:"rapid_deploy"` + Protection ImageProtection `json:"protection"` + Deprecated time.Time `json:"deprecated"` + Deleted time.Time `json:"deleted"` + Labels map[string]string `json:"labels"` } // ImageProtection represents the protection level of a image. diff --git a/hcloud/schema/iso.go b/hcloud/schema/iso.go index e4104689..dfcc4e34 100644 --- a/hcloud/schema/iso.go +++ b/hcloud/schema/iso.go @@ -4,11 +4,12 @@ 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"` + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + Architecture *string `json:"architecture"` + Deprecated time.Time `json:"deprecated"` } // ISOGetResponse defines the schema of the response when retrieving a single ISO. diff --git a/hcloud/schema/server_type.go b/hcloud/schema/server_type.go index 5d4f10b0..e2fe2f72 100644 --- a/hcloud/schema/server_type.go +++ b/hcloud/schema/server_type.go @@ -2,15 +2,16 @@ 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"` + 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"` + Architecture string `json:"architecture"` + Prices []PricingServerTypePrice `json:"prices"` } // ServerTypeListResponse defines the schema of the response when diff --git a/hcloud/schema_test.go b/hcloud/schema_test.go index fd0a3645..e31c2038 100644 --- a/hcloud/schema_test.go +++ b/hcloud/schema_test.go @@ -428,6 +428,7 @@ func TestISOFromSchema(t *testing.T) { "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public", + "architecture": "x86", "deprecated": "2018-02-28T00:00:00+00:00" }`) @@ -448,6 +449,11 @@ func TestISOFromSchema(t *testing.T) { if iso.Type != ISOTypePublic { t.Errorf("unexpected type: %v", iso.Type) } + if iso.Architecture == nil { + t.Errorf("unexpected empty architecture") + } else if *iso.Architecture != ArchitectureX86 { + t.Errorf("unexpected architecture: %s", *iso.Architecture) + } if iso.Deprecated.IsZero() { t.Errorf("unexpected value for deprecated: %v", iso.Deprecated) } @@ -914,6 +920,7 @@ func TestServerTypeFromSchema(t *testing.T) { "disk": 20, "storage_type": "local", "cpu_type": "shared", + "architecture": "x86", "prices": [ { "location": "fsn1", @@ -959,6 +966,9 @@ func TestServerTypeFromSchema(t *testing.T) { if serverType.CPUType != CPUTypeShared { t.Errorf("unexpected cpu type: %q", serverType.CPUType) } + if serverType.Architecture != ArchitectureX86 { + t.Errorf("unexpected cpu architecture: %q", serverType.Architecture) + } if len(serverType.Pricings) != 1 { t.Errorf("unexpected number of pricings: %d", len(serverType.Pricings)) } else { @@ -1139,6 +1149,7 @@ func TestImageFromSchema(t *testing.T) { "bound_to": 1, "os_flavor": "ubuntu", "os_version": "16.04", + "architecture": "arm", "rapid_deploy": false, "protection": { "delete": true @@ -1196,6 +1207,9 @@ func TestImageFromSchema(t *testing.T) { if image.OSFlavor != "ubuntu" { t.Errorf("unexpected OSFlavor: %v", image.OSFlavor) } + if image.Architecture != ArchitectureARM { + t.Errorf("unexpected architecture: %v", image.Architecture) + } if image.RapidDeploy { t.Errorf("unexpected RapidDeploy: %v", image.RapidDeploy) } diff --git a/hcloud/server_type.go b/hcloud/server_type.go index cf712eb8..37ebb7f0 100644 --- a/hcloud/server_type.go +++ b/hcloud/server_type.go @@ -11,15 +11,16 @@ import ( // 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 + ID int + Name string + Description string + Cores int + Memory float32 + Disk int + StorageType StorageType + CPUType CPUType + Architecture Architecture + Pricings []ServerTypeLocationPricing } // StorageType specifies the type of storage.