Skip to content

Commit ef62a45

Browse files
committed
[feat] validate application name lenth
Signed-off-by: Arsen Gumin <gumin@live.ru>
1 parent 44972be commit ef62a45

File tree

6 files changed

+182
-4
lines changed

6 files changed

+182
-4
lines changed

internal/webhook/scheduledsparkapplication_validator.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func (v *ScheduledSparkApplicationValidator) ValidateCreate(ctx context.Context,
4545
return nil, nil
4646
}
4747
logger.Info("Validating SchedulingSparkApplication create", "name", app.Name, "namespace", app.Namespace)
48-
if err := v.validate(app); err != nil {
48+
if err := v.validate(ctx, app); err != nil {
4949
return nil, err
5050
}
5151
return nil, nil
@@ -58,7 +58,7 @@ func (v *ScheduledSparkApplicationValidator) ValidateUpdate(ctx context.Context,
5858
return nil, nil
5959
}
6060
logger.Info("Validating SchedulingSparkApplication update", "name", newApp.Name, "namespace", newApp.Namespace)
61-
if err := v.validate(newApp); err != nil {
61+
if err := v.validate(ctx, newApp); err != nil {
6262
return nil, err
6363
}
6464
return nil, nil
@@ -74,7 +74,11 @@ func (v *ScheduledSparkApplicationValidator) ValidateDelete(ctx context.Context,
7474
return nil, nil
7575
}
7676

77-
func (v *ScheduledSparkApplicationValidator) validate(_ *v1beta2.ScheduledSparkApplication) error {
78-
// TODO: implement validate logic
77+
func (v *ScheduledSparkApplicationValidator) validate(ctx context.Context, app *v1beta2.ScheduledSparkApplication) error {
78+
meta := app.ObjectMeta
79+
err := validateNameLength(ctx, meta)
80+
if err != nil {
81+
return err
82+
}
7983
return nil
8084
}

internal/webhook/sparkapplication_validator.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ import (
3434
// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook.
3535
// +kubebuilder:webhook:admissionReviewVersions=v1,failurePolicy=fail,groups=sparkoperator.k8s.io,matchPolicy=Exact,mutating=false,name=validate-sparkapplication.sparkoperator.k8s.io,path=/validate-sparkoperator-k8s-io-v1beta2-sparkapplication,reinvocationPolicy=Never,resources=sparkapplications,sideEffects=None,verbs=create;update,versions=v1beta2,webhookVersions=v1
3636

37+
const (
38+
// 63 (Kubernetes limit) - 10 (max suffix length, e.g., "-driver-svc")
39+
maxAppNameLength = 53
40+
)
41+
3742
type SparkApplicationValidator struct {
3843
client client.Client
3944

@@ -58,6 +63,11 @@ func (v *SparkApplicationValidator) ValidateCreate(ctx context.Context, obj runt
5863
return nil, nil
5964
}
6065
logger.Info("Validating SparkApplication create", "name", app.Name, "namespace", app.Namespace, "state", util.GetApplicationState(app))
66+
67+
if err := v.validateMetadata(ctx, app); err != nil {
68+
return nil, err
69+
}
70+
6171
if err := v.validateSpec(ctx, app); err != nil {
6272
return nil, err
6373
}
@@ -90,6 +100,10 @@ func (v *SparkApplicationValidator) ValidateUpdate(ctx context.Context, oldObj r
90100
return nil, nil
91101
}
92102

103+
if err := v.validateMetadata(ctx, newApp); err != nil {
104+
return nil, err
105+
}
106+
93107
if err := v.validateSpec(ctx, newApp); err != nil {
94108
return nil, err
95109
}
@@ -185,3 +199,12 @@ func (v *SparkApplicationValidator) validateResourceUsage(ctx context.Context, a
185199

186200
return nil
187201
}
202+
203+
func (v *SparkApplicationValidator) validateMetadata(ctx context.Context, app *v1beta2.SparkApplication) error {
204+
meta := app.ObjectMeta
205+
err := validateNameLength(ctx, meta)
206+
if err != nil {
207+
return err
208+
}
209+
return nil
210+
}

internal/webhook/webhook.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ limitations under the License.
1717
package webhook
1818

1919
import (
20+
"context"
21+
"fmt"
22+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+
"k8s.io/apimachinery/pkg/util/validation/field"
2024
ctrl "sigs.k8s.io/controller-runtime"
2125
)
2226

@@ -35,3 +39,19 @@ type Options struct {
3539
WebhookMetricsBindAddress string
3640
EnableResourceQuotaEnforcement bool
3741
}
42+
43+
// validateNameLength checks if the application name exceeds the limit for Kubernetes labels.
44+
// The RFC 1123 DNS label regex is handled by Kubernetes itself,
45+
// we only need to check the length here to provide a more specific
46+
// and early error message.
47+
func validateNameLength(_ context.Context, appMeta metav1.ObjectMeta) error {
48+
var allErrs field.ErrorList
49+
if len(appMeta.Name) > maxAppNameLength {
50+
allErrs = append(allErrs, field.Invalid(field.NewPath("metadata", "name"), appMeta.Name,
51+
fmt.Sprintf("name must be no more than %d characters to allow for resource suffixes", maxAppNameLength)))
52+
}
53+
if allErrs != nil {
54+
return allErrs.ToAggregate()
55+
}
56+
return nil
57+
}

internal/webhook/webhook_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package webhook
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"testing"
8+
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
)
11+
12+
// Note: The maxAppNameLength constant must also be defined in your main code.
13+
// We define it here for the test. The value 63 is a common length
14+
// limit for labels in Kubernetes.
15+
16+
func TestValidateNameLength(t *testing.T) {
17+
// Define test cases
18+
testCases := []struct {
19+
name string
20+
appName string
21+
expectError bool
22+
errorMsg string
23+
}{
24+
{
25+
name: "name with valid length",
26+
appName: "my-awesome-application",
27+
expectError: false,
28+
},
29+
{
30+
name: "empty name",
31+
appName: "",
32+
expectError: false, // Length is 0, which is less than the limit
33+
},
34+
{
35+
name: "name at max length boundary",
36+
appName: strings.Repeat("a", maxAppNameLength),
37+
expectError: false,
38+
},
39+
{
40+
name: "name exceeding max length by 1 character",
41+
appName: strings.Repeat("a", maxAppNameLength+1),
42+
expectError: true,
43+
errorMsg: fmt.Sprintf("metadata.name: Invalid value: %q: name must be no more than %d characters to allow for resource suffixes", strings.Repeat("a", maxAppNameLength+1), maxAppNameLength),
44+
},
45+
{
46+
name: "long name significantly over the limit",
47+
appName: "this-is-a-very-very-very-long-application-name-that-definitely-exceeds-the-kubernetes-label-length-limit",
48+
expectError: true,
49+
errorMsg: fmt.Sprintf("metadata.name: Invalid value: %q: name must be no more than %d characters to allow for resource suffixes", "this-is-a-very-very-very-long-application-name-that-definitely-exceeds-the-kubernetes-label-length-limit", maxAppNameLength),
50+
},
51+
}
52+
53+
// Run tests in a loop
54+
for _, tc := range testCases {
55+
t.Run(tc.name, func(t *testing.T) {
56+
// Prepare test data
57+
appMeta := metav1.ObjectMeta{
58+
Name: tc.appName,
59+
}
60+
61+
// Call the function under test
62+
err := validateNameLength(context.Background(), appMeta)
63+
64+
// Check the result
65+
if tc.expectError {
66+
if err == nil {
67+
t.Errorf("expected an error but got nil")
68+
return
69+
}
70+
if err.Error() != tc.errorMsg {
71+
t.Errorf("expected error message:\n%q\ngot:\n%q", tc.errorMsg, err.Error())
72+
}
73+
} else {
74+
if err != nil {
75+
t.Errorf("unexpected error: %v", err)
76+
}
77+
}
78+
})
79+
}
80+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
apiVersion: sparkoperator.k8s.io/v1beta2
2+
kind: SparkApplication
3+
metadata:
4+
name: this-is-a-very-very-very-long-application-name-that-exceeds-the-limit
5+
namespace: default
6+
spec:
7+
type: Scala
8+
mode: cluster
9+
image: docker.io/library/spark:4.0.0
10+
imagePullPolicy: IfNotPresent
11+
mainClass: org.apache.spark.examples.SparkPi
12+
mainApplicationFile: local:///opt/spark/examples/jars/spark-examples.jar
13+
arguments:
14+
- "5000"
15+
sparkVersion: 4.0.0
16+
driver:
17+
labels:
18+
version: 4.0.0
19+
cores: 1
20+
memory: 512m
21+
serviceAccount: spark-operator-spark
22+
executor:
23+
labels:
24+
version: 4.0.0
25+
instances: 1
26+
cores: 1
27+
memory: 512m

test/e2e/sparkapplication_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,4 +393,28 @@ var _ = Describe("Example SparkApplication", func() {
393393
Expect(strings.Contains(string(bytes), "Pi is roughly 3")).To(BeTrue())
394394
})
395395
})
396+
397+
Context("long-name-validation", func() {
398+
ctx := context.Background()
399+
path := filepath.Join("bad_examples", "long-name-validation.yaml")
400+
app := &v1beta2.SparkApplication{}
401+
402+
BeforeEach(func() {
403+
By("Parsing SparkApplication from file")
404+
file, err := os.Open(path)
405+
Expect(err).NotTo(HaveOccurred())
406+
Expect(file).NotTo(BeNil())
407+
408+
decoder := yaml.NewYAMLOrJSONDecoder(file, 100)
409+
Expect(decoder).NotTo(BeNil())
410+
Expect(decoder.Decode(app)).NotTo(HaveOccurred())
411+
})
412+
413+
It("Should be rejected by the webhook due to name length exceeding the limit", func() {
414+
By("Attempting to create SparkApplication with a name that exceeds the maximum length")
415+
err := k8sClient.Create(ctx, app)
416+
Expect(err).To(HaveOccurred())
417+
Expect(err.Error()).To(ContainSubstring("name must be no more than 53 characters"))
418+
})
419+
})
396420
})

0 commit comments

Comments
 (0)