diff --git a/cmd/maintenance.go b/cmd/maintenance.go index 30921b1e3c..31b778f068 100644 --- a/cmd/maintenance.go +++ b/cmd/maintenance.go @@ -84,7 +84,6 @@ var maintenanceServices = map[service][]string{ var serviceName service func newMaintenanceCMD() *cobra.Command { - command := &cobra.Command{ Use: "maintenance", Short: "Maintenance runner", @@ -108,6 +107,15 @@ func newMaintenanceCMD() *cobra.Command { func (c *controller) runMaintenance(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() + serviceID = viper.GetString("SERVICE_ID") + + log := logr.FromContextOrDiscard(ctx).WithValues( + "instanceNamespace", viper.GetString("INSTANCE_NAMESPACE"), + "claimName", viper.GetString("CLAIM_NAME"), + "claimNamespace", viper.GetString("CLAIM_NAMESPACE"), + "serviceId", serviceID, + ) + kubeClient, err := client.NewWithWatch(ctrl.GetConfigOrDie(), client.Options{ Scheme: pkg.SetupScheme(), }) @@ -115,6 +123,22 @@ func (c *controller) runMaintenance(cmd *cobra.Command, _ []string) error { return fmt.Errorf("failed to initialize kube client: %w", err) } + controlNamespace := viper.GetString("CONTROL_NAMESPACE") + if controlNamespace == "" { + controlNamespace = "syn-appcat-control" + } + + maintenanceConfigMapName := viper.GetString("MAINTENANCE_CONFIGMAP_NAME") + if maintenanceConfigMapName == "" { + maintenanceConfigMapName = "maintenance-config" + } + + maintConfig, err := maintenance.GetMaintenanceConfig(ctx, kubeClient, maintenanceConfigMapName, controlNamespace, serviceID) + if err != nil { + log.Error(err, "Failed to read maintenance config, continuing with defaults") + maintConfig = maintenance.MaintenanceConfig{} + } + maintClient, err := getMaintClient(ctx, kubeClient) if err != nil { return fmt.Errorf("cannot initialize control plane client: %w", err) @@ -124,13 +148,6 @@ func (c *controller) runMaintenance(cmd *cobra.Command, _ []string) error { return fmt.Errorf("missing required environment variables: %w", err) } - log := logr.FromContextOrDiscard(ctx).WithValues( - "instanceNamespace", viper.GetString("INSTANCE_NAMESPACE"), - "claimName", viper.GetString("CLAIM_NAME"), - "claimNamespace", viper.GetString("CLAIM_NAMESPACE"), - "serviceId", viper.GetString("SERVICE_ID"), - ) - vh := getVersionHandler(kubeClient, log) var m Maintenance @@ -165,11 +182,19 @@ func (c *controller) runMaintenance(cmd *cobra.Command, _ []string) error { } pinImageTag := viper.GetString("PIN_IMAGE_TAG") - disableAppcatRelease, err := strconv.ParseBool(viper.GetString("DISABLE_APPCAT_RELEASE")) + envDisableAppcatRelease, err := strconv.ParseBool(viper.GetString("DISABLE_APPCAT_RELEASE")) if err != nil { return fmt.Errorf("cannot parse env variable DISABLE_APPCAT_RELEASE to bool: %w", err) } + disableAppcatRelease := envDisableAppcatRelease || maintConfig.DisableAppcatRelease + disableServiceMaint := maintConfig.DisableServiceMaint + + if disableAppcatRelease && disableServiceMaint { + log.Info("Both appcat release and service maintenance disabled, skipping all maintenance") + return nil + } + if disableAppcatRelease && pinImageTag != "" { log.Info("AppCat release disabled and image tag pinned, skipping...") return nil @@ -213,6 +238,12 @@ func (c *controller) runMaintenance(cmd *cobra.Command, _ []string) error { log.Info("Image tag pinned by user configuration, skipping service maintenance", "pinnedTag", pinImageTag) return nil } + + if disableServiceMaint { + log.Info("Service maintenance disabled via component, skipping") + return nil + } + if err := m.DoMaintenance(ctx); err != nil { return fmt.Errorf("maintenance failed: %w", err) } @@ -254,7 +285,6 @@ func validateMandatoryEnvs(s service) error { } func getVersionHandler(k8sCLient client.Client, log logr.Logger) release.VersionHandler { - opts := release.ReleaserOpts{ ClaimName: viper.GetString("CLAIM_NAME"), ClaimNamespace: viper.GetString("CLAIM_NAMESPACE"), @@ -280,7 +310,6 @@ func getStackgresClient() (*stackgres.StackgresClient, error) { } func getControlPlaneKubeConfig(ctx context.Context, kubeClient client.Client) (client.WithWatch, error) { - secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "controlclustercredentials", diff --git a/cmd/maintenance_test.go b/cmd/maintenance_test.go new file mode 100644 index 0000000000..b902c25e89 --- /dev/null +++ b/cmd/maintenance_test.go @@ -0,0 +1,92 @@ +package cmd + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vshn/appcat/v4/pkg/maintenance" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestShouldDisable(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + cmName := "maintenance-config" + cmNamespace := "syn-appcat-control" + + tests := []struct { + name string + service string + envDisableAppcatRelease string + cmData map[string]string + wantDisableAppcat bool + wantDisableService bool + }{ + { + name: "env disables appcat, cm does not", + service: "keycloak", + envDisableAppcatRelease: "true", + cmData: map[string]string{}, + wantDisableAppcat: true, + wantDisableService: false, + }, + { + name: "cm disables both, env does not", + service: "redis", + envDisableAppcatRelease: "false", + cmData: map[string]string{ + "redis.disableServiceMaint": "true", + "redis.disableAppcatRelease": "true", + }, + wantDisableAppcat: true, + wantDisableService: true, + }, + { + name: "both sources disable appcat", + service: "mariadb", + envDisableAppcatRelease: "true", + cmData: map[string]string{ + "mariadb.disableAppcatRelease": "true", + }, + wantDisableAppcat: true, + wantDisableService: false, + }, + { + name: "nothing disabled", + service: "nextcloud", + envDisableAppcatRelease: "false", + cmData: map[string]string{}, + wantDisableAppcat: false, + wantDisableService: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: cmName, + Namespace: cmNamespace, + }, + Data: tt.cmData, + } + c := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(cm).Build() + + cfg, err := maintenance.GetMaintenanceConfig(context.Background(), c, cmName, cmNamespace, tt.service) + assert.NoError(t, err) + + envDisable := tt.envDisableAppcatRelease == "true" + + disableAppcat := envDisable || cfg.DisableAppcatRelease + disableService := cfg.DisableServiceMaint + + assert.Equal(t, tt.wantDisableAppcat, disableAppcat) + assert.Equal(t, tt.wantDisableService, disableService) + }) + } +} diff --git a/pkg/comp-functions/functions/common/maintenance/maintenance.go b/pkg/comp-functions/functions/common/maintenance/maintenance.go index 9406da5a12..8b4d0b9cb1 100644 --- a/pkg/comp-functions/functions/common/maintenance/maintenance.go +++ b/pkg/comp-functions/functions/common/maintenance/maintenance.go @@ -362,6 +362,14 @@ func (m *Maintenance) buildMaintenancePodTemplateSpec(imageTag, serviceAccount s Name: "DISABLE_APPCAT_RELEASE", Value: strconv.FormatBool(m.schedule.DisableAppcatRelease), }, + { + Name: "CONTROL_NAMESPACE", + Value: m.svc.Config.Data["controlNamespace"], + }, + { + Name: "MAINTENANCE_CONFIGMAP_NAME", + Value: m.svc.Config.Data["maintenanceConfigMapName"], + }, } return corev1.PodTemplateSpec{ diff --git a/pkg/maintenance/config.go b/pkg/maintenance/config.go new file mode 100644 index 0000000000..fdf8fc7330 --- /dev/null +++ b/pkg/maintenance/config.go @@ -0,0 +1,39 @@ +package maintenance + +import ( + "context" + "strconv" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// MaintenanceConfig holds the per-service mainteancne configuration +type MaintenanceConfig struct { + DisableServiceMaint bool + DisableAppcatRelease bool +} + +func GetMaintenanceConfig(ctx context.Context, c client.Reader, cmName, namespace, serviceID string) (MaintenanceConfig, error) { + conf := MaintenanceConfig{} + + cm := &corev1.ConfigMap{} + + err := c.Get(ctx, types.NamespacedName{Name: cmName, Namespace: namespace}, cm) + if err != nil { + return conf, err + } + + conf.DisableServiceMaint = parseBool(cm.Data[serviceID+".disableServiceMaint"]) + conf.DisableAppcatRelease = parseBool(cm.Data[serviceID+".disableAppcatRelease"]) + + return conf, nil +} + +// parseBool will parse the string to a boolean +// Invalid and empty strings are treated as false +func parseBool(s string) bool { + v, _ := strconv.ParseBool(s) + return v +} diff --git a/pkg/maintenance/config_test.go b/pkg/maintenance/config_test.go new file mode 100644 index 0000000000..8eac075b38 --- /dev/null +++ b/pkg/maintenance/config_test.go @@ -0,0 +1,100 @@ +package maintenance + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestGetMaintenanceConfig(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + tests := []struct { + name string + service string + configMap *corev1.ConfigMap + wantServiceDis bool + wantAppcatDis bool + wantErr bool + }{ + { + name: "both disabled for keycloak", + service: "keycloak", + configMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "maintenance-config", + Namespace: "syn-appcat-control", + }, + Data: map[string]string{ + "keycloak.disableServiceMaint": "true", + "keycloak.disableAppcatRelease": "true", + }, + }, + wantServiceDis: true, + wantAppcatDis: true, + }, + { + name: "nothing disabled for redis", + service: "redis", + configMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "maintenance-config", + Namespace: "syn-appcat-control", + }, + Data: map[string]string{ + "redis.disableServiceMaint": "false", + "redis.disableAppcatRelease": "false", + }, + }, + wantServiceDis: false, + wantAppcatDis: false, + }, + { + name: "missing keys default to false", + service: "mariadb", + configMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "maintenance-config", + Namespace: "syn-appcat-control", + }, + Data: map[string]string{}, + }, + wantServiceDis: false, + wantAppcatDis: false, + }, + { + name: "missing configmap returns an error, defaults false", + service: "forgejo", + configMap: nil, + wantServiceDis: false, + wantAppcatDis: false, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + objs := []runtime.Object{} + if tt.configMap != nil { + objs = append(objs, tt.configMap) + } + c := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objs...).Build() + + cfg, err := GetMaintenanceConfig(context.Background(), c, "maintenance-config", "syn-appcat-control", tt.service) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantServiceDis, cfg.DisableServiceMaint) + assert.Equal(t, tt.wantAppcatDis, cfg.DisableAppcatRelease) + }) + } +}