From 2d1c8aae026eac12ea3f4b05329e50537f1fbb1c Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Fri, 9 Jan 2026 18:02:18 +0100 Subject: [PATCH] feat(platform): add flux-plunger controller Add flux-plunger controller to automatically fix HelmRelease resources with "has no deployed releases" error. The controller watches HelmRelease resources and performs the following: - Detects HelmRelease with "has no deployed releases" error - Suspends the HelmRelease with flux-client-side-apply field manager - Deletes the latest Helm release secret - Updates annotation with processed version to prevent recursive deletion - Unsuspends the HelmRelease to allow Flux to retry Special handling for suspended HelmRelease: - If suspend=true and latest+1==processed: removes suspend - Otherwise: skips processing as suspended by external process Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- cmd/flux-plunger/main.go | 151 ++++++++ .../controller/fluxplunger/flux_plunger.go | 333 ++++++++++++++++++ .../core/platform/sources/flux-plunger.yaml | 22 ++ .../platform/templates/bundles/system.yaml | 1 + packages/system/flux-plunger/Chart.yaml | 6 + packages/system/flux-plunger/Makefile | 19 + .../images/flux-plunger/Dockerfile | 23 ++ .../flux-plunger/templates/deployment.yaml | 55 +++ .../system/flux-plunger/templates/rbac.yaml | 37 ++ .../flux-plunger/templates/service.yaml | 18 + .../templates/serviceaccount.yaml | 8 + packages/system/flux-plunger/values.yaml | 1 + 12 files changed, 674 insertions(+) create mode 100644 cmd/flux-plunger/main.go create mode 100644 internal/controller/fluxplunger/flux_plunger.go create mode 100644 packages/core/platform/sources/flux-plunger.yaml create mode 100644 packages/system/flux-plunger/Chart.yaml create mode 100644 packages/system/flux-plunger/Makefile create mode 100644 packages/system/flux-plunger/images/flux-plunger/Dockerfile create mode 100644 packages/system/flux-plunger/templates/deployment.yaml create mode 100644 packages/system/flux-plunger/templates/rbac.yaml create mode 100644 packages/system/flux-plunger/templates/service.yaml create mode 100644 packages/system/flux-plunger/templates/serviceaccount.yaml create mode 100644 packages/system/flux-plunger/values.yaml diff --git a/cmd/flux-plunger/main.go b/cmd/flux-plunger/main.go new file mode 100644 index 00000000..13fe88cd --- /dev/null +++ b/cmd/flux-plunger/main.go @@ -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) + } +} diff --git a/internal/controller/fluxplunger/flux_plunger.go b/internal/controller/fluxplunger/flux_plunger.go new file mode 100644 index 00000000..44886a2c --- /dev/null +++ b/internal/controller/fluxplunger/flux_plunger.go @@ -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) +} diff --git a/packages/core/platform/sources/flux-plunger.yaml b/packages/core/platform/sources/flux-plunger.yaml new file mode 100644 index 00000000..bbee0b34 --- /dev/null +++ b/packages/core/platform/sources/flux-plunger.yaml @@ -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 diff --git a/packages/core/platform/templates/bundles/system.yaml b/packages/core/platform/templates/bundles/system.yaml index 59e44263..a1e6a195 100644 --- a/packages/core/platform/templates/bundles/system.yaml +++ b/packages/core/platform/templates/bundles/system.yaml @@ -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)) -}} diff --git a/packages/system/flux-plunger/Chart.yaml b/packages/system/flux-plunger/Chart.yaml new file mode 100644 index 00000000..b87ca73a --- /dev/null +++ b/packages/system/flux-plunger/Chart.yaml @@ -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" diff --git a/packages/system/flux-plunger/Makefile b/packages/system/flux-plunger/Makefile new file mode 100644 index 00000000..264282d4 --- /dev/null +++ b/packages/system/flux-plunger/Makefile @@ -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 diff --git a/packages/system/flux-plunger/images/flux-plunger/Dockerfile b/packages/system/flux-plunger/images/flux-plunger/Dockerfile new file mode 100644 index 00000000..37821699 --- /dev/null +++ b/packages/system/flux-plunger/images/flux-plunger/Dockerfile @@ -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"] diff --git a/packages/system/flux-plunger/templates/deployment.yaml b/packages/system/flux-plunger/templates/deployment.yaml new file mode 100644 index 00000000..7ce6bb13 --- /dev/null +++ b/packages/system/flux-plunger/templates/deployment.yaml @@ -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 diff --git a/packages/system/flux-plunger/templates/rbac.yaml b/packages/system/flux-plunger/templates/rbac.yaml new file mode 100644 index 00000000..97d4eac7 --- /dev/null +++ b/packages/system/flux-plunger/templates/rbac.yaml @@ -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 }} diff --git a/packages/system/flux-plunger/templates/service.yaml b/packages/system/flux-plunger/templates/service.yaml new file mode 100644 index 00000000..69aa6afb --- /dev/null +++ b/packages/system/flux-plunger/templates/service.yaml @@ -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 diff --git a/packages/system/flux-plunger/templates/serviceaccount.yaml b/packages/system/flux-plunger/templates/serviceaccount.yaml new file mode 100644 index 00000000..aff76a13 --- /dev/null +++ b/packages/system/flux-plunger/templates/serviceaccount.yaml @@ -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 }} diff --git a/packages/system/flux-plunger/values.yaml b/packages/system/flux-plunger/values.yaml new file mode 100644 index 00000000..1da12610 --- /dev/null +++ b/packages/system/flux-plunger/values.yaml @@ -0,0 +1 @@ +image: "ghcr.io/cozystack/cozystack/flux-plunger:latest@sha256:6a6ec938973e6583c5e1573f7d25beddc1852a6699a089116b2407e29a564a72"