Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4d1229a
feat(core): add repository sync timestamps and kstatus condition types
drey Sep 1, 2026
1602599
chore(core): regenerate api artifacts for repository status fields
drey Sep 1, 2026
52a8570
feat(core): classify terminal helm repository errors and skip invalid…
drey Sep 1, 2026
ce71105
feat(core): classify terminal OCI repository errors
drey Sep 1, 2026
6bed005
feat(core): derive repository conditions in a pure evaluate function
drey Sep 1, 2026
e26a1a7
fix(core): pin Decision.Err filter and void stale Stalled reason on g…
drey Sep 1, 2026
f35445d
feat(core): schedule repository sync with exponential backoff
drey Sep 1, 2026
5f52257
fix(core): pass the addon name and wrap the cause in the uniqueness c…
drey Sep 1, 2026
f7714d4
test(core): cover ShouldAttempt nil-schedule branch and requeue floor
drey Sep 1, 2026
a0fcdce
test(core): verify repository status against kstatus
drey Sep 1, 2026
2ee247f
refactor(core): extract shared repository secret reconciliation
drey Sep 1, 2026
d749383
feat(core): report internal repository state instead of a status prov…
drey Sep 1, 2026
fb9325c
test(core): cover the API-failure and Stalled-precedence branches of …
drey Sep 1, 2026
65c9e52
feat(core): split repository sync into fetch and catalog phases
drey Sep 1, 2026
920f82e
feat(core): rework HelmClusterAddonRepository status semantics
drey Sep 1, 2026
eb9a291
fix(core): clean up internal objects when the repository url no longe…
drey Sep 1, 2026
62ee8cb
test(core): cover repository stall and recovery in e2e
drey Sep 1, 2026
d7b47e3
fix(core): use post-update object for recovery waits in e2e
drey Sep 1, 2026
7487cd7
docs(core): document repository conditions and sync schedule
drey Sep 1, 2026
c39ea94
docs(core): drop hand-written entries from generated release notes
drey Sep 1, 2026
dfc43c8
docs(core): name all sync-status fields and default kubectl columns
drey Sep 1, 2026
41cd556
fix(core): keep repository conditions honest between sync attempts
drey Sep 1, 2026
f87de7e
fix(core): report the cause behind an exhausted repository read
drey Sep 1, 2026
018b555
fix(core): patch the force annotation only when it is set
drey Sep 1, 2026
9a09f37
test(core): cover abnormal condition removal through the status patch
drey Sep 1, 2026
d154340
test(core): exclude the deliberate stall from the e2e log watcher
drey Sep 1, 2026
1d8d30c
Merge branch 'main' into feat/rework-repository-sync
drey Sep 2, 2026
d015947
fix(core): pass the repository type into EnsureSecrets
drey Sep 2, 2026
e0049f4
fix(core): render Next Sync as a timestamp instead of an age
drey Sep 2, 2026
a00803a
fix(core): keep the print-column note out of the API description
drey Sep 2, 2026
322d863
test(core): stop requiring a freshly created webhook and TLS secret
drey Sep 2, 2026
24d7a6b
test(core): pass the built module digest into the e2e run
drey Sep 2, 2026
81be56c
test(core): require the module digest instead of skipping the check
drey Sep 2, 2026
64bfab1
test(core): check the module digest after the module is enabled
drey Sep 2, 2026
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
17 changes: 17 additions & 0 deletions .github/workflows/build_dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
name: Build and Push images
outputs:
modules_module_tag: ${{ steps.modules_module_tag.outputs.MODULES_MODULE_TAG }}
modules_module_digest: ${{ steps.modules_module_digest.outputs.MODULES_MODULE_DIGEST }}
steps:
- name: Set vars
id: modules_module_tag
Expand Down Expand Up @@ -65,6 +66,21 @@ jobs:
module_tag: ${{ steps.modules_module_tag.outputs.MODULES_MODULE_TAG }}
svace_enabled: false

