Skip to content

Commit 5652daa

Browse files
authored
feat: enhanced validation and resiliance
* feat: http timout * chore: validation. halfway done * feat: imageid and projectid format validation
1 parent 6921cce commit 5652daa

File tree

9 files changed

+339
-40
lines changed

9 files changed

+339
-40
lines changed

pkg/provider/apis/validation/validation.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ var keypairNameRegex = regexp.MustCompile(`^[A-Za-z0-9@._-]*$`)
2424
// Basic validation: local-part@domain with reasonable character restrictions
2525
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
2626

27+
// machineTypeRegex is a regex pattern for validating machine type format
28+
// Pattern: lowercase letter(s) followed by digits, dot, then more digits (e.g., c1.2, m1.4, g1a.8)
29+
var machineTypeRegex = regexp.MustCompile(`^[a-z]+\d+\.\d+$`)
30+
31+
// labelKeyRegex validates Kubernetes label keys (must start/end with alphanumeric, can contain -, _, .)
32+
// Maximum length: 63 characters
33+
var labelKeyRegex = regexp.MustCompile(`^[a-zA-Z0-9]([-a-zA-Z0-9_.]*[a-zA-Z0-9])?$`)
34+
35+
// labelValueRegex validates Kubernetes label values (must start/end with alphanumeric, can contain -, _, ., can be empty)
36+
// Maximum length: 63 characters
37+
var labelValueRegex = regexp.MustCompile(`^([a-zA-Z0-9]([-a-zA-Z0-9_.]*[a-zA-Z0-9])?)?$`)
38+
2739
// ValidateProviderSpecNSecret validates provider spec and secret to check if all fields are present and valid
2840
func ValidateProviderSpecNSecret(spec *api.ProviderSpec, secrets *corev1.Secret) []error {
2941
var errors []error
@@ -39,6 +51,8 @@ func ValidateProviderSpecNSecret(spec *api.ProviderSpec, secrets *corev1.Secret)
3951
errors = append(errors, fmt.Errorf("secret must contain 'projectId' field"))
4052
} else if len(projectID) == 0 {
4153
errors = append(errors, fmt.Errorf("secret 'projectId' cannot be empty"))
54+
} else if !isValidUUID(string(projectID)) {
55+
errors = append(errors, fmt.Errorf("secret 'projectId' must be a valid UUID"))
4256
}
4357

4458
// Validate stackitToken (required for authentication)
@@ -52,13 +66,37 @@ func ValidateProviderSpecNSecret(spec *api.ProviderSpec, secrets *corev1.Secret)
5266
// Validate ProviderSpec
5367
if spec.MachineType == "" {
5468
errors = append(errors, fmt.Errorf("providerSpec.machineType is required"))
69+
} else if !isValidMachineType(spec.MachineType) {
70+
errors = append(errors, fmt.Errorf("providerSpec.machineType has invalid format (expected format: c1.2, m1.4, etc.)"))
5571
}
5672

5773
// ImageID is required unless BootVolume.Source is specified
5874
hasBootVolumeSource := spec.BootVolume != nil && spec.BootVolume.Source != nil
5975
if spec.ImageID == "" && !hasBootVolumeSource {
6076
errors = append(errors, fmt.Errorf("providerSpec.imageId or bootVolume.source is required"))
6177
}
78+
// Validate ImageID format if specified
79+
if spec.ImageID != "" && !isValidUUID(spec.ImageID) {
80+
errors = append(errors, fmt.Errorf("providerSpec.imageId must be a valid UUID"))
81+
}
82+
83+
// Validate Labels
84+
if spec.Labels != nil {
85+
for key, value := range spec.Labels {
86+
if len(key) > 63 {
87+
errors = append(errors, fmt.Errorf("providerSpec.labels key '%s' exceeds maximum length of 63 characters", key))
88+
}
89+
if !labelKeyRegex.MatchString(key) {
90+
errors = append(errors, fmt.Errorf("providerSpec.labels key '%s' has invalid format (must start/end with alphanumeric, can contain -, _, .)", key))
91+
}
92+
if len(value) > 63 {
93+
errors = append(errors, fmt.Errorf("providerSpec.labels value for key '%s' exceeds maximum length of 63 characters", key))
94+
}
95+
if !labelValueRegex.MatchString(value) {
96+
errors = append(errors, fmt.Errorf("providerSpec.labels value for key '%s' has invalid format (must start/end with alphanumeric, can contain -, _, ., can be empty)", key))
97+
}
98+
}
99+
}
62100

