docs/rfc/0003-crd-modernization.md
Modernize Fission's CRDs to match current Kubernetes API conventions:
metav1.Condition arrays to all *Status types, with standard
condition types per CRD; add a FunctionStatus subresource (currently
missing).pkg/apis/core/v1/validation.go to
CEL +kubebuilder:validation:XValidation markers on the types, where
possible. Keep the webhook for cross-resource rules.+listType=map, +listMapKey=name,
+kubebuilder:validation:Immutable) so CRDs work cleanly under SSA.All changes are additive: existing status fields, existing clients, existing CLI behavior all keep working.
kubectl wait --for=condition=Ready function/foo does
not work today. GitOps tools (Argo CD, Flux) can't surface Fission
resource health in their dashboards. Operators can't reconcile on
Fission objects' states because Status is either absent (Function)
or a bare string (CanaryConfig). Conditions are the universal
Kubernetes status contract.pkg/apis/core/v1/validation.go,
~20 functions). They're enforced only by the admission webhook, which
means:
kubectl apply --dry-run=server against a cluster without the
webhook (e.g. early bootstrap) silently accepts junk.kubectl explain with validation,
IDE YAML plugins) see no rules.
CEL rules live in the CRD itself, are enforced by the apiserver, and
are documented in the OpenAPI schema.listType=map, two controllers patching the same
Function spec will clobber each other's entries in Spec.Secrets
(for example). SSA-compliant CRDs are also required for some
Argo CD features and for well-behaved operator reconciliation.FunctionStatus with Conditions []metav1.Condition and promote
Function to use +kubebuilder:subresource:status.Conditions to every existing *Status: PackageStatus,
HTTPTriggerStatus, KubernetesWatchTriggerStatus,
MessageQueueTriggerStatus, TimeTriggerStatus, CanaryConfigStatus.Package.Status.BuildStatus) remain
populated for compat.validation.go to +kubebuilder:validation:XValidation
markers. Hard cases (cross-object references like Package→Environment
existence, RBAC-aware checks) stay in the webhook.validation.go remains as a library usable by the CLI (fission spec validate) for client-side checks.+listType=map + +listMapKey=name to every list-of-objects
field that has an identity.+listType=set to lists of scalars where appropriate.+kubebuilder:validation:Immutable to: Function.Spec.Environment,
Package.Spec.Environment, Environment.Spec.Version.crds/v1/ generated schema has x-kubernetes-list-type and
x-kubernetes-list-map-keys in the right places.// Function
const (
FunctionReady = "Ready"
FunctionPackageReady = "PackageReady" // points at referenced Package
FunctionEnvReady = "EnvironmentReady" // points at referenced Environment
FunctionProgressing = "Progressing" // transient reconcile
)
// Package
const (
PackageBuildSucceeded = "BuildSucceeded" // replaces BuildStatusSucceeded enum (kept in parallel)
PackageReady = "Ready" // composite: BuildSucceeded && URL populated (for tarball) or OCIRef valid (for OCI)
)
// HTTPTrigger
const (
HTTPTriggerRouteAdmitted = "RouteAdmitted" // router accepted the mux entry
HTTPTriggerReady = "Ready"
)
// MessageQueueTrigger
const (
MQTBindingReady = "BindingReady" // mqtmanager or KEDA hooked up
MQTReady = "Ready"
)
// TimeTrigger
const (
TimeTriggerScheduled = "Scheduled"
TimeTriggerReady = "Ready"
)
// KubernetesWatchTrigger
const (
KWatchSubscribed = "Subscribed"
KWatchReady = "Ready"
)
// CanaryConfig
const (
CanaryProgressing = "Progressing"
CanaryReady = "Ready"
)
Each condition uses metav1.Condition with Type, Status, Reason,
Message, LastTransitionTime, ObservedGeneration. Reasons follow
PascalCase convention (e.g. BuildPending, ImagePullFailed,
EnvironmentMissing).
// +kubebuilder:subresource:status
type Function struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec FunctionSpec `json:"spec"`
// +optional
Status FunctionStatus `json:"status,omitempty"`
}
type FunctionStatus struct {
// ObservedGeneration reflects the spec generation last reconciled.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Conditions are the latest observations of a Function's state.
// +optional
// +patchMergeKey=type
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
}
Current in-code check (validation.go:FunctionSpec.Validate):
if fs.Environment.Name == "" {
result = multierror.Append(result, MakeValidationErr(ErrorUnsupportedType, "FunctionSpec.Environment.Name", fs.Environment.Name, "environment name required"))
}
Becomes:
// +kubebuilder:validation:XValidation:rule="self.environment.name != ''",message="environment name required"
type FunctionSpec struct { ... }
More examples:
// Package.Spec.Deployment: exactly one of literal / url / oci
// +kubebuilder:validation:XValidation:rule="[has(self.literal) && self.literal != '', has(self.url) && self.url != '', has(self.oci)].filter(x, x).size() == 1",message="exactly one of literal, url, or oci must be set"
// HTTPTrigger.Spec: host is non-empty when ingress.annotations has cert-manager.io
// +kubebuilder:validation:XValidation:rule="!(has(self.ingressConfig.annotations) && self.ingressConfig.annotations.exists(k, k.startsWith('cert-manager.io'))) || self.host != ''",message="host required when cert-manager annotations present"
// Environment.Spec.Poolsize: non-negative
// +kubebuilder:validation:Minimum=0
// CanaryConfig.Spec.WeightIncrement: 1..100
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=100
Apply to every list of objects:
// FunctionSpec
type FunctionSpec struct {
// +listType=map
// +listMapKey=name
Secrets []SecretReference `json:"secrets,omitempty"`
// +listType=map
// +listMapKey=name
ConfigMaps []ConfigMapReference `json:"configmaps,omitempty"`
// ...
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="environment is immutable"
Environment EnvironmentReference `json:"environment"`
}
make codegen && make generate-crds && make generate-webhooks must run
and produce clean diffs. CI already enforces this; this RFC adds:
make check-cel target that runs kubectl apply --dry-run=server
of a curated set of valid + invalid fixtures against a kind cluster.docs/crd-conditions.md generated from condition constants.Purely additive.
Conditions arrays on statuses — existing clients ignore them.FunctionStatus subresource — existing writers that set .status
via the main resource will get a 422 only if they previously wrote
to .status (which would have been a no-op since there was no status
subresource). All existing writers use the main resource and write
.spec only.fission fn get output gains a new
CONDITIONS column (opt-in via -o wide).*Status.Conditions fields. Add FunctionStatus. Regenerate
CRDs. Ships in v1.N.BuildSucceeded; executor writes EndpointsReady (aligned with
RFC-0002); router writes RouteAdmitted. Ships in v1.N.Immutable markers. Ships in v1.N.validation.go kept for CLI client-side use only. Webhook retains
cross-object rules. v1.(N+1).pkg/conditions/ round-trip; no
duplicate condition types; LastTransitionTime only updates on
status change.kubectl wait --for=condition=Ready function/foo
succeeds after the function's package builds and endpoints are ready.test_conditions.sh
that asserts the full condition timeline for a happy-path function.pkg/apis/core/v1/testdata/; make check-cel applies them via
envtest.test/ssa/test_two_writers.sh.kubectl apply --dry-run=server -f <re-exported>; expect 100%
success rate on any existing resource.conditions fields; need a Helm lookup-free pattern for the CRDs.Function
status itself? It's observational, not desired-state. Lean: keep it
on metrics only, not on Status.Status string — do we keep it forever or deprecate
via release notes? Lean: keep forever (cheap), add conditions
alongside.