# The build action exposes no outputs, so the digest of what it just pushed
# is resolved here and handed to the e2e job, which asserts the cluster is
# running exactly this artifact.
- name: Resolve module digest
id: modules_module_digest
run: |
IMAGE="dev-registry.deckhouse.io/sys/deckhouse-oss/modules/${{ vars.MODULES_MODULE_NAME }}:${{ steps.modules_module_tag.outputs.MODULES_MODULE_TAG }}"
DIGEST="$(crane digest "$IMAGE")"
if [[ ! "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "::error title=Cannot resolve module digest::crane digest $IMAGE returned '$DIGEST'"
exit 1
fi
echo "$IMAGE -> $DIGEST"
echo "MODULES_MODULE_DIGEST=$DIGEST" >> "$GITHUB_OUTPUT"

show_dev_manifest:
runs-on: [self-hosted, large]
name: Show manifest
Expand Down Expand Up @@ -142,6 +158,7 @@ jobs:
KIND_CLUSTER_NAME: d8-operator-helm-${{ github.run_number }}
DEV_REGISTRY_DOCKER_CONFIG: ${{ secrets.DEV_REGISTRY_DOCKER_CONFIG }}
E2E_MODULE_TAG_NAME: ${{ needs.build_dev.outputs.modules_module_tag }}
E2E_MODULE_DIGEST: ${{ needs.build_dev.outputs.modules_module_digest }}
E2E_MODULE_SOURCE: operator-helm

- name: Delete kind cluster
Expand Down
16 changes: 16 additions & 0 deletions api/v1alpha1/conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ const (
ConditionTypeSynced = "Synced"
ConditionTypeUninstallFailed = "UninstallFailed"

// kstatus abnormal-true conditions. Present only while applicable.
ConditionTypeReconciling = "Reconciling"
ConditionTypeStalled = "Stalled"

ReasonMaintenanceModeActive = "MaintenanceModeActive"
ReasonMaintenanceModeInactive = "MaintenanceModeInactive"
ReasonSyncFailed = "SyncFailed"
Expand All @@ -36,6 +40,18 @@ const (
ReasonUninstallFailed = "UninstallFailed"
ReasonChartClaimConflict = "ChartClaimConflict"

// HelmClusterAddonRepository condition reasons.
ReasonAuxiliaryResourcesFailed = "AuxiliaryResourcesFailed"
ReasonCatalogUpdateFailed = "CatalogUpdateFailed"
ReasonAwaitingInitialSync = "AwaitingInitialSync"
ReasonProgressingWithRetry = "ProgressingWithRetry"
ReasonRetriesExceeded = "RetriesExceeded"
ReasonAuthenticationFailed = "AuthenticationFailed"
ReasonSourceNotFound = "SourceNotFound"
ReasonSourceRejectedRequest = "SourceRejectedRequest"
ReasonInvalidRepositoryURL = "InvalidRepositoryURL"
ReasonUnsupportedRepositoryType = "UnsupportedRepositoryType"

// HelmRelease error reasons
ReasonReleaseFailed = "ReleaseFailed"
ReasonTestFailed = "TestFailed"
Expand Down
32 changes: 32 additions & 0 deletions api/v1alpha1/helm_cluster_addon_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ const (
HelmClusterAddonRepositoryLabelSourceName = "helm.deckhouse.io/cluster-addon-repository"
)

// The "Next Sync" print column below is a string, not a date, on purpose: a date
// column prints how long ago its value was, and kubectl renders any instant more
// than a second in the future as <invalid>. nextSyncTime is always in the future.
//
// This note is deliberately outside the doc comment below — controller-gen folds
// every non-marker line of that block into the resource's API description.

// HelmClusterAddonRepository represents a Helm or OCI-compliant repository containing Helm charts that can be referenced by HelmClusterAddon resources.
//
// +kubebuilder:object:root=true
Expand All @@ -36,6 +43,10 @@ const (
// +kubebuilder:resource:singular=helmclusteraddonrepository,scope=Cluster
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status",description="The readiness status of the repository"
// +kubebuilder:printcolumn:name="Synced",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status",description="Repository synchronization status"
// +kubebuilder:printcolumn:name="Last Sync",type="date",JSONPath=".status.lastSuccessfulSyncTime",description="Time of the last successful catalog synchronization"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:printcolumn:name="Next Sync",type="string",JSONPath=".status.nextSyncTime",priority=1,description="Scheduled time of the next synchronization attempt"
// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].message",priority=1
// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
Expand Down Expand Up @@ -108,10 +119,31 @@ type HelmClusterAddonRepositoryAuth struct {

type HelmClusterAddonRepositoryStatus struct {
// Conditions represent the latest available observations of the repository state.
//
// Ready reports whether the repository is usable: auxiliary resources are in place,
// the internal source object is healthy and the repository has responded to a catalog
// read on the current spec. A transient read failure does not flip Ready to False.
//
// Synced reports whether the chart catalog is up to date.
//
// Reconciling and Stalled follow the kstatus convention: they are present only while
// applicable. Reconciling means work is in progress or a retry is scheduled; Stalled
// means the repository will not recover without a change.
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
// Generation represents resource generation that was last processed by the controller.
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// LastSuccessfulSyncTime is the last time the chart catalog was fully brought up to date,
// including creating and pruning chart resources.
// +optional
LastSuccessfulSyncTime *metav1.Time `json:"lastSuccessfulSyncTime,omitempty"`
// NextSyncTime is the scheduled time of the next synchronization attempt.
// +optional
NextSyncTime *metav1.Time `json:"nextSyncTime,omitempty"`
// ConsecutiveFetchFailures counts consecutive failures to read from the repository.
// It drives the retry backoff and resets on the first success.
// +optional
ConsecutiveFetchFailures int32 `json:"consecutiveFetchFailures,omitempty"`
}

// HelmClusterAddonRepositoryList contains a list of HelmClusterAddonRepositories.
Expand Down
8 changes: 8 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 14 additions & 1 deletion crds/doc-ru-helmclusteraddonrepositories.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ spec:
status:
properties:
conditions:
description: Условия отражают последние наблюдения за состоянием репозитория.
description: |
Условия отражают последние наблюдения за состоянием репозитория.

`Ready` сообщает, пригоден ли репозиторий: вспомогательные ресурсы на месте, внутренний объект источника исправен, и репозиторий ответил на чтение каталога на текущей спецификации. Транзиентная ошибка чтения не переводит `Ready` в `False`.

`Synced` сообщает, актуален ли каталог чартов.

`Reconciling` и `Stalled` следуют соглашению kstatus: они присутствуют, только когда применимы. `Reconciling` означает, что работа выполняется или запланирован повтор; `Stalled` — что репозиторий не восстановится без вмешательства.
observedGeneration:
description: Поколение ресурса, обработанное контроллером последним.
lastSuccessfulSyncTime:
description: Время последнего успешного приведения каталога чартов в актуальное состояние.
nextSyncTime:
description: Запланированное время следующей попытки синхронизации.
consecutiveFetchFailures:
description: Число подряд идущих неудачных обращений к репозиторию. Определяет задержку повтора и обнуляется при первом успехе.
47 changes: 45 additions & 2 deletions crds/helmclusteraddonrepositories.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ spec:
jsonPath: .status.conditions[?(@.type=='Synced')].status
name: Synced
type: string
- description: Time of the last successful catalog synchronization
jsonPath: .status.lastSuccessfulSyncTime
name: Last Sync
type: date
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
- description: Scheduled time of the next synchronization attempt
jsonPath: .status.nextSyncTime
name: Next Sync
priority: 1
type: string
- jsonPath: .status.conditions[?(@.type=='Ready')].message
name: Message
priority: 1
type: string
name: v1alpha1
schema:
openAPIV3Schema:
Expand Down Expand Up @@ -88,8 +104,18 @@ spec:
status:
properties:
conditions:
description: Conditions represent the latest available observations
of the repository state.
description: |-
Conditions represent the latest available observations of the repository state.

Ready reports whether the repository is usable: auxiliary resources are in place,
the internal source object is healthy and the repository has responded to a catalog
read on the current spec. A transient read failure does not flip Ready to False.

Synced reports whether the chart catalog is up to date.

Reconciling and Stalled follow the kstatus convention: they are present only while
applicable. Reconciling means work is in progress or a retry is scheduled; Stalled
means the repository will not recover without a change.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
Expand Down Expand Up @@ -145,6 +171,23 @@ spec:
- type
type: object
type: array
consecutiveFetchFailures:
description: |-
ConsecutiveFetchFailures counts consecutive failures to read from the repository.
It drives the retry backoff and resets on the first success.
format: int32
type: integer
lastSuccessfulSyncTime:
description: |-
LastSuccessfulSyncTime is the last time the chart catalog was fully brought up to date,
including creating and pruning chart resources.
format: date-time
type: string
nextSyncTime:
description: NextSyncTime is the scheduled time of the next synchronization
attempt.
format: date-time
type: string
observedGeneration:
description: Generation represents resource generation that was last
processed by the controller.
Expand Down
35 changes: 35 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,38 @@ The following custom resources are used to manage Helm charts in the module:
- A HelmClusterAddon resource referencing a specific HelmClusterAddonChart can only be created as a single instance in the cluster. This is because Helm charts can contain custom resource definitions (CRDs), and installing them multiple times at the cluster level is not allowed.

See [usage examples](example.html) for practical scenarios.

## Repository Status

`HelmClusterAddonRepository` reports four conditions.

`Ready` tells whether the repository is usable: its auxiliary resources are in
place, its internal source object is healthy, and the repository responded to a
catalog read on the current spec. A transient read failure does not flip `Ready`
to `False` — installed addons keep working and only the catalog goes stale.

`Synced` tells whether the chart catalog is up to date.

`Reconciling` and `Stalled` follow the kstatus convention and are present only
while they apply. `Reconciling` means work is in progress or a retry is
scheduled; `Stalled` means the repository will not recover on its own.

| Ready | Synced | What it means | What to do |
|---|---|---|---|
| True | True | The repository is healthy. | Nothing. |
| True | False | The catalog read failed but the repository was usable before. | Check the `Reconciling` message and `Last Sync`. Retries are already scheduled. |
| False | True | The catalog is fresh, but the source of chart artifacts is unhealthy. | Check the `Ready` message: it is translated from the internal source object. |
| False | False | The repository is unreachable or misconfigured. | Check `Stalled`: `AuthenticationFailed`, `SourceNotFound` and `InvalidRepositoryURL` need a change in `spec`. |
| Unknown | any | The first catalog read on the current spec has not succeeded yet. | Wait for the next attempt shown in `Next Sync`. |

Synchronization runs every 5 minutes. After a failed read the delay doubles —
5m, 10m, 20m, 40m — up to one hour, and the repository is reported as `Stalled`
with reason `RetriesExceeded` once the delay reaches the cap. Retries continue
at that cadence, because the cause may disappear on the repository side. The
schedule is visible in `status.nextSyncTime` and with
`kubectl get helmclusteraddonrepository -o wide`. `kubectl get
helmclusteraddonrepository` shows the `Last Sync` and `Age` columns by
default, and `-o wide` adds `Next Sync` and the `Ready` message. The same
values are in the status itself as `lastSuccessfulSyncTime` and
`nextSyncTime`, alongside `consecutiveFetchFailures`, which counts the
consecutive failed reads driving the backoff.
35 changes: 35 additions & 0 deletions docs/README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,38 @@ weight: 10
- Ресурс HelmClusterAddon, ссылающийся на заданный HelmClusterAddonChart, может быть создан в кластере только в единственном экземпляре. Это обусловлено тем, что Helm-чарты могут содержать определения кастомных ресурсов (CRD), повторная установка которых на уровне кластера недопустима.

Примеры использования приведены в разделе [примеры использования](example.html).

## Статус репозитория

`HelmClusterAddonRepository` сообщает о себе четырьмя условиями.

`Ready` показывает, пригоден ли репозиторий: вспомогательные ресурсы на месте,
внутренний объект источника исправен, и репозиторий ответил на чтение каталога
на текущей спецификации. Транзиентная ошибка чтения не переводит `Ready` в
`False` — установленные аддоны продолжают работать, устаревает только каталог.

`Synced` показывает, актуален ли каталог чартов.

`Reconciling` и `Stalled` следуют соглашению kstatus и присутствуют, только
когда применимы. `Reconciling` означает, что работа выполняется или запланирован
повтор; `Stalled` — что репозиторий сам не восстановится.

| Ready | Synced | Что означает | Что делать |
|---|---|---|---|
| True | True | Репозиторий исправен. | Ничего. |
| True | False | Чтение каталога не удалось, но до этого репозиторий был пригоден. | Посмотреть сообщение `Reconciling` и колонку `Last Sync`. Повторы уже запланированы. |
| False | True | Каталог свежий, но источник артефактов чартов неисправен. | Посмотреть сообщение `Ready` — оно транслируется от внутреннего объекта источника. |
| False | False | Репозиторий недоступен или настроен неверно. | Посмотреть `Stalled`: `AuthenticationFailed`, `SourceNotFound` и `InvalidRepositoryURL` требуют правки `spec`. |
| Unknown | любое | Первое чтение каталога на текущей спецификации ещё не удалось. | Дождаться следующей попытки, время которой указано в `Next Sync`. |

Синхронизация выполняется каждые 5 минут. После неудачного чтения задержка
удваивается — 5m, 10m, 20m, 40m — до часа, и по достижении потолка репозиторий
переводится в `Stalled` с причиной `RetriesExceeded`. Повторы при этом
продолжаются, потому что причина может уйти на стороне репозитория. Расписание
видно в `status.nextSyncTime` и через
`kubectl get helmclusteraddonrepository -o wide`. `kubectl get
helmclusteraddonrepository` по умолчанию показывает колонки `Last Sync` и
`Age`, а `-o wide` добавляет `Next Sync` и сообщение из `Ready`. Те же
значения лежат в самом статусе — `lastSuccessfulSyncTime` и `nextSyncTime`, —
рядом с `consecutiveFetchFailures`, который считает подряд идущие неудачные
чтения, определяющие задержку повтора.
3 changes: 2 additions & 1 deletion images/operator-helm-controller/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/deckhouse/operator-helm/api v0.0.0-00010101000000-000000000000
github.com/google/go-containerregistry v0.20.6
github.com/opencontainers/go-digest v1.0.0
github.com/samber/lo v1.53.0
github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1
github.com/werf/3p-fluxcd-pkg/chartutil v1.17.0-nelm.1
github.com/werf/3p-helm-controller/api v0.1.5
Expand All @@ -19,6 +20,7 @@ require (
k8s.io/apimachinery v0.35.1
k8s.io/client-go v0.35.1
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
sigs.k8s.io/cli-utils v0.37.2
sigs.k8s.io/controller-runtime v0.23.1
)

Expand Down Expand Up @@ -61,7 +63,6 @@ require (
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/samber/lo v1.53.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/pflag v1.0.10 // indirect
Expand Down
Loading
Loading