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 <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
This commit is contained in:
parent
bfd2e1fd15
commit
2d1c8aae02
12 changed files with 674 additions and 0 deletions
151
cmd/flux-plunger/main.go
Normal file
151
cmd/flux-plunger/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
333
internal/controller/fluxplunger/flux_plunger.go
Normal file
333
internal/controller/fluxplunger/flux_plunger.go
Normal 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)
|
||||
}
|
||||
22
packages/core/platform/sources/flux-plunger.yaml
Normal file
22
packages/core/platform/sources/flux-plunger.yaml
Normal 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
|
||||
|
|
@ -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)) -}}
|
||||
|
|
|
|||
6
packages/system/flux-plunger/Chart.yaml
Normal file
6
packages/system/flux-plunger/Chart.yaml
Normal 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"
|
||||
19
packages/system/flux-plunger/Makefile
Normal file
19
packages/system/flux-plunger/Makefile
Normal 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
|
||||
23
packages/system/flux-plunger/images/flux-plunger/Dockerfile
Normal file
23
packages/system/flux-plunger/images/flux-plunger/Dockerfile
Normal 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"]
|
||||
55
packages/system/flux-plunger/templates/deployment.yaml
Normal file
55
packages/system/flux-plunger/templates/deployment.yaml
Normal 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
|
||||
37
packages/system/flux-plunger/templates/rbac.yaml
Normal file
37
packages/system/flux-plunger/templates/rbac.yaml
Normal 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 }}
|
||||
18
packages/system/flux-plunger/templates/service.yaml
Normal file
18
packages/system/flux-plunger/templates/service.yaml
Normal 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
|
||||
|
|
@ -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 }}
|
||||
1
packages/system/flux-plunger/values.yaml
Normal file
1
packages/system/flux-plunger/values.yaml
Normal file
|
|
@ -0,0 +1 @@
|
|||
image: "ghcr.io/cozystack/cozystack/flux-plunger:latest@sha256:6a6ec938973e6583c5e1573f7d25beddc1852a6699a089116b2407e29a564a72"
|
||||
Loading…
Add table
Add a link
Reference in a new issue