Skip to content

Commit

Permalink
feat(kubernetes): custom plans and storage encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
peknur committed May 16, 2024
1 parent c1e8eca commit e32be3a
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 31 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ See updating [Changelog example here](https://keepachangelog.com/en/1.0.0/)

## [Unreleased]

## Added
- kubernetes: add support for node group custom plans
- kubernetes: add support for data at rest encryption in node groups

## [8.4.0]

## Added
Expand Down
80 changes: 50 additions & 30 deletions upcloud/kubernetes.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package upcloud

type (
KubernetesClusterState string
KubernetesNodeGroupState string
KubernetesClusterType string
KubernetesClusterTaintEffect string
KubernetesNodeState string
)

const (
KubernetesClusterStatePending KubernetesClusterState = "pending"
KubernetesClusterStateRunning KubernetesClusterState = "running"
Expand All @@ -25,43 +33,43 @@ const (
KubernetesNodeStateRunning KubernetesNodeState = "running"
KubernetesNodeStateTerminating KubernetesNodeState = "terminating"
KubernetesNodeStateUnknown KubernetesNodeState = "unknown"
)

type (
KubernetesClusterState string
KubernetesNodeGroupState string
KubernetesClusterType string
KubernetesClusterTaintEffect string
KubernetesNodeState string
KubernetesStorageTierHDD StorageTier = StorageTierHDD
KubernetesStorageTierMaxIOPS StorageTier = StorageTierMaxIOPS
)

type KubernetesCluster struct {
ControlPlaneIPFilter []string `json:"control_plane_ip_filter"`
Labels []Label `json:"labels"`
Name string `json:"name"`
Network string `json:"network"`
NetworkCIDR string `json:"network_cidr"`
NodeGroups []KubernetesNodeGroup `json:"node_groups"`
State KubernetesClusterState `json:"state"`
UUID string `json:"uuid"`
Version string `json:"version"`
Zone string `json:"zone"`
Plan string `json:"plan"`
PrivateNodeGroups bool `json:"private_node_groups"`
ControlPlaneIPFilter []string `json:"control_plane_ip_filter,omitempty"`
Labels []Label `json:"labels,omitempty"`
Name string `json:"name,omitempty"`
Network string `json:"network,omitempty"`
NetworkCIDR string `json:"network_cidr,omitempty"`
NodeGroups []KubernetesNodeGroup `json:"node_groups,omitempty"`
State KubernetesClusterState `json:"state,omitempty"`
UUID string `json:"uuid,omitempty"`
Version string `json:"version,omitempty"`
Zone string `json:"zone,omitempty"`
Plan string `json:"plan,omitempty"`
PrivateNodeGroups bool `json:"private_node_groups,omitempty"`
// The default storage encryption strategy for all node groups.
StorageEncryption StorageEncryption `json:"storage_encryption,omitempty"`
}

type KubernetesNodeGroup struct {
AntiAffinity bool `json:"anti_affinity,omitempty"`
Count int `json:"count,omitempty"`
KubeletArgs []KubernetesKubeletArg `json:"kubelet_args,omitempty"`
Labels []Label `json:"labels,omitempty"`
Name string `json:"name,omitempty"`
Plan string `json:"plan,omitempty"`
SSHKeys []string `json:"ssh_keys,omitempty"`
State KubernetesNodeGroupState `json:"state,omitempty"`
Storage string `json:"storage,omitempty"`
Taints []KubernetesTaint `json:"taints,omitempty"`
UtilityNetworkAccess bool `json:"utility_network_access,omitempty"`
AntiAffinity bool `json:"anti_affinity,omitempty"`
Count int `json:"count,omitempty"`
KubeletArgs []KubernetesKubeletArg `json:"kubelet_args,omitempty"`
Labels []Label `json:"labels,omitempty"`
Name string `json:"name,omitempty"`
Plan string `json:"plan,omitempty"`
CustomPlan *KubernetesNodeGroupCustomPlan `json:"custom_plan,omitempty"`
SSHKeys []string `json:"ssh_keys,omitempty"`
State KubernetesNodeGroupState `json:"state,omitempty"`
Storage string `json:"storage,omitempty"`
Taints []KubernetesTaint `json:"taints,omitempty"`
UtilityNetworkAccess bool `json:"utility_network_access,omitempty"`
// Node group storage encryption strategy.
StorageEncryption StorageEncryption `json:"storage_encryption,omitempty"`
}

type KubernetesNodeGroupDetails struct {
Expand Down Expand Up @@ -97,3 +105,15 @@ type KubernetesVersion struct {
Id string `json:"id"`
Version string `json:"version"`
}

// KubernetesNodeGroupCustomPlan represents custom server plan used for each node in the node group.
type KubernetesNodeGroupCustomPlan struct {
// The number of CPU cores dedicated to individual node group node
Cores int `json:"cores"`
// The amount of memory in megabytes to assign to individual node group node
Memory int `json:"memory"`
// The size of the storage device in gigabytes.
StorageSize int `json:"storage_size"`
// The storage tier is MaxIOPS® or HDD.
StorageTier StorageTier `json:"storage_tier,omitempty"`
}
46 changes: 46 additions & 0 deletions upcloud/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,52 @@ func TestKubernetesNodeGroupDetails(t *testing.T) {
require.Equal(t, KubernetesNodeGroupStateRunning, got.State)
}

func TestKubernetesNodeGroupEncryptedCustomPlan(t *testing.T) {
t.Parallel()

p := []byte(`
{
"plan": "custom",
"storage_encryption": "data-at-rest",
"custom_plan": {
"cores": 4,
"memory": 2048,
"storage_size": 30,
"storage_tier": "hdd"
}
}
`)
got := KubernetesNodeGroup{}
require.NoError(t, json.Unmarshal(p, &got))
want := KubernetesNodeGroup{
Plan: "custom",
StorageEncryption: StorageEncryptionDataAtReset,
CustomPlan: &KubernetesNodeGroupCustomPlan{
Cores: 4,
Memory: 2048,
StorageSize: 30,
StorageTier: KubernetesStorageTierHDD,
},
}
require.Equal(t, want, got)
}

func TestKubernetesStorageEncryption(t *testing.T) {
t.Parallel()

p := []byte(`
{
"storage_encryption": "data-at-rest"
}
`)
got := KubernetesCluster{}
require.NoError(t, json.Unmarshal(p, &got))
want := KubernetesCluster{
StorageEncryption: StorageEncryptionDataAtReset,
}
require.Equal(t, want, got)
}

func exampleKubernetesCluster() KubernetesCluster {
return KubernetesCluster{
ControlPlaneIPFilter: []string{"0.0.0.0/0"},
Expand Down
6 changes: 6 additions & 0 deletions upcloud/request/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ type CreateKubernetesClusterRequest struct {
Zone string `json:"zone"`
Plan string `json:"plan,omitempty"`
PrivateNodeGroups bool `json:"private_node_groups"`
// The default storage encryption strategy for all node groups (optional).
StorageEncryption upcloud.StorageEncryption `json:"storage_encryption,omitempty"`
}

func (r *CreateKubernetesClusterRequest) RequestURL() string {
Expand Down Expand Up @@ -157,6 +159,10 @@ type KubernetesNodeGroup struct {
Taints []upcloud.KubernetesTaint `json:"taints,omitempty"`
AntiAffinity bool `json:"anti_affinity,omitempty"`
UtilityNetworkAccess *bool `json:"utility_network_access,omitempty"`
// Node group custom plan properties. Required when plan is set as "custom".
CustomPlan *upcloud.KubernetesNodeGroupCustomPlan `json:"custom_plan,omitempty"`
// node group storage encryption strategy (optional).
StorageEncryption upcloud.StorageEncryption `json:"storage_encryption,omitempty"`
}

type CreateKubernetesNodeGroupRequest struct {
Expand Down
62 changes: 62 additions & 0 deletions upcloud/request/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/UpCloudLtd/upcloud-go-api/v8/upcloud"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const exampleCreateKubernetesClusterRequestJSON string = `{
Expand Down Expand Up @@ -428,6 +429,67 @@ func TestDeleteKubernetesNodeGroupRequest(t *testing.T) {
assert.Equal(t, fmt.Sprintf("%s/id/node-groups/nid", kubernetesClusterBasePath), r.RequestURL())
}

func TestCreateKubernetesNodeGroupEncryptedCustomPlanRequest(t *testing.T) {
t.Parallel()

const want string = `
{
"plan": "custom",
"storage_encryption": "data-at-rest",
"custom_plan": {
"cores": 4,
"memory": 2048,
"storage_size": 30,
"storage_tier": "hdd"
}
}
`
ng := CreateKubernetesNodeGroupRequest{
NodeGroup: KubernetesNodeGroup{
Plan: "custom",
StorageEncryption: upcloud.StorageEncryptionDataAtReset,
CustomPlan: &upcloud.KubernetesNodeGroupCustomPlan{
Cores: 4,
Memory: 2048,
StorageSize: 30,
StorageTier: upcloud.KubernetesStorageTierHDD,
},
},
}
got, err := json.Marshal(&ng)
require.NoError(t, err)
require.JSONEq(t, want, string(got))
}

func TestCreateKubernetesStorageEncryptionRequest(t *testing.T) {
t.Parallel()

const want string = `
{
"name": "uks",
"storage_encryption": "data-at-rest",
"network": "00000000-0000-0000-0000-000000000000",
"network_cidr": "172.16.0.1/24",
"private_node_groups": false,
"control_plane_ip_filter": null,
"node_groups": null,
"version": "1.28",
"zone": "fi-hel2"
}
`
c := CreateKubernetesClusterRequest{
Name: "uks",
Version: "1.28",
Zone: "fi-hel2",
StorageEncryption: upcloud.StorageEncryptionDataAtReset,
Network: "00000000-0000-0000-0000-000000000000",
NetworkCIDR: "172.16.0.1/24",
}
got, err := json.Marshal(&c)
require.NoError(t, err)
require.JSONEq(t, want, string(got))
}

func exampleGetKubernetesClustersWithFiltersRequest() GetKubernetesClustersWithFiltersRequest {
return GetKubernetesClustersWithFiltersRequest{
Filters: []QueryFilter{
Expand Down
2 changes: 1 addition & 1 deletion upcloud/service/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (s *Service) GetKubernetesCluster(ctx context.Context, r *request.GetKubern
// CreateKubernetesCluster creates a new Kubernetes cluster.
func (s *Service) CreateKubernetesCluster(ctx context.Context, r *request.CreateKubernetesClusterRequest) (*upcloud.KubernetesCluster, error) {
if r == nil || len(r.Network) == 0 {
return nil, fmt.Errorf("bad request")
return nil, fmt.Errorf("bad request, network is not defined")
}

networkDetails, err := s.GetNetworkDetails(ctx, &request.GetNetworkDetailsRequest{UUID: r.Network})
Expand Down
83 changes: 83 additions & 0 deletions upcloud/service/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -630,3 +630,86 @@ func TestGetKubernetesVersions(t *testing.T) {
assert.Len(t, res, 2)
assert.Equal(t, res[0].Version, "v1.27.4")
}

func TestCreateKubernetesEncryptedCluster(t *testing.T) {
t.Parallel()

networkID := "03e4970d-7791-4b80-a892-682ae0faf46b"
srv, svc := setupTestServerAndService(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf("/%s/network/%s", client.APIVersion, networkID) {
_, _ = fmt.Fprint(w, exampleNetworkResponse)
return
}
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, fmt.Sprintf("/%s/kubernetes", client.APIVersion), r.URL.Path)

payload := request.CreateKubernetesClusterRequest{}
require.NoError(t, json.NewDecoder(r.Body).Decode(&payload))
cluster := upcloud.KubernetesCluster{
Network: networkID,
StorageEncryption: payload.StorageEncryption,
}
js, err := json.Marshal(cluster)
require.NoError(t, err)
_, err = w.Write(js)
require.NoError(t, err)
}))
defer srv.Close()

req := request.CreateKubernetesClusterRequest{
Network: networkID,
StorageEncryption: upcloud.StorageEncryptionDataAtReset,
}
res, err := svc.CreateKubernetesCluster(context.Background(), &req)
require.NoError(t, err)
require.Equal(t, req.StorageEncryption, res.StorageEncryption)
require.Equal(t, networkID, res.Network)
}

func TestCreateKubernetesEncryptedCustomNodeGroup(t *testing.T) {
t.Parallel()

srv, svc := setupTestServerAndService(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, fmt.Sprintf("/%s/kubernetes/_UUID_/node-groups", client.APIVersion), r.URL.Path)

payload := request.KubernetesNodeGroup{}
require.NoError(t, json.NewDecoder(r.Body).Decode(&payload))
require.NotNil(t, payload.CustomPlan)
nodeGroup := upcloud.KubernetesNodeGroup{
StorageEncryption: payload.StorageEncryption,
Plan: payload.Plan,
CustomPlan: &upcloud.KubernetesNodeGroupCustomPlan{
Cores: payload.CustomPlan.Cores,
Memory: payload.CustomPlan.Memory,
StorageSize: payload.CustomPlan.StorageSize,
StorageTier: payload.CustomPlan.StorageTier,
},
}
js, err := json.Marshal(nodeGroup)
require.NoError(t, err)

_, err = w.Write(js)
require.NoError(t, err)
}))
defer srv.Close()

req := request.CreateKubernetesNodeGroupRequest{
ClusterUUID: "_UUID_",
NodeGroup: request.KubernetesNodeGroup{
Plan: "custom",
StorageEncryption: upcloud.StorageEncryptionDataAtReset,
CustomPlan: &upcloud.KubernetesNodeGroupCustomPlan{
Cores: 1,
Memory: 2048,
StorageSize: 25,
StorageTier: upcloud.KubernetesStorageTierMaxIOPS,
},
},
}
res, err := svc.CreateKubernetesNodeGroup(context.Background(), &req)
require.NoError(t, err)
require.NotNil(t, res.CustomPlan)
require.Equal(t, req.NodeGroup.CustomPlan, res.CustomPlan)
require.Equal(t, req.NodeGroup.StorageEncryption, res.StorageEncryption)
}
10 changes: 10 additions & 0 deletions upcloud/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import (
"time"
)

type (
// StorageEncryption is storage encryption strategy enum type.
StorageEncryption string

// StorageTier is storage tier enum type.
StorageTier string
)

// Constants
const (
StorageTypeBackup = "backup"
Expand Down Expand Up @@ -45,6 +53,8 @@ const (
StorageImportStateCancelling = "cancelling"
StorageImportStateCancelled = "cancelled"
StorageImportStateCompleted = "completed"

StorageEncryptionDataAtReset StorageEncryption = "data-at-rest"
)

// Storages represents a /storage response
Expand Down

0 comments on commit e32be3a

Please sign in to comment.