Skip to content

Commit 2911251

Browse files
authored
fix: iaas client only created once (#24)
* fix: iaas client only created once * fix: implemented single-credential lifecycle rather than falsely suggesting credential updates would be picked up automatically
1 parent aa3fb24 commit 2911251

15 files changed

+194
-160
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ kubectl apply -f samples/machine.yaml
9999

100100
This provider uses the official [STACKIT Go SDK](https://github.com/stackitcloud/stackit-sdk-go) for all interactions with the STACKIT IaaS API. The SDK provides type-safe API access, built-in authentication handling, and is officially maintained by STACKIT.
101101

102-
The SDK client is stateless and supports different credentials per MachineClass, allowing multi-tenancy scenarios where different machine pools use different STACKIT projects.
102+
Each provider instance is bound to a single STACKIT project via the service account credentials provided in the Secret. The SDK client is initialized once on first use and automatically handles token refresh. In Gardener deployments, each shoot cluster gets its own control plane with a dedicated MCM and provider instance.
103103

104104
### Authentication & Credentials
105105

@@ -115,6 +115,8 @@ The provider requires STACKIT credentials to be provided via a Kubernetes Secret
115115

116116
The service account key should be obtained from the STACKIT Portal (Project Settings → Service Accounts → Create Key) and contains JWT credentials and a private key for secure authentication.
117117

118+
**Credential Rotation:** The provider captures credentials on first use and reuses the same STACKIT SDK client for all subsequent requests (the SDK automatically handles token refresh). If the Secret is updated with new credentials, the provider pod must be restarted to pick up the changes. This follows the standard Kubernetes pattern for credential rotation.
119+
118120
### Environment Variables
119121

120122
The provider supports the following environment variables for configuration:

pkg/provider/core.go

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineR
5353
serviceAccountKey := string(req.Secret.Data["serviceAccountKey"])
5454
region := string(req.Secret.Data["region"])
5555

56+
// Initialize client on first use (lazy initialization)
57+
if err := p.ensureClient(serviceAccountKey); err != nil {
58+
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to initialize STACKIT client: %v", err))
59+
}
60+
5661
// Build labels: merge ProviderSpec labels with MCM-specific labels
5762
labels := make(map[string]string)
5863
// Start with user-provided labels from ProviderSpec
@@ -164,7 +169,7 @@ func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineR
164169
}
165170

166171
// Call STACKIT API to create server
167-
server, err := p.client.CreateServer(ctx, serviceAccountKey, projectID, region, createReq)
172+
server, err := p.client.CreateServer(ctx, projectID, region, createReq)
168173
if err != nil {
169174
klog.Errorf("Failed to create server for machine %q: %v", req.Machine.Name, err)
170175
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to create server: %v", err))
@@ -202,20 +207,23 @@ func (p *Provider) DeleteMachine(ctx context.Context, req *driver.DeleteMachineR
202207
return nil, status.Error(codes.InvalidArgument, "ProviderID is required")
203208
}
204209

205-
// Extract token from Secret for authentication
210+
// Extract credentials from Secret
206211
serviceAccountKey := string(req.Secret.Data["serviceAccountKey"])
207-
208-
// Extract region from Secret
209212
region := string(req.Secret.Data["region"])
210213

214+
// Initialize client on first use (lazy initialization)
215+
if err := p.ensureClient(serviceAccountKey); err != nil {
216+
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to initialize STACKIT client: %v", err))
217+
}
218+
211219
// Parse ProviderID to extract projectID and serverID
212220
projectID, serverID, err := parseProviderID(req.Machine.Spec.ProviderID)
213221
if err != nil {
214222
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid ProviderID format: %v", err))
215223
}
216224

217225
// Call STACKIT API to delete server
218-
err = p.client.DeleteServer(ctx, serviceAccountKey, projectID, region, serverID)
226+
err = p.client.DeleteServer(ctx, projectID, region, serverID)
219227
if err != nil {
220228
// Check if server was not found (404) - this is OK for idempotency
221229
if errors.Is(err, ErrServerNotFound) {
@@ -258,12 +266,15 @@ func (p *Provider) GetMachineStatus(ctx context.Context, req *driver.GetMachineS
258266
return nil, status.Error(codes.NotFound, "machine does not have a ProviderID yet")
259267
}
260268

261-
// Extract token from Secret for authentication
269+
// Extract credentials from Secret
262270
serviceAccountKey := string(req.Secret.Data["serviceAccountKey"])
263-
264-
// Extract region from Secret
265271
region := string(req.Secret.Data["region"])
266272

273+
// Initialize client on first use (lazy initialization)
274+
if err := p.ensureClient(serviceAccountKey); err != nil {
275+
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to initialize STACKIT client: %v", err))
276+
}
277+
267278
// Parse ProviderID to extract projectID and serverID
268279
// Expected format: stackit://<projectId>/<serverId>
269280
projectID, serverID, err := parseProviderID(req.Machine.Spec.ProviderID)
@@ -272,7 +283,7 @@ func (p *Provider) GetMachineStatus(ctx context.Context, req *driver.GetMachineS
272283
}
273284

274285
// Call STACKIT API to get server status
275-
server, err := p.client.GetServer(ctx, serviceAccountKey, projectID, region, serverID)
286+
server, err := p.client.GetServer(ctx, projectID, region, serverID)
276287
if err != nil {
277288
// Check if server was not found (404)
278289
if errors.Is(err, ErrServerNotFound) {
@@ -313,8 +324,13 @@ func (p *Provider) ListMachines(ctx context.Context, req *driver.ListMachinesReq
313324
serviceAccountKey := string(req.Secret.Data["serviceAccountKey"])
314325
region := string(req.Secret.Data["region"])
315326

327+
// Initialize client on first use (lazy initialization)
328+
if err := p.ensureClient(serviceAccountKey); err != nil {
329+
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to initialize STACKIT client: %v", err))
330+
}
331+
316332
// Call STACKIT API to list all servers
317-
servers, err := p.client.ListServers(ctx, serviceAccountKey, projectID, region)
333+
servers, err := p.client.ListServers(ctx, projectID, region)
318334
if err != nil {
319335
klog.Errorf("Failed to list servers for MachineClass %q: %v", req.MachineClass.Name, err)
320336
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to list servers: %v", err))

pkg/provider/core_create_machine_basic_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ var _ = Describe("CreateMachine", func() {
9595
var capturedReq *CreateServerRequest
9696
var capturedProjectID string
9797

98-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
98+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
9999
capturedProjectID = projectID
100100
capturedReq = req
101101
return &Server{
@@ -165,7 +165,7 @@ var _ = Describe("CreateMachine", func() {
165165

166166
Context("when STACKIT API fails", func() {
167167
It("should return Internal error on API failure", func() {
168-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
168+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
169169
return nil, fmt.Errorf("API connection failed")
170170
}
171171

pkg/provider/core_create_machine_config_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ var _ = Describe("CreateMachine", func() {
8989
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
9090

9191
var capturedReq *CreateServerRequest
92-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
92+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
9393
capturedReq = req
9494
return &Server{
9595
ID: "test-server-id",
@@ -107,7 +107,7 @@ var _ = Describe("CreateMachine", func() {
107107

108108
It("should not send KeypairName when empty", func() {
109109
var capturedReq *CreateServerRequest
110-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
110+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
111111
capturedReq = req
112112
return &Server{
113113
ID: "test-server-id",
@@ -135,7 +135,7 @@ var _ = Describe("CreateMachine", func() {
135135
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
136136

137137
var capturedReq *CreateServerRequest
138-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
138+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
139139
capturedReq = req
140140
return &Server{
141141
ID: "test-server-id",
@@ -153,7 +153,7 @@ var _ = Describe("CreateMachine", func() {
153153

154154
It("should not send AvailabilityZone when empty", func() {
155155
var capturedReq *CreateServerRequest
156-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
156+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
157157
capturedReq = req
158158
return &Server{
159159
ID: "test-server-id",
@@ -181,7 +181,7 @@ var _ = Describe("CreateMachine", func() {
181181
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
182182

183183
var capturedReq *CreateServerRequest
184-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
184+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
185185
capturedReq = req
186186
return &Server{
187187
ID: "test-server-id",
@@ -199,7 +199,7 @@ var _ = Describe("CreateMachine", func() {
199199

200200
It("should not send AffinityGroup when empty", func() {
201201
var capturedReq *CreateServerRequest
202-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
202+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
203203
capturedReq = req
204204
return &Server{
205205
ID: "test-server-id",
@@ -227,7 +227,7 @@ var _ = Describe("CreateMachine", func() {
227227
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
228228

229229
var capturedReq *CreateServerRequest
230-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
230+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
231231
capturedReq = req
232232
return &Server{
233233
ID: "test-server-id",
@@ -247,7 +247,7 @@ var _ = Describe("CreateMachine", func() {
247247

248248
It("should not send ServiceAccountMails when empty", func() {
249249
var capturedReq *CreateServerRequest
250-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
250+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
251251
capturedReq = req
252252
return &Server{
253253
ID: "test-server-id",
@@ -276,7 +276,7 @@ var _ = Describe("CreateMachine", func() {
276276
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
277277

278278
var capturedReq *CreateServerRequest
279-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
279+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
280280
capturedReq = req
281281
return &Server{
282282
ID: "test-server-id",
@@ -295,7 +295,7 @@ var _ = Describe("CreateMachine", func() {
295295

296296
It("should not send Agent when nil", func() {
297297
var capturedReq *CreateServerRequest
298-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
298+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
299299
capturedReq = req
300300
return &Server{
301301
ID: "test-server-id",
@@ -325,7 +325,7 @@ var _ = Describe("CreateMachine", func() {
325325
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
326326

327327
var capturedReq *CreateServerRequest
328-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
328+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
329329
capturedReq = req
330330
return &Server{
331331
ID: "test-server-id",
@@ -348,7 +348,7 @@ var _ = Describe("CreateMachine", func() {
348348

349349
It("should not send Metadata when nil", func() {
350350
var capturedReq *CreateServerRequest
351-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
351+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
352352
capturedReq = req
353353
return &Server{
354354
ID: "test-server-id",

pkg/provider/core_create_machine_networking_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ var _ = Describe("CreateMachine - Networking", func() {
8080
}
8181

8282
var capturedReq *CreateServerRequest
83-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
83+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
8484
capturedReq = req
8585
return &Server{
8686
ID: "test-server-id",
@@ -126,7 +126,7 @@ var _ = Describe("CreateMachine - Networking", func() {
126126
}
127127

128128
var capturedReq *CreateServerRequest
129-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
129+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
130130
capturedReq = req
131131
return &Server{
132132
ID: "test-server-id",
@@ -174,7 +174,7 @@ var _ = Describe("CreateMachine - Networking", func() {
174174
}
175175

176176
var capturedReq *CreateServerRequest
177-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
177+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
178178
capturedReq = req
179179
return &Server{
180180
ID: "test-server-id",
@@ -215,7 +215,7 @@ var _ = Describe("CreateMachine - Networking", func() {
215215
}
216216

217217
var capturedReq *CreateServerRequest
218-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
218+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
219219
capturedReq = req
220220
return &Server{
221221
ID: "test-server-id",
@@ -263,7 +263,7 @@ var _ = Describe("CreateMachine - Networking", func() {
263263
}
264264

265265
var capturedReq *CreateServerRequest
266-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
266+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
267267
capturedReq = req
268268
return &Server{
269269
ID: "test-server-id",

pkg/provider/core_create_machine_storage_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ var _ = Describe("CreateMachine", func() {
9898
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
9999

100100
var capturedReq *CreateServerRequest
101-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
101+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
102102
capturedReq = req
103103
return &Server{
104104
ID: "test-server-id",
@@ -132,7 +132,7 @@ var _ = Describe("CreateMachine", func() {
132132
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
133133

134134
var capturedReq *CreateServerRequest
135-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
135+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
136136
capturedReq = req
137137
return &Server{
138138
ID: "test-server-id",
@@ -162,7 +162,7 @@ var _ = Describe("CreateMachine", func() {
162162
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
163163

164164
var capturedReq *CreateServerRequest
165-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
165+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
166166
capturedReq = req
167167
return &Server{
168168
ID: "test-server-id",
@@ -196,7 +196,7 @@ var _ = Describe("CreateMachine", func() {
196196
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
197197

198198
var capturedReq *CreateServerRequest
199-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
199+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
200200
capturedReq = req
201201
return &Server{
202202
ID: "test-server-id",
@@ -216,7 +216,7 @@ var _ = Describe("CreateMachine", func() {
216216

217217
It("should not send volumes when not specified", func() {
218218
var capturedReq *CreateServerRequest
219-
mockClient.createServerFunc = func(ctx context.Context, token, projectID, region string, req *CreateServerRequest) (*Server, error) {
219+
mockClient.createServerFunc = func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) {
220220
capturedReq = req
221221
return &Server{
222222
ID: "test-server-id",

0 commit comments

Comments
 (0)