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+
150233func (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