63101
// Validate Networking
64102
if spec.Networking != nil {
@@ -218,3 +256,8 @@ func isValidUUID(s string) bool {
218256
func isValidEmail(s string) bool {
219257
return emailRegex.MatchString(s)
220258
}
259+
260+
// isValidMachineType checks if a string matches the machine type format
261+
func isValidMachineType(s string) bool {
262+
return machineTypeRegex.MatchString(s)
263+
}

pkg/provider/apis/validation/validation_test.go

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ var _ = Describe("ValidateProviderSpecNSecret", func() {
2626
}
2727
secret = &corev1.Secret{
2828
Data: map[string][]byte{
29-
"projectId": []byte("test-project"),
29+
"projectId": []byte("11111111-2222-3333-4444-555555555555"),
3030
"stackitToken": []byte("test-token"),
3131
},
3232
}
@@ -45,13 +45,33 @@ var _ = Describe("ValidateProviderSpecNSecret", func() {
4545
Expect(errors[0].Error()).To(ContainSubstring("machineType"))
4646
})
4747

48+
It("should fail when MachineType has invalid format", func() {
49+
providerSpec.MachineType = "InvalidFormat"
50+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
51+
Expect(errors).NotTo(BeEmpty())
52+
Expect(errors[0].Error()).To(ContainSubstring("machineType has invalid format"))
53+
})
54+
55+
It("should succeed when MachineType has valid format", func() {
56+
providerSpec.MachineType = "c1.2"
57+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
58+
Expect(errors).To(BeEmpty())
59+
})
60+
4861
It("should fail when ImageID is empty", func() {
4962
providerSpec.ImageID = ""
5063
errors := ValidateProviderSpecNSecret(providerSpec, secret)
5164
Expect(errors).NotTo(BeEmpty())
5265
Expect(errors[0].Error()).To(ContainSubstring("imageId"))
5366
})
5467

68+
It("should fail when ImageID is not a valid UUID", func() {
69+
providerSpec.ImageID = "invalid-uuid"
70+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
71+
Expect(errors).NotTo(BeEmpty())
72+
Expect(errors[0].Error()).To(ContainSubstring("imageId must be a valid UUID"))
73+
})
74+
5575
It("should fail when both required fields are empty", func() {
5676
providerSpec.MachineType = ""
5777
providerSpec.ImageID = ""
@@ -79,6 +99,126 @@ var _ = Describe("ValidateProviderSpecNSecret", func() {
7999
errors := ValidateProviderSpecNSecret(providerSpec, secret)
80100
Expect(errors).To(BeEmpty())
81101
})
102+
103+
It("should succeed with label keys containing allowed characters", func() {
104+
providerSpec.Labels = map[string]string{
105+
"app.kubernetes.io_component": "worker",
106+
"environment-type": "prod",
107+
"version": "v1.2.3",
108+
}
109+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
110+
Expect(errors).To(BeEmpty())
111+
})
112+
113+
It("should succeed with label values containing allowed characters", func() {
114+
providerSpec.Labels = map[string]string{
115+
"env": "production-env_01.test",
116+
"version": "v1.2.3-alpha",
117+
}
118+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
119+
Expect(errors).To(BeEmpty())
120+
})
121+
122+
It("should succeed with empty label value", func() {
123+
providerSpec.Labels = map[string]string{
124+
"optional-label": "",
125+
}
126+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
127+
Expect(errors).To(BeEmpty())
128+
})
129+
130+
It("should fail when label key exceeds 63 characters", func() {
131+
longKey := string(make([]byte, 64))
132+
for i := range longKey {
133+
longKey = longKey[:i] + "a" + longKey[i+1:]
134+
}
135+
providerSpec.Labels = map[string]string{
136+
longKey: "value",
137+
}
138+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
139+
Expect(errors).NotTo(BeEmpty())
140+
Expect(errors[0].Error()).To(ContainSubstring("exceeds maximum length of 63 characters"))
141+
})
142+
143+
It("should fail when label value exceeds 63 characters", func() {
144+
longValue := string(make([]byte, 64))
145+
for i := range longValue {
146+
longValue = longValue[:i] + "a" + longValue[i+1:]
147+
}
148+
providerSpec.Labels = map[string]string{
149+
"key": longValue,
150+
}
151+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
152+
Expect(errors).NotTo(BeEmpty())
153+
Expect(errors[0].Error()).To(ContainSubstring("exceeds maximum length of 63 characters"))
154+
})
155+
156+
It("should fail when label key starts with non-alphanumeric", func() {
157+
providerSpec.Labels = map[string]string{
158+
"-invalid-key": "value",
159+
}
160+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
161+
Expect(errors).NotTo(BeEmpty())
162+
Expect(errors[0].Error()).To(ContainSubstring("invalid format"))
163+
})
164+
165+
It("should fail when label key ends with non-alphanumeric", func() {
166+
providerSpec.Labels = map[string]string{
167+
"invalid-key-": "value",
168+
}
169+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
170+
Expect(errors).NotTo(BeEmpty())
171+
Expect(errors[0].Error()).To(ContainSubstring("invalid format"))
172+
})
173+
174+
It("should fail when label key contains invalid characters", func() {
175+
providerSpec.Labels = map[string]string{
176+
"invalid@key": "value",
177+
}
178+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
179+
Expect(errors).NotTo(BeEmpty())
180+
Expect(errors[0].Error()).To(ContainSubstring("invalid format"))
181+
})
182+
183+
It("should fail when label value starts with non-alphanumeric", func() {
184+
providerSpec.Labels = map[string]string{
185+
"key": "-invalid-value",
186+
}
187+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
188+
Expect(errors).NotTo(BeEmpty())
189+
Expect(errors[0].Error()).To(ContainSubstring("invalid format"))
190+
})
191+
192+
It("should fail when label value ends with non-alphanumeric", func() {
193+
providerSpec.Labels = map[string]string{
194+
"key": "invalid-value-",
195+
}
196+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
197+
Expect(errors).NotTo(BeEmpty())
198+
Expect(errors[0].Error()).To(ContainSubstring("invalid format"))
199+
})
200+
201+
It("should fail when label value contains invalid characters", func() {
202+
providerSpec.Labels = map[string]string{
203+
"key": "invalid@value",
204+
}
205+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
206+
Expect(errors).NotTo(BeEmpty())
207+
Expect(errors[0].Error()).To(ContainSubstring("invalid format"))
208+
})
209+
210+
It("should fail with multiple label validation errors", func() {
211+
longKey := string(make([]byte, 64))
212+
for i := range longKey {
213+
longKey = longKey[:i] + "a" + longKey[i+1:]
214+
}
215+
providerSpec.Labels = map[string]string{
216+
longKey: "value1",
217+
"-invalid-key": "value2",
218+
}
219+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
220+
Expect(errors).To(HaveLen(2))
221+
})
82222
})
83223

84224
Context("Networking validation", func() {
@@ -562,5 +702,12 @@ var _ = Describe("ValidateProviderSpecNSecret", func() {
562702
Expect(errors).NotTo(BeEmpty())
563703
Expect(errors[0].Error()).To(ContainSubstring("projectId"))
564704
})
705+
706+
It("should fail when projectId is not a valid UUID", func() {
707+
secret.Data["projectId"] = []byte("invalid-uuid")
708+
errors := ValidateProviderSpecNSecret(providerSpec, secret)
709+
Expect(errors).NotTo(BeEmpty())
710+
Expect(errors[0].Error()).To(ContainSubstring("projectId' must be a valid UUID"))
711+
})
565712
})
566713
})

0 commit comments

Comments
 (0)