Skip to content

feat: namespace-scoped application family - #77

Open
drey wants to merge 128 commits into
mainfrom
feat/ns-scoped-applications
Open

drey wants to merge 128 commits into
mainfrom
feat/ns-scoped-applications

Conversation

@drey

@drey drey commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a namespace-scoped resource family so a namespace owner can install a Helm chart without cluster-wide rights: five new CRDs (HelmApplication, HelmApplicationRepository, HelmApplicationChart, HelmClusterApplicationRepository, HelmClusterApplicationChart) and their controllers. The existing addon code is generalized rather than copied — both families now run through the same reconcilers.

Why

Installing a chart required cluster-admin, because every resource the module owns is cluster-scoped. A namespace owner needs a way to install charts in their own namespace without gaining access to the whole cluster.

Two requirements shaped the design. A release must be applied under a bounded identity, or a chart carrying a cluster-scoped object would escape the namespace boundary. And the new family must not become a second copy of the addon code, which lives on just as long and would have to be edited in both places.

Key changes

API (api/v1alpha1, api/naming, crds/). Five new CRDs with their Russian doc counterparts. RepositorySpec/RepositoryStatus and the chart catalog status are now shared types: all eight CRDs generate byte-identical to before, except one description line in the addon catalog. The catalog naming scheme moved to api/naming and became injective: the hash now covers the repository and chart joined by a byte no name can hold, so two pairs can no longer produce one object name.

Controller (images/operator-helm-controller/internal). Domain contracts in internal/source (repository, catalog, release, access) with a per-kind adapter in internal/adapter. The catalogs collapsed into one generic internal/catalog package, and the repository and release reconcilers are now shared. Adds controllers for the five new kinds and a HelmApplication webhook. A transitional step renames existing catalog objects to the current scheme on every repository reconcile, independent of the remote, carrying their status over before the old object is deleted.

Release identity (internal/services/access_service.go). Each application gets a token-less ServiceAccount in d8-operator-helm, a Role seeded in the application's namespace with full rights inside it, and a RoleBinding. The Role is created once and never read back, so a namespace owner may narrow it afterwards. The internal HelmRelease carries spec.serviceAccountName and spec.storageNamespace, and the release storage lives in the application's namespace.

Chart values service (images/chart-values-controller/internal). A repository kind is described by one family value instead of branching. The HTTP contract gained a namespace field, and authorization is decided per family: permission to create a HelmClusterAddon for addons, permission to create a HelmApplication in the requested namespace for applications. The namespace is part of a chart's identity only for a namespaced family; for a cluster-scoped one it is only the authorization context. Auxiliary object names now key on the repository namespace, and the cluster-scoped name shape is unchanged.

Templates (templates/). RBAC for both controllers, the webhook configuration and the ValidatingAdmissionPolicy cover the new kinds. The controller's rights over roles are narrowed to create, escalate and bind, since the Role is only ever seeded and never read back. Seven user-facing ClusterRoles are added for both Deckhouse role models, so a namespace owner can reach the new kinds at all: write access sits at admin level, the controller-written chart catalogs are read-only, and the cluster-scoped repository stays with cluster editors. The admission policy now also closes the application and application-repository statuses, which the seeded Role can otherwise reach.

