cozystack/pkg/apis/apps/validation/validation.go
Aleksei Sviridkin ac1132e16a
test(api): address review round 4 findings
Four follow-ups from review round 4:

1. BLOCKER: the Update(forceAllowCreate=true) path delegates to Create() when the object does not yet exist (rest.go:452) — the typical kubectl apply upsert flow. Add TestUpdate_ForceAllowCreate_RejectsTenantDashName using a fake client so a future refactor of that delegation cannot silently bypass the tenant name check that r.validateNameFormat alone cannot catch.

2. BLOCKER: the e2e BATS test used || true inside the command substitution, which swallowed the kubectl exit code. Rework the test to capture exit code and stdout+stderr explicitly, then assert the exit code is non-zero before asserting on the error message. This distinguishes validation-success (kubectl exit 0 — regression) from environmental failures (exit non-zero but wrong message) from the happy path.

3. Extract TenantKind = "Tenant" as a named constant in the validation package with a comment pointing at the upstream ApplicationDefinition source of truth, and switch the kindName check to use it.

4. Add a clarifying comment on TestValidateApplicationName_TenantLengthFallthrough that it pins an architectural layering decision and is not a user-facing requirement, so a future promotion of tenant length into tenant-specific wording is a legitimate change rather than a test regression.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-12 14:17:12 +03:00

75 lines
3.2 KiB
Go

/*
Copyright 2024 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validation
import (
"regexp"
k8svalidation "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// TenantKind is the Application.Kind string that gates the tenant-specific
// name rules below. It must stay in sync with the `kind` field of the tenant
// ApplicationDefinition (packages/system/tenant-rd/cozyrds/tenant.yaml) which
// is the upstream source the aggregated API reads at startup via
// config.Application.Kind.
const TenantKind = "Tenant"
// tenantNameRegex enforces alphanumeric-only tenant names that begin with a
// lowercase letter. This is stricter than DNS-1035 because the tenant Helm
// chart's tenant.name helper (packages/apps/tenant/templates/_helpers.tpl)
// splits Release.Name on "-" and fails unless the result is exactly
// ["tenant", "<name>"]. Any dash inside <name> would break that invariant at
// Helm template time, so the aggregated API must reject such names up-front
// with a specific error. Requiring a leading letter (rather than letting
// leading-digit names fall through to DNS-1035) keeps the error message
// tenant-specific for all invalid inputs.
var tenantNameRegex = regexp.MustCompile(`^[a-z][a-z0-9]*$`)
// ValidateApplicationName validates that an Application name is acceptable for
// the given kind. All applications must conform to DNS-1035 because their
// names are used to create Kubernetes resources (Services, Namespaces, etc.)
// that require DNS-1035 compliance. Tenant applications additionally must be
// alphanumeric and begin with a lowercase letter because of the Helm chart
// constraint described on tenantNameRegex.
// Note: length validation is handled separately by validateNameLength in the
// REST handler, which computes dynamic limits based on Helm release prefix.
func ValidateApplicationName(name, kindName string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(name) == 0 {
allErrs = append(allErrs, field.Required(fldPath, "name is required"))
return allErrs
}
// Tenant names must be alphanumeric starting with a letter — see
// tenantNameRegex comment for the reason. Check before DNS-1035 so the
// error message is specific to the tenant contract, not the generic DNS
// label rules.
if kindName == TenantKind && !tenantNameRegex.MatchString(name) {
allErrs = append(allErrs, field.Invalid(fldPath, name,
"tenant names must start with a lowercase letter and contain only lowercase letters and digits; dashes are not allowed"))
return allErrs
}
for _, msg := range k8svalidation.IsDNS1035Label(name) {
allErrs = append(allErrs, field.Invalid(fldPath, name, msg))
}
return allErrs
}