[platform] Add flux-plunger controller to fix HelmRelease errors (#1843)

## What this PR does

This PR introduces a new flux-plunger controller that automatically
fixes HelmRelease resources experiencing "has no deployed releases"
errors by cleaning up corrupted Helm release secrets.

### Problem

When Helm releases fail with "has no deployed releases" error, FluxCD
cannot recover automatically. This typically happens when:
- Helm release secrets are corrupted
- Failed upgrades leave the release in an inconsistent state
- Manual intervention is required to delete the problematic secret

### Solution

The flux-plunger controller:
1. Watches HelmRelease resources for "has no deployed releases" errors
2. Suspends the HelmRelease (with `flux-client-side-apply` field
manager)
3. Identifies and deletes the latest (problematic) Helm release secret
4. Tracks processed versions via annotation to prevent recursive
deletion
5. Unsuspends the HelmRelease to allow FluxCD to retry

**Key features:**
- Only deletes one secret at a time (prevents cascading deletions)
- Uses version tracking: if `latest+1 == processed`, skips (already
handled)
- Handles suspended HelmReleases: only unsuspends if it was suspended by
flux-plunger itself
- Gracefully handles invalid annotations by treating them as unprocessed

### Components

- **Controller:** `internal/controller/fluxplunger/flux_plunger.go`
- **Main:** `cmd/flux-plunger/main.go`  
- **Helm Chart:** `packages/system/flux-plunger/`
- **Platform Integration:** Added to `paas-full.yaml` bundle

### Release note

```release-note
[platform] Add flux-plunger controller to automatically fix HelmRelease resources with "has no deployed releases" error by cleaning up corrupted Helm secrets
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

* **New Features**
* Introduced a new controller that automatically resolves HelmRelease
resource failures with built-in health probes and graceful recovery
procedures.
  * Enabled by default in system installations.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Andrei Kvapil 2026-01-20 01:56:00 +01:00 committed by GitHub
commit b0b1a1f1f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 674 additions and 0 deletions

151
cmd/flux-plunger/main.go Normal file
View file

@ -0,0 +1,151 @@
/*
Copyright 2025.
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 main
import (
"crypto/tls"
"flag"
"os"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"github.com/cozystack/cozystack/internal/controller/fluxplunger"
// +kubebuilder:scaffold:imports
)
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(helmv2.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
func main() {
var metricsAddr string
var enableLeaderElection bool
var probeAddr string
var secureMetrics bool
var enableHTTP2 bool
var tlsOpts []func(*tls.Config)
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
flag.BoolVar(&secureMetrics, "metrics-secure", true,
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
flag.BoolVar(&enableHTTP2, "enable-http2", false,
"If set, HTTP/2 will be enabled for the metrics server")
opts := zap.Options{
Development: false,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
// if the enable-http2 flag is false (the default), http/2 should be disabled
// due to its vulnerabilities. More specifically, disabling http/2 will
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
// Rapid Reset CVEs. For more information see:
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
// - https://github.com/advisories/GHSA-4374-p667-p6c8
disableHTTP2 := func(c *tls.Config) {
setupLog.Info("disabling http/2")
c.NextProtos = []string{"http/1.1"}
}
if !enableHTTP2 {
tlsOpts = append(tlsOpts, disableHTTP2)
}
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
// More info:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
// - https://book.kubebuilder.io/reference/metrics.html
metricsServerOptions := metricsserver.Options{
BindAddress: metricsAddr,
SecureServing: secureMetrics,
TLSOpts: tlsOpts,
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsServerOptions,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "flux-plunger.cozystack.io",
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
// when the Manager ends. This requires the binary to immediately end when the
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
// speeds up voluntary leader transitions as the new leader don't have to wait
// LeaseDuration time first.
//
// In the default scaffold provided, the program ends immediately after
// the manager stops, so would be fine to enable this option. However,
// if you are doing or is intended to do any operation such as perform cleanups
// after the manager stops then its usage might be unsafe.
// LeaderElectionReleaseOnCancel: true,
})
if err != nil {
setupLog.Error(err, "unable to create manager")
os.Exit(1)
}
if err = (&fluxplunger.FluxPlunger{
Client: mgr.GetClient(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "FluxPlunger")
os.Exit(1)
}
// +kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up health check")
os.Exit(1)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up ready check")
os.Exit(1)
}
setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}

View file

@ -0,0 +1,333 @@
package fluxplunger
import (
"context"
"fmt"
"sort"
"strconv"
"strings"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)
const (
annotationLastProcessedVersion = "flux-plunger.cozystack.io/last-processed-version"
errorMessageNoDeployedReleases = "has no deployed releases"
fieldManager = "flux-client-side-apply"
)
// FluxPlunger watches HelmRelease resources and fixes "has no deployed releases" errors
type FluxPlunger struct {
client.Client
}
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases,verbs=get;list;watch;update;patch
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;delete
// Reconcile handles HelmRelease resources with "has no deployed releases" error
func (r *FluxPlunger) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
// Get the HelmRelease
hr := &helmv2.HelmRelease{}
if err := r.Get(ctx, req.NamespacedName, hr); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Check if HelmRelease is suspended
if hr.Spec.Suspend {
logger.Info("HelmRelease is suspended, checking if we need to unsuspend")
// Get the list of Helm release secrets
secrets, err := r.listHelmReleaseSecrets(ctx, hr.Namespace, hr.Name)
if err != nil {
logger.Error(err, "Failed to list Helm release secrets")
return ctrl.Result{}, err
}
// If no secrets, treat latest version as 0
latestVersion := 0
if len(secrets) > 0 {
latestSecret := getLatestSecret(secrets)
latestVersion = extractVersionNumber(latestSecret.Name)
} else {
logger.Info("No Helm release secrets found while suspended, treating as version 0")
}
// Check if version is previous to just processed (latestVersion+1 == processedVersion)
// This is the ONLY condition when we unsuspend
shouldUnsuspend := false
if hr.Annotations != nil {
if processedVersionStr, exists := hr.Annotations[annotationLastProcessedVersion]; exists {
processedVersion, err := strconv.Atoi(processedVersionStr)
if err == nil && latestVersion+1 == processedVersion {
shouldUnsuspend = true
}
}
}
if shouldUnsuspend {
// Unsuspend the HelmRelease
logger.Info("Secret was already deleted in previous run, removing suspend", "latest", latestVersion, "processed", latestVersion+1)
if err := r.unsuspendHelmRelease(ctx, hr); err != nil {
logger.Info("Could not unsuspend HelmRelease, will retry on next reconcile", "error", err.Error())
return ctrl.Result{}, nil
}
return ctrl.Result{}, nil
}
// If not previous to processed, skip all actions
logger.Info("HelmRelease is suspended by external process, skipping", "latest", latestVersion)
return ctrl.Result{}, nil
}
// Check if HelmRelease has the specific error
if !hasNoDeployedReleasesError(hr) {
logger.V(1).Info("HelmRelease does not have 'has no deployed releases' error, skipping")
return ctrl.Result{}, nil
}
logger.Info("Detected HelmRelease with 'has no deployed releases' error")
// Get the list of Helm release secrets
secrets, err := r.listHelmReleaseSecrets(ctx, hr.Namespace, hr.Name)
if err != nil {
logger.Error(err, "Failed to list Helm release secrets")
return ctrl.Result{}, err
}
if len(secrets) == 0 {
logger.Info("No Helm release secrets found, skipping")
return ctrl.Result{}, nil
}
// Find the latest version
latestSecret := getLatestSecret(secrets)
latestVersion := extractVersionNumber(latestSecret.Name)
logger.Info("Found latest Helm release version", "version", latestVersion, "secret", latestSecret.Name)
// Check if we just processed the next version (current + 1 == processed)
if hr.Annotations != nil {
if processedVersionStr, exists := hr.Annotations[annotationLastProcessedVersion]; exists {
processedVersion, err := strconv.Atoi(processedVersionStr)
if err == nil {
if latestVersion+1 == processedVersion {
logger.Info("Already processed, secret was deleted previously", "latest", latestVersion, "processed", processedVersion)
return ctrl.Result{}, nil
}
} else {
// Failed to parse annotation, treat as if annotation doesn't exist
logger.Info("Failed to parse annotation, will process", "annotation", processedVersionStr, "error", err)
}
}
}
// Suspend the HelmRelease
logger.Info("Suspending HelmRelease")
if err := r.suspendHelmRelease(ctx, hr); err != nil {
// Optimistic lock conflicts are normal - FluxCD also updates HelmRelease
// Don't return error, just log and let controller-runtime requeue on next update
logger.Info("Could not suspend HelmRelease, will retry on next reconcile", "error", err.Error())
return ctrl.Result{}, nil
}
// Delete the latest secret
logger.Info("Deleting latest Helm release secret", "secret", latestSecret.Name)
if err := r.Delete(ctx, &latestSecret); err != nil {
logger.Error(err, "Failed to delete Helm release secret")
return ctrl.Result{}, err
}
// Update annotation with processed version
logger.Info("Updating annotation with processed version", "version", latestVersion)
if err := r.updateProcessedVersionAnnotation(ctx, hr, latestVersion); err != nil {
logger.Info("Could not update annotation, will retry on next reconcile", "error", err.Error())
return ctrl.Result{}, nil
}
// Unsuspend the HelmRelease
logger.Info("Unsuspending HelmRelease")
if err := r.unsuspendHelmRelease(ctx, hr); err != nil {
logger.Info("Could not unsuspend HelmRelease, will retry on next reconcile", "error", err.Error())
return ctrl.Result{}, nil
}
logger.Info("Successfully processed HelmRelease", "version", latestVersion)
return ctrl.Result{}, nil
}
// hasNoDeployedReleasesError checks if the HelmRelease has the specific error
func hasNoDeployedReleasesError(hr *helmv2.HelmRelease) bool {
for _, condition := range hr.Status.Conditions {
if condition.Type == "Ready" && condition.Status == metav1.ConditionFalse {
if strings.Contains(condition.Message, errorMessageNoDeployedReleases) {
return true
}
}
}
return false
}
// listHelmReleaseSecrets lists all Helm release secrets for a specific release
func (r *FluxPlunger) listHelmReleaseSecrets(ctx context.Context, namespace, releaseName string) ([]corev1.Secret, error) {
secretList := &corev1.SecretList{}
listOpts := []client.ListOption{
client.InNamespace(namespace),
client.MatchingLabels{
"name": releaseName,
"owner": "helm",
},
}
if err := r.List(ctx, secretList, listOpts...); err != nil {
return nil, fmt.Errorf("failed to list secrets: %w", err)
}
// Filter only helm.sh/release.v1 secrets
filtered := []corev1.Secret{}
for _, secret := range secretList.Items {
if secret.Type == "helm.sh/release.v1" {
filtered = append(filtered, secret)
}
}
return filtered, nil
}
// getLatestSecret returns the secret with the highest version number
func getLatestSecret(secrets []corev1.Secret) corev1.Secret {
if len(secrets) == 1 {
return secrets[0]
}
sort.Slice(secrets, func(i, j int) bool {
vi := extractVersionNumber(secrets[i].Name)
vj := extractVersionNumber(secrets[j].Name)
return vi > vj
})
return secrets[0]
}
// extractVersionFromSecretName extracts version string from secret name
// e.g., "sh.helm.release.v1.cozystack-resource-definitions.v10" -> "v10"
func extractVersionFromSecretName(secretName string) string {
parts := strings.Split(secretName, ".")
if len(parts) > 0 {
return parts[len(parts)-1]
}
return ""
}
// extractVersionNumber extracts numeric version from secret name
// e.g., "sh.helm.release.v1.cozystack-resource-definitions.v10" -> 10
func extractVersionNumber(secretName string) int {
version := extractVersionFromSecretName(secretName)
// Remove 'v' prefix if present
version = strings.TrimPrefix(version, "v")
num, err := strconv.Atoi(version)
if err != nil {
return 0
}
return num
}
// suspendHelmRelease sets suspend to true on the HelmRelease
func (r *FluxPlunger) suspendHelmRelease(ctx context.Context, hr *helmv2.HelmRelease) error {
// Re-fetch the HelmRelease to get the latest state
key := types.NamespacedName{Namespace: hr.Namespace, Name: hr.Name}
latestHR := &helmv2.HelmRelease{}
if err := r.Get(ctx, key, latestHR); err != nil {
return fmt.Errorf("failed to get latest HelmRelease: %w", err)
}
// If already suspended, nothing to do
if latestHR.Spec.Suspend {
return nil
}
patch := client.MergeFromWithOptions(latestHR.DeepCopy(), client.MergeFromWithOptimisticLock{})
latestHR.Spec.Suspend = true
return r.Patch(ctx, latestHR, patch, client.FieldOwner(fieldManager))
}
// unsuspendHelmRelease sets suspend to false on the HelmRelease
func (r *FluxPlunger) unsuspendHelmRelease(ctx context.Context, hr *helmv2.HelmRelease) error {
// Re-fetch the HelmRelease to get the latest state
key := types.NamespacedName{Namespace: hr.Namespace, Name: hr.Name}
latestHR := &helmv2.HelmRelease{}
if err := r.Get(ctx, key, latestHR); err != nil {
return fmt.Errorf("failed to get latest HelmRelease: %w", err)
}
// If already unsuspended, nothing to do
if !latestHR.Spec.Suspend {
return nil
}
patch := client.MergeFromWithOptions(latestHR.DeepCopy(), client.MergeFromWithOptimisticLock{})
latestHR.Spec.Suspend = false
return r.Patch(ctx, latestHR, patch, client.FieldOwner(fieldManager))
}
// updateProcessedVersionAnnotation updates the annotation with the processed version
func (r *FluxPlunger) updateProcessedVersionAnnotation(ctx context.Context, hr *helmv2.HelmRelease, version int) error {
// Re-fetch the HelmRelease to get the latest state
key := types.NamespacedName{Namespace: hr.Namespace, Name: hr.Name}
latestHR := &helmv2.HelmRelease{}
if err := r.Get(ctx, key, latestHR); err != nil {
return fmt.Errorf("failed to get latest HelmRelease: %w", err)
}
patch := client.MergeFromWithOptions(latestHR.DeepCopy(), client.MergeFromWithOptimisticLock{})
if latestHR.Annotations == nil {
latestHR.Annotations = make(map[string]string)
}
latestHR.Annotations[annotationLastProcessedVersion] = strconv.Itoa(version)
return r.Patch(ctx, latestHR, patch, client.FieldOwner(fieldManager))
}
// SetupWithManager sets up the controller with the Manager
func (r *FluxPlunger) SetupWithManager(mgr ctrl.Manager) error {
// Watch HelmReleases that either:
// 1. Have the specific error, OR
// 2. Are suspended with our annotation (to handle crash recovery)
pred := predicate.NewPredicateFuncs(func(obj client.Object) bool {
hr, ok := obj.(*helmv2.HelmRelease)
if !ok {
return false
}
// Always process if has error
if hasNoDeployedReleasesError(hr) {
return true
}
// Also process suspended HelmReleases with our annotation (crash recovery)
if hr.Spec.Suspend && hr.Annotations != nil {
if _, exists := hr.Annotations[annotationLastProcessedVersion]; exists {
return true
}
}
return false
})
return ctrl.NewControllerManagedBy(mgr).
Named("fluxplunger").
For(&helmv2.HelmRelease{}).
WithEventFilter(pred).
Complete(r)
}

View file

@ -0,0 +1,22 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.flux-plunger
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.cert-manager
- cozystack.cozystack-engine
components:
- name: flux-plunger
path: system/flux-plunger
install:
namespace: cozy-fluxcd
releaseName: flux-plunger

View file

@ -38,6 +38,7 @@
# Common Packages
{{include "cozystack.platform.package.default" (list "cozystack.cert-manager" $) }}
{{include "cozystack.platform.package.default" (list "cozystack.flux-plunger" $) }}
{{include "cozystack.platform.package.default" (list "cozystack.victoria-metrics-operator" $) }}
{{- $tenantComponents := dict -}}
{{- $tenantClusterValues := dict "_cluster" (dict "oidc-enabled" (ternary "true" "false" .Values.authentication.oidc.enabled)) -}}

View file

@ -0,0 +1,6 @@
apiVersion: v2
name: cozy-flux-plunger
description: Controller that automatically fixes HelmRelease resources with "has no deployed releases" error
type: application
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
appVersion: "1.0.0"

View file

@ -0,0 +1,19 @@
export NAME=flux-plunger
export NAMESPACE=cozy-fluxcd
include ../../../scripts/common-envs.mk
include ../../../scripts/package.mk
image:
docker buildx build -f images/flux-plunger/Dockerfile ../../../ \
--provenance false \
--tag $(REGISTRY)/flux-plunger:$(call settag,$(TAG)) \
--cache-from type=registry,ref=$(REGISTRY)/flux-plunger:latest \
--cache-to type=inline \
--metadata-file images/flux-plunger.json \
--push=$(PUSH) \
--label "org.opencontainers.image.source=https://github.com/cozystack/cozystack" \
--load=$(LOAD)
IMAGE="$(REGISTRY)/flux-plunger:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/flux-plunger.json -o json -r)" \
yq -i '.image = strenv(IMAGE)' values.yaml
rm -f images/flux-plunger.json

View file

@ -0,0 +1,23 @@
FROM golang:1.25-alpine AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /workspace
COPY go.mod go.sum ./
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go mod download
COPY pkg pkg/
COPY cmd cmd/
COPY internal internal/
COPY api api/
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags="-extldflags=-static" -o /flux-plunger cmd/flux-plunger/main.go
FROM scratch
COPY --from=builder /flux-plunger /flux-plunger
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
ENTRYPOINT ["/flux-plunger"]

View file

@ -0,0 +1,55 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: flux-plunger
labels:
app.kubernetes.io/name: flux-plunger
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: flux-plunger
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: flux-plunger
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
serviceAccountName: flux-plunger
containers:
- name: flux-plunger
image: "{{ .Values.image }}"
args:
{{- if .Values.debug }}
- --zap-log-level=debug
{{- else }}
- --zap-log-level=info
{{- end }}
- --metrics-bind-address=:8080
- --metrics-secure=false
ports:
- name: metrics
containerPort: 8080
- name: health
containerPort: 8081
livenessProbe:
httpGet:
path: /healthz
port: health
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz
port: health
initialDelaySeconds: 5
periodSeconds: 10
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 10m
memory: 64Mi

View file

@ -0,0 +1,37 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: flux-plunger
rules:
- apiGroups:
- helm.toolkit.fluxcd.io
resources:
- helmreleases
verbs:
- get
- list
- watch
- update
- patch
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- list
- delete
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: flux-plunger
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: flux-plunger
subjects:
- kind: ServiceAccount
name: flux-plunger
namespace: {{ .Release.Namespace }}

View file

@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
name: flux-plunger
labels:
app.kubernetes.io/name: flux-plunger
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
selector:
app.kubernetes.io/name: flux-plunger
app.kubernetes.io/instance: {{ .Release.Name }}
ports:
- name: metrics
port: 8080
targetPort: 8080
- name: health
port: 8081
targetPort: 8081

View file

@ -0,0 +1,8 @@
apiVersion: v1
automountServiceAccountToken: true
kind: ServiceAccount
metadata:
name: flux-plunger
labels:
app.kubernetes.io/name: flux-plunger
app.kubernetes.io/instance: {{ .Release.Name }}

View file

@ -0,0 +1 @@
image: "ghcr.io/cozystack/cozystack/flux-plunger:latest@sha256:6a6ec938973e6583c5e1573f7d25beddc1852a6699a089116b2407e29a564a72"