Repository: hetznercloud/hcloud-go
Pull request #249 (2023-04-12 07:53:18)

feat: add support for ARM APIs


Patch:
diff --git a/hcloud/architecture.go b/hcloud/architecture.go
new file mode 100644
index 0000000..8e0c1df
--- /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 6a71898..04d61b5 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/iso.go b/hcloud/iso.go
index ed2825b..d5814cb 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 091426b..b72db88 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 7a3be88..76775b1 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 e410468..dfcc4e3 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 5d4f10b..e2fe2f7 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/server_type.go b/hcloud/server_type.go
index cf712eb..37ebb7f 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.


Test patch:
diff --git a/hcloud/image_test.go b/hcloud/image_test.go
index 28fd0cd..745552b 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/schema_test.go b/hcloud/schema_test.go
index fd0a364..e31c203 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)
 	}