Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for ARM APIs #249

Merged
merged 1 commit into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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