Skip to content

Commit 482b1e2

Browse files
Pidu2tiagodccbastjansimu
authored
Add functionality to tag Root Volume (#44)
Co-authored-by: Tiago da Costa Cova <Tiago.daCostaCova@pm.me> Co-authored-by: Sebastian Widmer <widmer.sebastian@gmail.com> Co-authored-by: Sebastian Widmer <sebastian.widmer@vshn.net> Co-authored-by: Simon Gerber <gesimu@gmail.com>
1 parent 3238880 commit 482b1e2

7 files changed

Lines changed: 457 additions & 56 deletions

File tree

api/cloudscale/provider/v1beta1/cloudscaleprovider_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ type CloudscaleMachineProviderSpec struct {
7171
Image string `json:"image"`
7272
// RootVolumeSizeGB is the size of the root volume in GB.
7373
RootVolumeSizeGB int `json:"rootVolumeSizeGB"`
74+
// RootVolumeTags is a map of tags to apply to the root volume.
75+
RootVolumeTags map[string]string `json:"rootVolumeTags"`
7476
// SSHKeys is a list of SSH keys to add to the machine.
7577
SSHKeys []string `json:"sshKeys"`
7678
// UseIPV6 is a flag to enable IPv6 on the machine.

api/cloudscale/provider/v1beta1/zz_generated.deepcopy.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ func runManager(metricsAddr, probeAddr, watchNamespace string, enableLeaderElect
180180
ServerGroupClientFactory: func(token string) cloudscale.ServerGroupService {
181181
return newClient(token).ServerGroups
182182
},
183+
VolumeClientFactory: func(token string) cloudscale.VolumeService {
184+
return newClient(token).Volumes
185+
},
183186
})
184187

185188
if err := capimachine.AddWithActuator(mgr, machineActuator, featureGate); err != nil {

pkg/machine/actuator.go

Lines changed: 133 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"context"
66
"encoding/json"
77
"fmt"
8+
"maps"
89
"strings"
10+
"time"
911

1012
"github.com/cloudscale-ch/cloudscale-go-sdk/v6"
1113
"github.com/google/go-jsonnet"
@@ -14,6 +16,7 @@ import (
1416
corev1 "k8s.io/api/core/v1"
1517
"k8s.io/apimachinery/pkg/api/equality"
1618
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19+
"k8s.io/apimachinery/pkg/util/wait"
1720
"k8s.io/utils/ptr"
1821
"sigs.k8s.io/controller-runtime/pkg/client"
1922
"sigs.k8s.io/controller-runtime/pkg/log"
@@ -41,6 +44,7 @@ type Actuator struct {
4144

4245
serverClientFactory func(token string) cloudscale.ServerService
4346
serverGroupClientFactory func(token string) cloudscale.ServerGroupService
47+
volumeClientFactory func(token string) cloudscale.VolumeService
4448
}
4549

4650
// ActuatorParams holds parameter information for Actuator.
@@ -51,6 +55,7 @@ type ActuatorParams struct {
5155

5256
ServerClientFactory func(token string) cloudscale.ServerService
5357
ServerGroupClientFactory func(token string) cloudscale.ServerGroupService
58+
VolumeClientFactory func(token string) cloudscale.VolumeService
5459
}
5560

5661
// NewActuator returns an actuator.
@@ -62,6 +67,7 @@ func NewActuator(params ActuatorParams) *Actuator {
6267

6368
serverClientFactory: params.ServerClientFactory,
6469
serverGroupClientFactory: params.ServerGroupClientFactory,
70+
volumeClientFactory: params.VolumeClientFactory,
6571
}
6672
}
6773

@@ -81,12 +87,8 @@ func (a *Actuator) Create(ctx context.Context, machine *machinev1beta1.Machine)
8187
return fmt.Errorf("failed to load user data secret: %w", err)
8288
}
8389

84-
// Null is not allowed for tags in the cloudscale API
85-
if spec.Tags == nil {
86-
spec.Tags = make(map[string]string)
87-
}
88-
spec.Tags[machineNameTag] = machine.Name
89-
spec.Tags[machineClusterIDTag] = mctx.clusterId
90+
// prepare server tags by combining fixed and user-provided tags
91+
serverTags := buildServerTags(machine.Name, mctx.clusterId, spec.Tags)
9092

9193
// Null is not allowed for SSH keys in the cloudscale API
9294
if spec.SSHKeys == nil {
@@ -112,7 +114,7 @@ func (a *Actuator) Create(ctx context.Context, machine *machinev1beta1.Machine)
112114
Name: name,
113115

114116
TaggedResourceRequest: cloudscale.TaggedResourceRequest{
115-
Tags: ptr.To(cloudscale.TagMap(spec.Tags)),
117+
Tags: ptr.To(cloudscale.TagMap(serverTags)),
116118
},
117119
Zone: spec.Zone,
118120
ZonalResourceRequest: cloudscale.ZonalResourceRequest{
@@ -136,6 +138,59 @@ func (a *Actuator) Create(ctx context.Context, machine *machinev1beta1.Machine)
136138

137139
l.Info("Created machine", "machine", machine.Name, "uuid", s.UUID, "server", s)
138140

141+
// Tag the RootVolume if tags are set
142+
// It can take some time for CloudScale to populate the root volume UUID
143+
if len(spec.RootVolumeTags) > 0 {
144+
backoff := wait.Backoff{
145+
Duration: 1 * time.Second,
146+
Factor: 2.0,
147+
Jitter: 0.1,
148+
Steps: 10,
149+
Cap: 5 * time.Minute,
150+
}
151+
vc := a.volumeClientFactory(mctx.token)
152+
153+
var lastErr error
154+
var rootVolumeUUID string
155+
err := wait.ExponentialBackoff(backoff, func() (bool, error) {
156+
// query server to check if root volume UUID has been populated
157+
s, err := sc.Get(ctx, s.UUID)
158+
if err != nil {
159+
lastErr = err
160+
return false, nil
161+
}
162+
if len(s.Volumes) == 0 {
163+
lastErr = fmt.Errorf("no volumes found for server %q", s.UUID)
164+
return false, nil
165+
}
166+
// NOTE: cloudscale currently guarantees that the first entry in the volumes array is the root volume
167+
rootVolumeUUID = s.Volumes[0].UUID
168+
if rootVolumeUUID == "" {
169+
lastErr = fmt.Errorf("root volume UUID is empty for server %q", s.UUID)
170+
return false, nil
171+
}
172+
// ensure volume is queryable before tagging
173+
if _, err := vc.Get(ctx, rootVolumeUUID); err != nil {
174+
lastErr = fmt.Errorf("failed to get volume for server %q", s.UUID)
175+
return false, nil
176+
}
177+
return true, nil
178+
})
179+
180+
if err != nil {
181+
if lastErr == nil {
182+
lastErr = err
183+
}
184+
return fmt.Errorf("failed to get root volume UUID for machine %q: %w (last error: %v)", machine.Name, err, lastErr)
185+
}
186+
187+
if err := tagRootVolume(ctx, vc, rootVolumeUUID, spec.RootVolumeTags); err != nil {
188+
return fmt.Errorf("failed to tag root volume of machine %q: %w", machine.Name, err)
189+
}
190+
191+
l.Info("Tagged volume", "volume", rootVolumeUUID, "machine", machine.Name, "uuid", s.UUID, "server", s)
192+
}
193+
139194
if err := updateMachineFromCloudscaleServer(machine, *s); err != nil {
140195
return fmt.Errorf("failed to update machine %q from cloudscale API response: %w", machine.Name, err)
141196
}
@@ -147,6 +202,34 @@ func (a *Actuator) Create(ctx context.Context, machine *machinev1beta1.Machine)
147202
return nil
148203
}
149204

205+
func tagRootVolume(ctx context.Context, vc cloudscale.VolumeService, uuid string, tags map[string]string) error {
206+
// The cloudscale API is confused by a nil map in a non-nil TagMap pointer
207+
if tags == nil {
208+
tags = make(map[string]string)
209+
}
210+
211+
req := &cloudscale.VolumeRequest{
212+
TaggedResourceRequest: cloudscale.TaggedResourceRequest{
213+
Tags: ptr.To(cloudscale.TagMap(tags)),
214+
},
215+
}
216+
if err := vc.Update(ctx, uuid, req); err != nil {
217+
reqRaw, _ := json.Marshal(req)
218+
return fmt.Errorf("failed to tag root volume %q: %w, req:%s", uuid, err, string(reqRaw))
219+
}
220+
return nil
221+
}
222+
223+
func buildServerTags(machineName, clusterID string, userTags map[string]string) map[string]string {
224+
tags := make(map[string]string)
225+
maps.Copy(tags, userTags)
226+
// add fixed tags
227+
tags[machineNameTag] = machineName
228+
tags[machineClusterIDTag] = clusterID
229+
230+
return tags
231+
}
232+
150233
func (a *Actuator) Exists(ctx context.Context, machine *machinev1beta1.Machine) (bool, error) {
151234
mctx, err := a.getMachineContext(ctx, machine)
152235
if err != nil {
@@ -164,12 +247,55 @@ func (a *Actuator) Update(ctx context.Context, machine *machinev1beta1.Machine)
164247
if err != nil {
165248
return fmt.Errorf("failed to get machine context: %w", err)
166249
}
250+
spec := mctx.spec
167251
sc := a.serverClientFactory(mctx.token)
168252

169253
s, err := a.getServer(ctx, sc, *mctx)
170254
if err != nil {
171255
return fmt.Errorf("failed to get server %q: %w", machine.Name, err)
172256
}
257+
// getServer function returns nil if no server found
258+
if s == nil {
259+
return fmt.Errorf("server not found for machine %q", machine.Name)
260+
}
261+
262+
// 1. Update Server Tags
263+
serverTags := buildServerTags(machine.Name, mctx.clusterId, spec.Tags)
264+
if !maps.Equal(s.Tags, serverTags) {
265+
updateReq := &cloudscale.ServerUpdateRequest{
266+
TaggedResourceRequest: cloudscale.TaggedResourceRequest{
267+
Tags: ptr.To(cloudscale.TagMap(serverTags)),
268+
},
269+
}
270+
271+
if err := sc.Update(ctx, s.UUID, updateReq); err != nil {
272+
return fmt.Errorf("failed to update tags for machine %q (server uuid %q): %w", machine.Name, s.UUID, err)
273+
}
274+
}
275+
276+
// 2. Update Root Volume Tags
277+
if len(s.Volumes) > 0 {
278+
// NOTE: cloudscale currently guarantees that the first entry in the volumes array is the root volume
279+
rootVolumeUUID := s.Volumes[0].UUID
280+
vc := a.volumeClientFactory(mctx.token)
281+
282+
vol, err := vc.Get(ctx, rootVolumeUUID)
283+
if err != nil {
284+
return fmt.Errorf("failed to get root volume %q for machine %q: %w", rootVolumeUUID, machine.Name, err)
285+
}
286+
if vol == nil {
287+
return fmt.Errorf("root volume %q not found for machine %q", rootVolumeUUID, machine.Name)
288+
}
289+
290+
if !maps.Equal(vol.Tags, spec.RootVolumeTags) {
291+
if err := tagRootVolume(ctx, vc, rootVolumeUUID, spec.RootVolumeTags); err != nil {
292+
return fmt.Errorf("failed to tag root volume of machine %q: %w", machine.Name, err)
293+
}
294+
}
295+
} else {
296+
// this should not happen for a running server but better to handle it
297+
return fmt.Errorf("failed to tag root volume of machine %q: server has no volumes", machine.Name)
298+
}
173299

174300
if err := updateMachineFromCloudscaleServer(machine, *s); err != nil {
175301
return fmt.Errorf("failed to update machine %q from cloudscale API response: %w", machine.Name, err)

0 commit comments

Comments
 (0)