Tests and docs. New e2e suites helmapplication and helmapplicationrepository. The derived-name scheme is mirrored in tests/e2e/internal/naming (the operator's internal package cannot be imported) and tied to the operator's own implementation by twin tests. README and EXAMPLE in both languages describe the new family and correct the claim that cluster-admin is required. CI gained a unit-test workflow and a generated-files check, and the Go lint job is enabled again now that the linter is built from source rather than downloaded.

Review focus / risks

  • Every chart catalog object is renamed, the released addon ones included. The name a repository and chart pair produces was not injective, so two repositories could fight over one catalog object; the hash now covers the pair joined by a byte no name can hold. Existing objects therefore move. On the first reconcile after the upgrade each repository recreates its catalog objects under the new name, carries the old status over — a version a consumer still holds keeps its RemovedFromRepository marker and media type — and deletes the old object. The rename runs before anything touches the remote, so a repository not yet due for a sync, or one whose registry is gone for good, still migrates. Everything about it is marked TRANSITIONAL and comes out once every cluster has reconciled once: MigrateNames in internal/catalog/catalog.go, its interface entry, and Inputs.MigrateErr.
  • Impersonation is never actually exercised. The tests confirm serviceAccountName and storageNamespace are set, but the werf/3p-helm-controller fork holds full rights itself: an implementation honouring the storage namespace while ignoring the service account would pass every check in this branch. Proving it needs a chart containing a cluster-scoped object and an assertion of Ready=False with forbidden. This is the one load-bearing assumption no test closes.
  • Creating a HelmApplication is effectively namespace-admin. The seeded Role grants */*/* within the namespace. It is a deliberate decision, documented, but worth confirming the boundary is acceptable.
  • The user-facing roles rest on review alone. Seven ClusterRoles cover both Deckhouse role models. Nothing checks them automatically: the module linter skips templates/rbacv2/** entirely, its wildcard rule only reads files named rbac-for-us.yaml, and its classic-model rule verifies the name and the access-level annotation but not the rules. No e2e exercises the aggregation chain either. Worth reading the labels against the upstream selectors by hand, in particular that write access lands at admin level and never at an editor one.
  • The released addon family. No expectation changed in the thirteen addon test files touched; conditions, reasons, requeues, finalizers and the HTTP contract match. Three deliberate differences: catalog object names move as above, an addon release name longer than 53 characters is now truncated instead of being rejected by Helm, and three catalog error messages were reworded. Every other derived name is frozen and pinned by a test.
  • The namespace divergence for helmclusterapplicationrepository. The request must carry a namespace and is authorized against it, but the namespace is dropped when the chart is resolved. Worth confirming the divergence is genuinely not exploitable.
  • Repository isolation is only partly proven. The e2e creates same-named repositories in two namespaces but with the same URL, so their chart sets coincide: a controller pruning objects by label without a namespace restriction would still pass that test.

drey added 30 commits September 10, 2026 11:53
Signed-off-by: Ilya Drey <ilya.drey@flant.com>
Signed-off-by: Ilya Drey <ilya.drey@flant.com>
Signed-off-by: Ilya Drey <ilya.drey@flant.com>
Move LabelRepositoryName, LabelChartName and the UnavailableReason*
constants out of helm_cluster_addon_chart.go into constants.go, since
they are now shared by three chart-catalog families instead of one.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
Signed-off-by: Ilya Drey <ilya.drey@flant.com>
Signed-off-by: Ilya Drey <ilya.drey@flant.com>
…elpers

Bound spec.metadata.name to 63 characters via a CEL rule: the controller
writes the object's name into a source label value on every internal
resource it creates (chart_service.go, release_service.go,
oci_repo_service.go, chart_claim_service.go), and Kubernetes caps label
values at 63 characters. The kind is unreleased, so tightening it now is
free; closing it after release would be a breaking change. No lower bound
is added, since nothing references a HelmApplication by name.

Added RepositoryName() and RepositoryKind() on
*HelmApplicationLastAppliedChartRef, mirroring the existing helpers on
HelmApplication's spec, so callers that need "which repository did we
last deploy from" (uninstall, and the namespaced-to-cluster migration
IsChartStatusInfoOutdated detects) don't have to re-implement the
repository/clusterRepository XOR by hand.

Also covers ConfigurationApplyInProgress/UpdateInstallInProgress with
tests that reach their Unknown+ReasonReconciling branch, and clarifies
a subtest name that overstated its assertion.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
…tion

The description asserted that the resource "requires no cluster-wide
permissions". Whether that holds is decided by the RBAC that ships with
the controllers, not by the schema, so the CRD is the wrong place to
promise it. The namespace statement it followed already carries the part
that is true of the API itself.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The three repository kinds differ only in scope; their spec and status
were already identical field for field, description for description, so
the generated CRDs are unchanged by this commit. One declaration lets the
controller reconcile all of them through a single code path.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The three catalog kinds are projections of a repository index and were
already identical in shape. Sharing the status type changes one word in
the released HelmClusterAddonChart description ("addon chart state" ->
"chart state") and its translation; nothing else in the schema moves.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
Internal objects of every family share d8-operator-helm, so a name derived
from the source name alone lets two same-named namespaced sources overwrite
each other's HelmRepository and auth secret. DerivedName always includes a
hash over kind, namespace and name. The addon naming functions are left
untouched and their exact output is now pinned by a test: those names are
live objects.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
source.Repository is how the services will see any repository kind; the
three adapters hold everything that differs between kinds — labels, derived
names, owner GVK. The addon adapter reproduces the released names and
labels exactly. Nothing consumes the contract yet.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
…catalog

The catalog writer is the one place generic over API types: catalog objects
must be created, listed and status-patched with their concrete kind. A
namespaced catalog lists by the repository namespace, which is what keeps two
same-named repositories in different namespaces apart. The merge and sort
rules move here unchanged with their tests; the application catalogs have no
consumers until HelmApplication is reconciled.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
…h the contract

EnsureSecrets, EnsureInternalHelmRepository and the cleanup paths take a
source.Repository instead of the addon type; names and labels come from the
adapter. The addon reconciler wraps its object at the boundary, so nothing
observable changes for the addon family.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
RepoSyncService fetches through source.Repository and writes through
source.Catalog; its own catalog code is gone, the generic catalog owns it.
OCIRepoService cleans up by InternalNames and implements ConsumerForcer for
the addon family. Behaviour is unchanged: the synchronization tests pass with
their expectations untouched.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
…iler

The repository reconciler reads the API object through a source.Repository
adapter handed to it as a factory, so the same code serves any repository
kind. The package moves from reconcile/helmclusteraddonrepository to
reconcile/repository; Evaluate and its tests move with it unchanged. The
addon controller is the first, and so far the only, user.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
A namespaced source cannot be identified by its name alone once its internal
objects share d8-operator-helm with everything else; the new mapper reads the
namespace from the second source label. The existing mapper is untouched.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
…ApplicationRepository

Both kinds are served by the shared repository reconciler through their
adapters. Their internal objects carry namespace-aware derived names and, for
the namespaced kind, a source-namespace label the watches map back through.
Neither has consumers yet: HelmApplication is the next plan, and until then
force requests stop at the repository and no chart version is protected from
pruning.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
…neralization

Mapper log lines for other families' internal objects drop to debug
verbosity; a reconciler test now covers the namespaced kind end to end;
catalog errors name the repository namespace; RBAC gains /finalizers on the
new resources; stale comments corrected and the stage-5 stand-ins marked.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
source.Release is how the services will see any release kind; the
companion contracts (RepositoryResolver, ChartClaim, TargetNamespaceEnsurer,
AccessManager, ReleaseLister) name the places where the addon and the
application families differ, each with a no-op for the family that does
not need it. HelmReleaseName bounds a release name to Helm's 53-character
limit without touching any name that already fits; Catalog.Lookup lets a
release find the version it asks for. Nothing consumes the contract yet.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The addon release adapter reproduces the released names and labels exactly
and bounds the release name to Helm's limit. The repository resolver and
the release lister give the repository branch a way to reach the addon's
consumers without knowing their kind.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
…es through the release contract

ChartService, ReleaseService and MaintenanceService take a source.Release
(and, for the chart, the source.Repository whose internal HelmRepository
it points at). ResolveChartSource takes the repository url so every kind
can use it. The HelmRelease gains serviceAccountName and storageNamespace
when the family impersonates — no family does yet. The addon reconciler
wraps its objects at the boundary; behaviour is unchanged.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
…e lister

The release side of OCIRepoService takes the contract. Forcing the
consumers' internal OCIRepositories moves into ForceService, which is
handed the family's ReleaseLister instead of listing addons itself; the
catalog's in-use lookup is built from the same lister. The addon family is
the only one wired so far.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
…release contract

ClaimService takes a source.Release and implements source.ChartClaim; it
stays an addon service in substance — the holder is read as a
HelmClusterAddon. Creating the target namespace moves out of the reconciler
into NamespaceService, the addon's TargetNamespaceEnsurer.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The release reconciler reads the API object through a source.Release
adapter and takes its kind-specific collaborators as Deps: repository
resolver, chart claim, target-namespace ensurer and access manager. Each
step is the addon's step of today; the package moves from
reconcile/helmclusteraddon to reconcile/release. AccessSetupFailed is the
reason a release reports when its identity cannot be set up — no family
sets one up yet.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The application adapter is the one place the two mutually exclusive
repository fields are read; downstream sees a kind, a namespace and a
name. Its internal objects carry namespace-aware derived names, the Helm
release name is prefixed so a hand-installed release cannot be taken over,
and lastAppliedChart is replaced wholesale. The HelmApplication indexes
carry the repository kind and namespace, which is what lets the
application catalogs see real consumers scoped to their namespace.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
…ied with

AccessService creates a ServiceAccount in the operator namespace without a
token, seeds a Role with full rights inside the application namespace and
binds the two. The Role is created once and never reconciled: it is where a
namespace owner cuts the rights down, and a RoleBinding cannot grant
anything beyond the namespace regardless of its content. Cleanup removes
the account and the binding and keeps the Role.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
…sing them

Two mappers enqueue the HelmApplication objects referencing a repository
or one of its charts, through indexes that carry the repository kind and
namespace. The application repository controllers now force their real
consumers instead of nobody; the last stage-5 stand-ins of the repository
plan are gone.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
HelmApplication is served by the shared release reconciler with no chart
claim, no target-namespace creation and an identity of its own: the chart
is applied as a ServiceAccount bound to a namespace Role, so the release
cannot reach beyond its namespace. The webhook rejects system namespaces
and deletion under maintenance. The controller gains rights on roles and
role bindings (with escalate and bind) and on service accounts in its own
namespace; helm-controller already holds every verb.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The chart of an application is applied as a ServiceAccount bound to a Role
with every permission inside the namespace, so the right to create the
resource is the right to administer the namespace. The CRD description —
the source of the module's resource documentation — now says so, and that
the Role may be narrowed by the namespace owner.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
drey added 13 commits September 13, 2026 14:37
The HelmApplication webhook and NamespaceService read a Namespace through the
manager's cached client, whose reflector needs watch — a verb the ClusterRole
does not grant. Without it the reflector relists in a loop and the cache is
stale between relists, which matters for the webhook's delete path deciding
whether the namespace is terminating. Both now read through mgr.GetAPIReader(),
the same pattern ClaimService already uses for a decision that must not be made
against stale data.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
Transport built a bare &http.Transport{} per probe, carrying none of the
default's dial/idle timeouts and never releasing its connection pool, and
ChartLayerMediaType had no deadline of its own, so a caller with none could
be held open indefinitely by a hung registry. Transport now clones
http.DefaultTransport before setting TLS, and the probe both bounds itself
with a probeTimeout floor (kept below the caller's own deadline when it has
one) and closes the single-use transport's idle connections once it returns.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
…pace

main.go disabled caching for ServiceAccount and RoleBinding but left Secret
alone, and the repository controllers watch Secrets, so the operator held
every Secret in the cluster in memory. Every Secret the operator reads or
writes lives in the module namespace (the repository services all take
helmv1alpha1.TargetNamespace), so cache.Options.ByObject restricts the Secret
informer to it. Adding Secret to Client.Cache.DisableFor instead would not
help: an explicit Watches(&corev1.Secret{}, ...) starts an informer regardless.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The message is produced on update as well as on create, where "cannot be
created" describes neither the request nor the rule.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
Scoping escalate by resourceNames denied every seeded role: the check
authorizes against the name in the request path, and a create carries
none, so the rule matched nothing and each application failed to set up
its identity. bind keeps the scope — it is authorized against the name in
the binding's roleRef, which is always present.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
It belongs to whoever works there, and that is a user without a namespace
of their own — exactly the person this family exists for. Refusing an
application in default kept them out for no reason the cluster shares.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
Every repository kind now records the size of the catalog it published at
its last successful read, refreshed on each synchronization. The field is
a pointer: a repository nobody has managed to read yet answers nothing,
which is not the same answer as a repository offering no charts.

A failed read leaves the previous count standing rather than replacing it
with zero, so the field always describes a real reading.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The experimental role model is dropped until it is added deliberately,
and the classic one is reduced to the two levels that matter: Editor may
do anything with an application and list its repositories and charts,
Admin adds the repositories themselves and a read of the chart catalog.
Levels accumulate upwards, so Admin carries only the difference.

The cluster-scoped repository and its catalog lose their user-facing
level with the experimental model and are reachable only cluster-wide.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
Reading a repository is reading its password, since the credentials live
in the object; PrivilegedUser is the level from which Deckhouse permits
reading Secrets, so that is where both kinds become readable. Admin keeps
only what it adds: creating and modifying a repository.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The addon family and the cluster-wide application repository had no role
of their own, so they were reachable only by a binding written by hand.
ClusterAdmin now carries them, and reads both cluster-scoped catalogs —
writing a catalog belongs to the controller and to no one else.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
Installing an application carries namespace-admin rights, so the level
below it had nothing to offer that was safe to offer: reading a
repository is reading its password, and everything else it granted led
straight to that install. Admin now holds the whole namespaced family.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
An application may take its chart from a HelmClusterApplicationRepository
that only ClusterAdmin manages, and until now the person writing that
application could not see what the repository offers. The catalog moves
down to Admin; ClusterAdmin keeps the addon one, which is all that is
left to add there.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
A client that lists a catalog usually wants to follow it, and refusing
the watch only pushes it into polling.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
drey and others added 16 commits September 18, 2026 15:21
Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The experimental role model was dropped from the chart until it could be
added deliberately, and the role names changed in DKP 1.78 in the
meantime. The capabilities are written for that model alone and gated on
the platform version, so a cluster below 1.78 gets none of them and
keeps being served by the classic roles.

The levels follow what each one is allowed to see. Reading an
application, its chart catalog and its repository sits at user: the
repository carries its registry credentials in the object itself, and
user is the level at which Deckhouse first permits reading secrets in a
namespace. Installing an application is namespace-admin equivalent —
the controller seeds a Role with unrestricted rights there — so the
writes reach no level below admin. The cluster-scoped chart catalog is
absent from both: a namespace role is granted through a RoleBinding,
where a cluster-scoped rule grants nothing.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
Every cluster-scoped kind the module exposes, and its own configuration,
land with the administrators of the delivery subsystem declared in
module.yaml rather than with the kubernetes ones they were parked on
before. user-authz ships no delivery roles yet, so the capabilities
reach nobody until it does.

The viewer level reads all of them, repositories included: the split
that kept a repository out of it would have to be paid for elsewhere,
since a subsystem administrator manages those repositories in the first
place. Writing is left to the manager level, and neither chart catalog
is writable at any level — the controller is its only author.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The classic roles granted the whole namespaced family to Admin alone,
which put reading an application at the same level as installing one.
Reading now sits at PrivilegedUser — the level at which Deckhouse first
permits reading secrets, which is what reading a repository amounts to,
its credentials living in the object — together with both chart catalogs
an application may pick from. Admin keeps only what it adds: the writes.

This is the level split the RBACv2 capabilities of this module already
use, so the two role models now grant the same thing at the same height.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The auth and TLS secrets were reconciled by a method on BaseRepoService
that neither repository service used itself, so the reconciler reached
them through whichever of the two was at hand — and had to explain in a
comment that this was safe because both embed the same base with the
same namespace. Their removal was worse: the same loop over the two
secret names was written twice, once inside CleanupHelmRepository and
once inside CleanupOCIRepository, and the deletion path explained that
cleaning up "the helm way" also works for an oci repository.

RepoSecretsService now owns both ends. The reconciler holds it as a
dependency of its own, the deletion path removes the secrets once before
it looks at the repository type, and the repository services are left
with the objects they actually own.

CleanupOCIRepository disappears entirely: despite its name it deleted no
OCIRepository — that object belongs to a release, is named in
ReleaseNames rather than InternalNames, and is removed by the release
reconciler — so the secrets were everything it did. Its branch of the
deletion switch collapses into a check for the one type that does have
an internal object, and the oci repository service is no longer a
dependency of this reconciler.

A failure to delete a secret now reports "auxiliary secrets" on the
status instead of "internal repository", the name it inherited from the
method it used to be part of.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The catalog logged the chart object under "name", the key controller-runtime
already fills with the repository being reconciled. zap emits both, and every
JSON reader downstream keeps the last one, so the repository's identity was
silently replaced by a derived chart name and the line read as being about a
resource nobody reconciled.

The object a line names now travels under a key of its own, which is the rule
the rest of the controller follows.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
controller-runtime puts a logger into the context only in its reconcile
handler, so log.FromContext in a MapFunc returns the bare root logger: these
lines carried no controller, no kind and, in four of six, no object at all.
An operator reading one could not tell which resource, or even which
controller, it came from.

Each line now names the object whose event is being mapped, and the two
mappers registered once per repository kind name that kind, so their otherwise
identical lines can be told apart.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
Both reconcilers logged the abstract "internal repository", which means a
HelmRepository in one and an OCIRepository in the other, and named no object.
The name alone would not have settled it either: every internal object of one
owner shares a single derived name in one namespace, so only the type says
which one is stuck. The status keeps the abstract wording it shows the user;
the log now carries the type and the key someone has to reach for.

Also drops a no-op context round-trip in both Reconcile methods and a repository
name that duplicated what controller-runtime already puts on every line.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The "Reconciled ..." lines reported only the operation. Internal names are
derived from a hash, so nobody could get from such a line to the object it
wrote without recomputing one by hand.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
HTTP 429 fell into the catch-all 4xx arm and was reported as terminal, so a
repository saturated its failure counter on the first throttled read and an
addon whose artifact probe was throttled scheduled no retry at all. It is the
one rejection in that range that says "later" rather than "no".

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The namespace role is no longer seeded once and forgotten: it is read back,
rewritten and watched for drift, which needs get and patch on the single role
name the module writes, and list and watch on the kind for the informer behind
that watch. escalate stays granted for every role because, like create, it is
authorized against a name the request does not carry.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
…lers

Each reconciler now gathers the facts of one pass and hands them to a pure
Evaluate that returns the whole desired status, with the collaborators it calls
declared where they are used. The status manager moves to internal/status, the
outcome types split per family, the chart source vocabulary and the per-pass
helpers get packages of their own, and the watch mappers move from utils to the
controllers that own them.

The behaviour the status reports comes with it, because it is written against
that decision function: a failure no retry resolves raises Stalled and schedules
nothing, Reconciling is maintained on every pass rather than only on a forced
one, and a Role or RoleBinding occupying the application identity's name without
the module's managed-by label is refused rather than adopted.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The application identity now refuses a Role or RoleBinding that carries no
managed-by label, which retires the case asserting that a stripped label is
written back: a label-less object is foreign, so it stalls the application
instead. The replacement covers both directions in a block of its own, because
a scenario that blocks an application deliberately produces controller error
logs the shared assertion would trip over.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The limitation already said both objects are reconciled; it did not say what
decides they are the module's, nor what happens when something else holds the
name first.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The reduction of the internal object's conditions never read its Stalled
condition, so a release whose remediation attempts were spent looked like any
other failure and was reported as ProgressingWithRetry — a retry that is not
coming, which kstatus reads before Ready and so parks the object at InProgress
for good. The internal repository has been propagated this way since its own
status rework; the release side had no equivalent.

The verdict now carries whether the object gave up alongside what went wrong,
so the release stalls with the fault the error rules named rather than with a
count of spent attempts, and the progress condition stays off. A stalled object
may still carry the Reconciling condition of the attempt that gave up, so that
short circuit is skipped for it.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
The condition and chart version descriptions had grown into accounts of how the
controller behaves, which belongs next to the code rather than in a schema every
consumer renders. The Russian translations follow the same cut and the CRDs are
regenerated.

Two markers lost along with the prose come back: the +optional on the
application's conditions, and the blank line that keeps the note above
HelmApplicationLastAppliedChartRef out of the field's description, which is the
convention repository_types.go spells out. Neither affects the generated schema.

Signed-off-by: Ilya Drey <ilya.drey@flant.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant