Skip to content

Commit 6921cce

Browse files
authored
Feat: metadata, stackit api auth
* chore: renaming e2e test files * feat: metadata * chore: updating e2e test file license headers * chore: comment cleanup * chore: tidy up * feat: stackit api auth
1 parent b5e4db7 commit 6921cce

28 files changed

Lines changed: 444 additions & 301 deletions

pkg/provider/apis/provider_spec.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ type ProviderSpec struct {
7272
// Optional field. The STACKIT agent provides monitoring and management capabilities
7373
// If not specified, defaults to the STACKIT platform default behavior
7474
Agent *AgentSpec `json:"agent,omitempty"`
75+
76+
// Metadata is a generic JSON object for storing arbitrary key-value pairs
77+
// Optional field. Can be used to store custom metadata that doesn't fit into other fields
78+
// Example: {"environment": "production", "cost-center": "12345"}
79+
Metadata map[string]interface{} `json:"metadata,omitempty"`
7580
}
7681

7782
// AgentSpec defines the STACKIT agent configuration for a server

pkg/provider/apis/validation/validation.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ func ValidateProviderSpecNSecret(spec *api.ProviderSpec, secrets *corev1.Secret)
4141
errors = append(errors, fmt.Errorf("secret 'projectId' cannot be empty"))
4242
}
4343

44+
// Validate stackitToken (required for authentication)
45+
stackitToken, ok := secrets.Data["stackitToken"]
46+
if !ok {
47+
errors = append(errors, fmt.Errorf("secret must contain 'stackitToken' field"))
48+
} else if len(stackitToken) == 0 {
49+
errors = append(errors, fmt.Errorf("secret 'stackitToken' cannot be empty"))
50+
}
51+
4452
// Validate ProviderSpec
4553
if spec.MachineType == "" {
4654
errors = append(errors, fmt.Errorf("providerSpec.machineType is required"))
@@ -120,6 +128,10 @@ func ValidateProviderSpecNSecret(spec *api.ProviderSpec, secrets *corev1.Secret)
120128
// Agent is optional with no specific constraints - just a boolean flag
121129
// No validation needed as any value (nil, true, false) is acceptable
122130

131+
// Validate Metadata
132+
// Metadata is optional with no specific constraints - freeform JSON object
133+
// No validation needed as any key-value pairs are acceptable
134+
123135
return errors
124136
}
125137

pkg/provider/apis/validation/validation_test.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ var _ = Describe("ValidateProviderSpecNSecret", func() {
2626
}
2727
secret = &corev1.Secret{
2828
Data: map[string][]byte{
29-
"projectId": []byte("test-project"),
29+
"projectId": []byte("test-project"),
30+
"stackitToken": []byte("test-token"),
3031
},
3132
}
3233
})
@@ -505,6 +506,42 @@ var _ = Describe("ValidateProviderSpecNSecret", func() {
505506
})
506507
})
507508

509+
Context("Metadata validation", func() {
510+
It("should succeed when metadata is nil", func() {
511+
providerSpec.Metadata = nil
512+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
513+
Expect(errors).To(BeEmpty())
514+
})
515+
516+
It("should succeed with empty metadata", func() {
517+
providerSpec.Metadata = map[string]interface{}{}
518+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
519+
Expect(errors).To(BeEmpty())
520+
})
521+
522+
It("should succeed with valid metadata", func() {
523+
providerSpec.Metadata = map[string]interface{}{
524+
"environment": "production",
525+
"cost-center": "12345",
526+
"owner": "team-a",
527+
}
528+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
529+
Expect(errors).To(BeEmpty())
530+
})
531+
532+
It("should succeed with nested metadata objects", func() {
533+
providerSpec.Metadata = map[string]interface{}{
534+
"tags": map[string]interface{}{
535+
"env": "prod",
536+
"tier": "backend",
537+
},
538+
"count": 42,
539+
}
540+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
541+
Expect(errors).To(BeEmpty())
542+
})
543+
})
544+
508545
Context("Secret validation", func() {
509546
It("should fail when secret is nil", func() {
510547
errors := ValidateProviderSpecNSecret(providerSpec, nil)

pkg/provider/core.go

Lines changed: 73 additions & 100 deletions
Large diffs are not rendered by default.

pkg/provider/core_create_machine_test.go

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ var _ = Describe("CreateMachine", func() {
4242
// Create secret with projectId
4343
secret = &corev1.Secret{
4444
Data: map[string][]byte{
45-
"projectId": []byte("test-project-123"),
45+
"projectId": []byte("test-project-123"),
46+
"stackitToken": []byte("test-token-123"),
4647
},
4748
}
4849

@@ -93,7 +94,7 @@ var _ = Describe("CreateMachine", func() {
9394
var capturedReq *CreateServerRequest
9495
var capturedProjectID string
9596

96-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
97+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
9798
capturedProjectID = projectID
9899
capturedReq = req
99100
return &Server{
@@ -163,7 +164,7 @@ var _ = Describe("CreateMachine", func() {
163164

164165
Context("when STACKIT API fails", func() {
165166
It("should return Internal error on API failure", func() {
166-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
167+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
167168
return nil, fmt.Errorf("API connection failed")
168169
}
169170

@@ -187,7 +188,7 @@ var _ = Describe("CreateMachine", func() {
187188
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
188189

189190
var capturedReq *CreateServerRequest
190-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
191+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
191192
capturedReq = req
192193
return &Server{
193194
ID: "test-server-id",
@@ -208,7 +209,7 @@ var _ = Describe("CreateMachine", func() {
208209
secret.Data["userData"] = []byte("#cloud-config\nruncmd:\n - echo 'Hello from Secret'")
209210

210211
var capturedReq *CreateServerRequest
211-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
212+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
212213
capturedReq = req
213214
return &Server{
214215
ID: "test-server-id",
@@ -236,7 +237,7 @@ var _ = Describe("CreateMachine", func() {
236237
secret.Data["userData"] = []byte("#cloud-config from Secret")
237238

238239
var capturedReq *CreateServerRequest
239-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
240+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
240241
capturedReq = req
241242
return &Server{
242243
ID: "test-server-id",
@@ -255,7 +256,7 @@ var _ = Describe("CreateMachine", func() {
255256

256257
It("should not send userData when neither ProviderSpec nor Secret have it", func() {
257258
var capturedReq *CreateServerRequest
258-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
259+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
259260
capturedReq = req
260261
return &Server{
261262
ID: "test-server-id",
@@ -275,7 +276,7 @@ var _ = Describe("CreateMachine", func() {
275276
secret.Data["userData"] = []byte("")
276277

277278
var capturedReq *CreateServerRequest
278-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
279+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
279280
capturedReq = req
280281
return &Server{
281282
ID: "test-server-id",
@@ -312,7 +313,7 @@ var _ = Describe("CreateMachine", func() {
312313
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
313314

314315
var capturedReq *CreateServerRequest
315-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
316+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
316317
capturedReq = req
317318
return &Server{
318319
ID: "test-server-id",
@@ -346,7 +347,7 @@ var _ = Describe("CreateMachine", func() {
346347
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
347348

348349
var capturedReq *CreateServerRequest
349-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
350+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
350351
capturedReq = req
351352
return &Server{
352353
ID: "test-server-id",
@@ -376,7 +377,7 @@ var _ = Describe("CreateMachine", func() {
376377
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
377378

378379
var capturedReq *CreateServerRequest
379-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
380+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
380381
capturedReq = req
381382
return &Server{
382383
ID: "test-server-id",
@@ -410,7 +411,7 @@ var _ = Describe("CreateMachine", func() {
410411
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
411412

412413
var capturedReq *CreateServerRequest
413-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
414+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
414415
capturedReq = req
415416
return &Server{
416417
ID: "test-server-id",
@@ -430,7 +431,7 @@ var _ = Describe("CreateMachine", func() {
430431

431432
It("should not send volumes when not specified", func() {
432433
var capturedReq *CreateServerRequest
433-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
434+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
434435
capturedReq = req
435436
return &Server{
436437
ID: "test-server-id",
@@ -459,7 +460,7 @@ var _ = Describe("CreateMachine", func() {
459460
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
460461

461462
var capturedReq *CreateServerRequest
462-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
463+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
463464
capturedReq = req
464465
return &Server{
465466
ID: "test-server-id",
@@ -477,7 +478,7 @@ var _ = Describe("CreateMachine", func() {
477478

478479
It("should not send KeypairName when empty", func() {
479480
var capturedReq *CreateServerRequest
480-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
481+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
481482
capturedReq = req
482483
return &Server{
483484
ID: "test-server-id",
@@ -505,7 +506,7 @@ var _ = Describe("CreateMachine", func() {
505506
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
506507

507508
var capturedReq *CreateServerRequest
508-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
509+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
509510
capturedReq = req
510511
return &Server{
511512
ID: "test-server-id",
@@ -523,7 +524,7 @@ var _ = Describe("CreateMachine", func() {
523524

524525
It("should not send AvailabilityZone when empty", func() {
525526
var capturedReq *CreateServerRequest
526-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
527+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
527528
capturedReq = req
528529
return &Server{
529530
ID: "test-server-id",
@@ -551,7 +552,7 @@ var _ = Describe("CreateMachine", func() {
551552
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
552553

553554
var capturedReq *CreateServerRequest
554-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
555+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
555556
capturedReq = req
556557
return &Server{
557558
ID: "test-server-id",
@@ -569,7 +570,7 @@ var _ = Describe("CreateMachine", func() {
569570

570571
It("should not send AffinityGroup when empty", func() {
571572
var capturedReq *CreateServerRequest
572-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
573+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
573574
capturedReq = req
574575
return &Server{
575576
ID: "test-server-id",
@@ -597,7 +598,7 @@ var _ = Describe("CreateMachine", func() {
597598
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
598599

599600
var capturedReq *CreateServerRequest
600-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
601+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
601602
capturedReq = req
602603
return &Server{
603604
ID: "test-server-id",
@@ -617,7 +618,7 @@ var _ = Describe("CreateMachine", func() {
617618

618619
It("should not send ServiceAccountMails when empty", func() {
619620
var capturedReq *CreateServerRequest
620-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
621+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
621622
capturedReq = req
622623
return &Server{
623624
ID: "test-server-id",
@@ -646,7 +647,7 @@ var _ = Describe("CreateMachine", func() {
646647
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
647648

648649
var capturedReq *CreateServerRequest
649-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
650+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
650651
capturedReq = req
651652
return &Server{
652653
ID: "test-server-id",
@@ -665,7 +666,7 @@ var _ = Describe("CreateMachine", func() {
665666

666667
It("should not send Agent when nil", func() {
667668
var capturedReq *CreateServerRequest
668-
mockClient.createServerFunc = func(ctx context.Context, projectID string, req *CreateServerRequest) (*Server, error) {
669+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
669670
capturedReq = req
670671
return &Server{
671672
ID: "test-server-id",
@@ -680,5 +681,58 @@ var _ = Describe("CreateMachine", func() {
680681
Expect(capturedReq).NotTo(BeNil())
681682
Expect(capturedReq.Agent).To(BeNil())
682683
})
684+
685+
It("should pass Metadata to API when specified", func() {
686+
providerSpec := &api.ProviderSpec{
687+
MachineType: "c1.2",
688+
ImageID: "image-uuid-123",
689+
Metadata: map[string]interface{}{
690+
"environment": "production",
691+
"cost-center": "12345",
692+
"count": 42,
693+
},
694+
}
695+
providerSpecRaw, _ := encodeProviderSpec(providerSpec)
696+
req.MachineClass.ProviderSpec.Raw = providerSpecRaw
697+
698+
var capturedReq *CreateServerRequest
699+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
700+
capturedReq = req
701+
return &Server{
702+
ID: "test-server-id",
703+
Name: req.Name,
704+
Status: "CREATING",
705+
}, nil
706+
}
707+
708+
_, err := provider.CreateMachine(ctx, req)
709+
710+
Expect(err).NotTo(HaveOccurred())
711+
Expect(capturedReq).NotTo(BeNil())
712+
Expect(capturedReq.Metadata).NotTo(BeNil())
713+
Expect(capturedReq.Metadata).To(HaveLen(3))
714+
Expect(capturedReq.Metadata["environment"]).To(Equal("production"))
715+
Expect(capturedReq.Metadata["cost-center"]).To(Equal("12345"))
716+
// JSON marshaling converts int to float64
717+
Expect(capturedReq.Metadata["count"]).To(BeNumerically("==", 42))
718+
})
719+
720+
It("should not send Metadata when nil", func() {
721+
var capturedReq *CreateServerRequest
722+
mockClient.createServerFunc = func(ctx context.Context, token, projectID string, req *CreateServerRequest) (*Server, error) {
723+
capturedReq = req
724+
return &Server{
725+
ID: "test-server-id",
726+
Name: req.Name,
727+
Status: "CREATING",
728+
}, nil
729+
}
730+
731+
_, err := provider.CreateMachine(ctx, req)
732+
733+
Expect(err).NotTo(HaveOccurred())
734+
Expect(capturedReq).NotTo(BeNil())
735+
Expect(capturedReq.Metadata).To(BeNil())
736+
})
683737
})
684738
})

0 commit comments

Comments
 (0)