Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3081,6 +3081,11 @@ spec:
x-kubernetes-list-type: map
type: object
type: object
x-kubernetes-validations:
- message: metadata.name must match RFC 1035 DNS label format
rule: self.metadata.name.matches('^[a-z]([-a-z0-9]*[a-z0-9])?$')
- message: metadata.name must be no more than 63 characters
rule: size(self.metadata.name) <= 63
served: true
storage: true
subresources:
Expand Down
5 changes: 5 additions & 0 deletions manifests/base/crds/trainer.kubeflow.org_trainjobs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3081,6 +3081,11 @@ spec:
x-kubernetes-list-type: map
type: object
type: object
x-kubernetes-validations:
- message: metadata.name must match RFC 1035 DNS label format
rule: self.metadata.name.matches('^[a-z]([-a-z0-9]*[a-z0-9])?$')
- message: metadata.name must be no more than 63 characters
rule: size(self.metadata.name) <= 63
served: true
storage: true
subresources:
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/trainer/v1alpha1/trainjob_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const (
// +kubebuilder:storageversion
// +kubebuilder:printcolumn:name="State",type=string,JSONPath=`.status.conditions[-1:].type`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
// +kubebuilder:validation:XValidation:rule="self.metadata.name.matches('^[a-z]([-a-z0-9]*[a-z0-9])?$')", message="metadata.name must match RFC 1035 DNS label format"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would using !format.dns1123Label().validate(self.metadata.name).hasValue() be slightly better?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion @astefanutti ! I initially tried using the format library expression as recommended in the Kubernetes docs (e.g., !format.dns1035Label().validate(self.metadata.name).hasValue()), but it failed to pass the CEL cost estimation and could not be applied due to cost budget errors in the CRD. Hence I changed to regex check

Please let me know if you'd like me to try any other alternatives or if this existing approach works for now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@juniemariam thanks for the explanation. That looks good to me as it is.

// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) <= 63", message="metadata.name must be no more than 63 characters"

// TrainJob represents configuration of a training job.
type TrainJob struct {
Expand Down
11 changes: 1 addition & 10 deletions pkg/webhooks/trainjob_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ import (
"fmt"

apiruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook"
Expand Down Expand Up @@ -52,20 +50,13 @@ func (w *TrainJobWebhook) ValidateCreate(ctx context.Context, obj apiruntime.Obj
log := ctrl.LoggerFrom(ctx).WithName("trainJob-webhook")
log.V(5).Info("Validating create", "TrainJob", klog.KObj(trainJob))

allErrs := field.ErrorList{}

// Check RFC 1035 name validation errors
for _, err := range validation.IsDNS1035Label(trainJob.Name) {
allErrs = append(allErrs, field.Invalid(field.NewPath("metadata", "name"), trainJob.Name, err))
}

runtimeRefGK := runtime.RuntimeRefToRuntimeRegistryKey(trainJob.Spec.RuntimeRef)
runtime, ok := w.runtimes[runtimeRefGK]
if !ok {
return nil, fmt.Errorf("unsupported runtime: %s", runtimeRefGK)
}
warnings, errors := runtime.ValidateObjects(ctx, nil, trainJob)
return warnings, append(allErrs, errors...).ToAggregate()
return warnings, errors.ToAggregate()
}

func (w *TrainJobWebhook) ValidateUpdate(ctx context.Context, oldObj apiruntime.Object, newObj apiruntime.Object) (admission.Warnings, error) {
Expand Down
13 changes: 0 additions & 13 deletions pkg/webhooks/trainjob_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,6 @@ func TestValidateCreate(t *testing.T) {
wantError: nil,
wantWarnings: nil,
},
"invalid trainjob name with uppercase letters": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had assumed this test would/could be kept?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@astefanutti Just to clarify—now that the validation is enforced in the CRD using CEL, do we still need to keep the Go/webhook logic and its related unit tests?

I moved the tests out of the webhook layer because the validation logic now lives in the CRD, so the webhook no longer performs this check.

Could you please advise whether it’s better to keep both the Go logic and tests (alongside the CEL rule and integration test), or just rely on CEL + integration tests?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@juniemariam that makes sense. Yes an integration test could be useful.

obj: testingutil.MakeTrainJobWrapper("default", "Invalid-job-name").
RuntimeRef(trainer.SchemeGroupVersion.WithKind(trainer.ClusterTrainingRuntimeKind), "test-runtime").
Obj(),
wantError: field.ErrorList{
&field.Error{
Type: field.ErrorTypeInvalid,
Field: "metadata.name",
BadValue: "Invalid-job-name",
},
},
wantWarnings: nil,
},
"unsupported runtime": {
obj: testingutil.MakeTrainJobWrapper("default", "valid-job-name").
RuntimeRef(trainer.SchemeGroupVersion.WithKind(trainer.ClusterTrainingRuntimeKind), "unsupported-runtime").
Expand Down
48 changes: 48 additions & 0 deletions test/integration/webhooks/trainjob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,54 @@ var _ = ginkgo.Describe("TrainJob Webhook", ginkgo.Ordered, func() {
},
testingutil.BeForbiddenError()),
)
ginkgo.DescribeTable("RFC1035-compliant TrainJob name validation", func(trainJob func() *trainer.TrainJob, errorMatcher gomega.OmegaMatcher) {
gomega.Expect(k8sClient.Create(ctx, trainJob())).Should(errorMatcher)
},
ginkgo.Entry("Should succeed to create TrainJob with valid RFC1035-compliant name",
func() *trainer.TrainJob {
return testingutil.MakeTrainJobWrapper(ns.Name, "valid-job-name").
RuntimeRef(trainer.SchemeGroupVersion.WithKind(trainer.ClusterTrainingRuntimeKind), runtimeName).
Obj()
},
gomega.Succeed()),
ginkgo.Entry("Should succeed to create TrainJob with name exactly 63 characters",
func() *trainer.TrainJob {
return testingutil.MakeTrainJobWrapper(ns.Name,
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk"). // 63 chars
RuntimeRef(trainer.SchemeGroupVersion.WithKind(trainer.ClusterTrainingRuntimeKind), runtimeName).
Obj()
},
gomega.Succeed()),
ginkgo.Entry("Should fail to create TrainJob with uppercase letters in name",
func() *trainer.TrainJob {
return testingutil.MakeTrainJobWrapper(ns.Name, "Invalid-job-name").
RuntimeRef(trainer.SchemeGroupVersion.WithKind(trainer.ClusterTrainingRuntimeKind), runtimeName).
Obj()
},
testingutil.BeInvalidError()),
ginkgo.Entry("Should fail to create TrainJob starting with a digit",
func() *trainer.TrainJob {
return testingutil.MakeTrainJobWrapper(ns.Name, "1jobname").
RuntimeRef(trainer.SchemeGroupVersion.WithKind(trainer.ClusterTrainingRuntimeKind), runtimeName).
Obj()
},
testingutil.BeInvalidError()),
ginkgo.Entry("Should fail to create TrainJob ending with a hyphen",
func() *trainer.TrainJob {
return testingutil.MakeTrainJobWrapper(ns.Name, "jobname-").
RuntimeRef(trainer.SchemeGroupVersion.WithKind(trainer.ClusterTrainingRuntimeKind), runtimeName).
Obj()
},
testingutil.BeInvalidError()),
ginkgo.Entry("Should fail to create TrainJob with name exceeding 63 characters",
func() *trainer.TrainJob {
return testingutil.MakeTrainJobWrapper(ns.Name,
"this-name-is-way-too-long-for-a-rfc1035-label-and-should-fail-validation").
RuntimeRef(trainer.SchemeGroupVersion.WithKind(trainer.ClusterTrainingRuntimeKind), runtimeName).
Obj()
},
testingutil.BeInvalidError()),
)
})
})

Expand Down
Loading