Skip to content

Commit

Permalink
feat: add support for ARM APIs (#249)
Browse files Browse the repository at this point in the history
  • Loading branch information
apricote authored Apr 12, 2023
1 parent 962afeb commit ce9859f
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 65 deletions.
12 changes: 12 additions & 0 deletions hcloud/architecture.go
Original file line number Diff line number Diff line change
@@ -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"
)
42 changes: 39 additions & 3 deletions hcloud/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -127,6 +159,7 @@ type ImageListOpts struct {
Sort []string
Status []ImageStatus
IncludeDeprecated bool
Architecture []Architecture
}

func (l ImageListOpts) values() url.Values {
Expand All @@ -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
}

Expand Down
80 changes: 80 additions & 0 deletions hcloud/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
23 changes: 18 additions & 5 deletions hcloud/iso.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}

Expand Down
40 changes: 23 additions & 17 deletions hcloud/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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,
},
Expand Down
35 changes: 18 additions & 17 deletions hcloud/schema/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 6 additions & 5 deletions hcloud/schema/iso.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 10 additions & 9 deletions hcloud/schema/server_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit ce9859f

Please sign in to comment.