diff --git a/apis/vshn/v1/common_types.go b/apis/vshn/v1/common_types.go index 24c5072f01..a2fd17689e 100644 --- a/apis/vshn/v1/common_types.go +++ b/apis/vshn/v1/common_types.go @@ -86,6 +86,19 @@ type VSHNDBaaSMaintenanceScheduleSpec struct { // TimeOfDay for installing updates in UTC. // Format: "hh:mm:ss". TimeOfDay TimeOfDay `json:"timeOfDay,omitempty"` + + // PinImageTag allows pinning the service to a specific image tag. + // When set, the exact specified tag will be used, even if it's older than the currently deployed version. + // WARNING: User takes full responsibility for version management and security updates. + // Downgrades are allowed when pinning - the customer assumes all risk. + PinImageTag string `json:"pinImageTag,omitempty"` + + // +kubebuilder:default=false + // DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + // When enabled, the instance will not automatically receive new AppCat composition revisions + // which may contain bug fixes, security patches, and new features. + // WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + DisableAppcatRelease bool `json:"disableAppcatRelease,omitempty"` } // GetMaintenanceDayOfWeek returns the currently set day of week @@ -108,6 +121,21 @@ func (n *VSHNDBaaSMaintenanceScheduleSpec) SetMaintenanceTimeOfDay(tod TimeOfDay n.TimeOfDay = tod } +// GetPinImageTag returns the pinned image tag if set +func (n *VSHNDBaaSMaintenanceScheduleSpec) GetPinImageTag() string { + return n.PinImageTag +} + +// IsPinImageTagSet returns true if an image tag is pinned +func (n *VSHNDBaaSMaintenanceScheduleSpec) IsPinImageTagSet() bool { + return n.PinImageTag != "" +} + +// IsAppcatReleaseDisabled returns true if AppCat release updates are disabled +func (n *VSHNDBaaSMaintenanceScheduleSpec) IsAppcatReleaseDisabled() bool { + return n.DisableAppcatRelease +} + // VSHNSizeSpec contains settings to control the sizing of a service. type VSHNSizeSpec struct { // CPU defines the amount of Kubernetes CPUs for an instance. diff --git a/apis/vshn/v1/common_types_test.go b/apis/vshn/v1/common_types_test.go index fe9931cb5a..8db11e8e6f 100644 --- a/apis/vshn/v1/common_types_test.go +++ b/apis/vshn/v1/common_types_test.go @@ -108,3 +108,59 @@ func Test_IsSet(t *testing.T) { }) } } + +func Test_PinImageTag(t *testing.T) { + tests := []struct { + name string + scheduleSpec VSHNDBaaSMaintenanceScheduleSpec + wantTag string + wantIsSet bool + }{ + { + name: "GivenPinImageTagSet_ThenExpectTagAndTrue", + scheduleSpec: VSHNDBaaSMaintenanceScheduleSpec{ + PinImageTag: "7.2.5", + }, + wantTag: "7.2.5", + wantIsSet: true, + }, + { + name: "GivenDefaultValue_ThenExpectEmptyAndFalse", + scheduleSpec: VSHNDBaaSMaintenanceScheduleSpec{}, + wantTag: "", + wantIsSet: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.wantTag, tt.scheduleSpec.GetPinImageTag()) + assert.Equal(t, tt.wantIsSet, tt.scheduleSpec.IsPinImageTagSet()) + }) + } +} + +func Test_IsAppcatReleaseDisabled(t *testing.T) { + tests := []struct { + name string + scheduleSpec VSHNDBaaSMaintenanceScheduleSpec + want bool + }{ + { + name: "GivenDisableAppcatReleaseTrue_ThenExpectTrue", + scheduleSpec: VSHNDBaaSMaintenanceScheduleSpec{ + DisableAppcatRelease: true, + }, + want: true, + }, + { + name: "GivenDefaultValue_ThenExpectFalse", + scheduleSpec: VSHNDBaaSMaintenanceScheduleSpec{}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.scheduleSpec.IsAppcatReleaseDisabled()) + }) + } +} diff --git a/apis/vshn/v1/dbaas_vshn_forgejo.go b/apis/vshn/v1/dbaas_vshn_forgejo.go index f19f7e58e7..d5411c0668 100644 --- a/apis/vshn/v1/dbaas_vshn_forgejo.go +++ b/apis/vshn/v1/dbaas_vshn_forgejo.go @@ -172,6 +172,8 @@ type VSHNForgejoStatus struct { // InitialMaintenance tracks the status of the initial maintenance job, // including when it ran and whether it succeeded or failed. InitialMaintenance InitialMaintenanceStatus `json:"initialMaintenance,omitempty"` + // CurrentReleaseTag contains the currently deployed image tag. + CurrentReleaseTag string `json:"currentReleaseTag,omitempty"` // ResourceStatus represents the observed state of a managed resource. xpv1.ResourceStatus `json:",inline"` diff --git a/apis/vshn/v1/dbaas_vshn_keycloak.go b/apis/vshn/v1/dbaas_vshn_keycloak.go index 4e934ca009..c125ab9a51 100644 --- a/apis/vshn/v1/dbaas_vshn_keycloak.go +++ b/apis/vshn/v1/dbaas_vshn_keycloak.go @@ -227,6 +227,8 @@ type VSHNKeycloakStatus struct { // InitialMaintenance tracks the status of the initial maintenance job, // including when it ran and whether it succeeded or failed. InitialMaintenance InitialMaintenanceStatus `json:"initialMaintenance,omitempty"` + // CurrentReleaseTag contains the currently deployed image tag. + CurrentReleaseTag string `json:"currentReleaseTag,omitempty"` // ResourceStatus represents the observed state of a managed resource. xpv1.ResourceStatus `json:",inline"` // LastConfigHash is the hash of last applied customConfigurationRef. diff --git a/apis/vshn/v1/dbaas_vshn_postgresql.go b/apis/vshn/v1/dbaas_vshn_postgresql.go index 17a4589034..941cc35dc6 100644 --- a/apis/vshn/v1/dbaas_vshn_postgresql.go +++ b/apis/vshn/v1/dbaas_vshn_postgresql.go @@ -168,9 +168,11 @@ type VSHNPostgreSQLServiceSpec struct { EnableEnvoy bool `json:"enableEnvoy,omitempty"` // +kubebuilder:default=true - // This is default option if neither repack or vacuum are selected + // RepackEnabled defines if `pg_repack` should be performed during the maintenance. Defaults to true. RepackEnabled bool `json:"repackEnabled,omitempty"` + // +kubebuilder:default=false + // VacuumEnabled defines if `VACUUM` should be performed during the maintenace. Defaults to false. VacuumEnabled bool `json:"vacuumEnabled,omitempty"` // Access defines additional users and databases for this instance. diff --git a/apis/vshn/v1/dbaas_vshn_redis.go b/apis/vshn/v1/dbaas_vshn_redis.go index fd5bdc9718..27553bd69b 100644 --- a/apis/vshn/v1/dbaas_vshn_redis.go +++ b/apis/vshn/v1/dbaas_vshn_redis.go @@ -156,6 +156,8 @@ type VSHNRedisStatus struct { // InitialMaintenance tracks the status of the initial maintenance job, // including when it ran and whether it succeeded or failed. InitialMaintenance InitialMaintenanceStatus `json:"initialMaintenance,omitempty"` + // CurrentReleaseTag contains the currently deployed image tag. + CurrentReleaseTag string `json:"currentReleaseTag,omitempty"` // ResourceStatus represents the observed state of a managed resource. xpv1.ResourceStatus `json:",inline"` } diff --git a/apis/vshn/v1/vshn_minio.go b/apis/vshn/v1/vshn_minio.go index 4d70862a33..07fee3dfe9 100644 --- a/apis/vshn/v1/vshn_minio.go +++ b/apis/vshn/v1/vshn_minio.go @@ -105,6 +105,8 @@ type VSHNMinioStatus struct { // InitialMaintenance tracks the status of the initial maintenance job, // including when it ran and whether it succeeded or failed. InitialMaintenance InitialMaintenanceStatus `json:"initialMaintenance,omitempty"` + // CurrentReleaseTag contains the currently deployed image tag. + CurrentReleaseTag string `json:"currentReleaseTag,omitempty"` // ResourceStatus represents the observed state of a managed resource. xpv1.ResourceStatus `json:",inline"` } diff --git a/apis/vshn/v1/vshn_nextcloud.go b/apis/vshn/v1/vshn_nextcloud.go index 93f552e4d7..69eba4cda6 100644 --- a/apis/vshn/v1/vshn_nextcloud.go +++ b/apis/vshn/v1/vshn_nextcloud.go @@ -184,6 +184,8 @@ type VSHNNextcloudStatus struct { // InitialMaintenance tracks the status of the initial maintenance job, // including when it ran and whether it succeeded or failed. InitialMaintenance InitialMaintenanceStatus `json:"initialMaintenance,omitempty"` + // CurrentReleaseTag contains the currently deployed image tag. + CurrentReleaseTag string `json:"currentReleaseTag,omitempty"` // ResourceStatus represents the observed state of a managed resource. xpv1.ResourceStatus `json:",inline"` } diff --git a/cmd/maintenance.go b/cmd/maintenance.go index fedb2029ec..15dc507bf3 100644 --- a/cmd/maintenance.go +++ b/cmd/maintenance.go @@ -164,6 +164,17 @@ func (c *controller) runMaintenance(cmd *cobra.Command, _ []string) error { panic("service name is mandatory") } + pinImageTag := viper.GetString("PIN_IMAGE_TAG") + disableAppcatRelease, 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) + } + + if disableAppcatRelease && pinImageTag != "" { + log.Info("AppCat release disabled and image tag pinned, skipping...") + return nil + } + if err = errors.Join( // Run backup before any changes, then release, then maintenance func() error { @@ -176,6 +187,11 @@ func (c *controller) runMaintenance(cmd *cobra.Command, _ []string) error { return nil }(), func() error { + if disableAppcatRelease { + log.Info("AppCat release updates disabled by user configuration") + return nil + } + enabled, err := strconv.ParseBool(viper.GetString("RELEASE_MANAGEMENT_ENABLED")) if err != nil { return fmt.Errorf("cannot determine if release management is enabled: %w", err) @@ -196,7 +212,13 @@ func (c *controller) runMaintenance(cmd *cobra.Command, _ []string) error { return m.ReleaseLatest(ctx, enabled, maintClient, minAge) }(), - m.DoMaintenance(ctx), + func() error { + if pinImageTag != "" { + log.Info("Image tag pinned by user configuration, skipping service maintenance", "pinnedTag", pinImageTag) + return nil + } + return m.DoMaintenance(ctx) + }(), ); err != nil { return fmt.Errorf("maintenance failed: %w", err) } diff --git a/crds/vshn.appcat.vshn.io_vshnforgejoes.yaml b/crds/vshn.appcat.vshn.io_vshnforgejoes.yaml index a8944e9e27..0a75381d42 100644 --- a/crds/vshn.appcat.vshn.io_vshnforgejoes.yaml +++ b/crds/vshn.appcat.vshn.io_vshnforgejoes.yaml @@ -95,6 +95,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -4758,6 +4773,9 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + currentReleaseTag: + description: CurrentReleaseTag contains the currently deployed image tag. + type: string initialMaintenance: description: |- InitialMaintenance tracks the status of the initial maintenance job, @@ -4852,6 +4870,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. diff --git a/crds/vshn.appcat.vshn.io_vshnkeycloaks.yaml b/crds/vshn.appcat.vshn.io_vshnkeycloaks.yaml index fdf41cd997..fb5163c3c4 100644 --- a/crds/vshn.appcat.vshn.io_vshnkeycloaks.yaml +++ b/crds/vshn.appcat.vshn.io_vshnkeycloaks.yaml @@ -98,6 +98,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -4810,6 +4825,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -9496,7 +9526,7 @@ spec: x-kubernetes-preserve-unknown-fields: true repackEnabled: default: true - description: This is default option if neither repack or vacuum are selected + description: RepackEnabled defines if `pg_repack` should be performed during the maintenance. Defaults to true. type: boolean serviceLevel: default: besteffort @@ -9518,6 +9548,7 @@ spec: default: {} vacuumEnabled: default: false + description: VacuumEnabled defines if `VACUUM` should be performed during the maintenace. Defaults to false. type: boolean type: object default: {} @@ -9708,6 +9739,9 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + currentReleaseTag: + description: CurrentReleaseTag contains the currently deployed image tag. + type: string initialMaintenance: description: |- InitialMaintenance tracks the status of the initial maintenance job, @@ -9808,6 +9842,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. diff --git a/crds/vshn.appcat.vshn.io_vshnmariadbs.yaml b/crds/vshn.appcat.vshn.io_vshnmariadbs.yaml index aab40a5f0a..0c9a7b7fa4 100644 --- a/crds/vshn.appcat.vshn.io_vshnmariadbs.yaml +++ b/crds/vshn.appcat.vshn.io_vshnmariadbs.yaml @@ -104,6 +104,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -5068,6 +5083,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. diff --git a/crds/vshn.appcat.vshn.io_vshnminios.yaml b/crds/vshn.appcat.vshn.io_vshnminios.yaml index a7d77c3981..c6b813e044 100644 --- a/crds/vshn.appcat.vshn.io_vshnminios.yaml +++ b/crds/vshn.appcat.vshn.io_vshnminios.yaml @@ -96,6 +96,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -4700,6 +4715,9 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + currentReleaseTag: + description: CurrentReleaseTag contains the currently deployed image tag. + type: string initialMaintenance: description: |- InitialMaintenance tracks the status of the initial maintenance job, @@ -4832,6 +4850,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. diff --git a/crds/vshn.appcat.vshn.io_vshnnextclouds.yaml b/crds/vshn.appcat.vshn.io_vshnnextclouds.yaml index e29e6f1e9c..b044e585f4 100644 --- a/crds/vshn.appcat.vshn.io_vshnnextclouds.yaml +++ b/crds/vshn.appcat.vshn.io_vshnnextclouds.yaml @@ -105,6 +105,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -4720,6 +4735,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -9406,7 +9436,7 @@ spec: x-kubernetes-preserve-unknown-fields: true repackEnabled: default: true - description: This is default option if neither repack or vacuum are selected + description: RepackEnabled defines if `pg_repack` should be performed during the maintenance. Defaults to true. type: boolean serviceLevel: default: besteffort @@ -9428,6 +9458,7 @@ spec: default: {} vacuumEnabled: default: false + description: VacuumEnabled defines if `VACUUM` should be performed during the maintenace. Defaults to false. type: boolean type: object default: {} @@ -9614,6 +9645,9 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + currentReleaseTag: + description: CurrentReleaseTag contains the currently deployed image tag. + type: string initialMaintenance: description: |- InitialMaintenance tracks the status of the initial maintenance job, @@ -9708,6 +9742,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. diff --git a/crds/vshn.appcat.vshn.io_vshnpostgresqls.yaml b/crds/vshn.appcat.vshn.io_vshnpostgresqls.yaml index b18c6e1b91..bd40817f43 100644 --- a/crds/vshn.appcat.vshn.io_vshnpostgresqls.yaml +++ b/crds/vshn.appcat.vshn.io_vshnpostgresqls.yaml @@ -156,6 +156,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -4844,7 +4859,7 @@ spec: x-kubernetes-preserve-unknown-fields: true repackEnabled: default: true - description: This is default option if neither repack or vacuum are selected + description: RepackEnabled defines if `pg_repack` should be performed during the maintenance. Defaults to true. type: boolean serviceLevel: default: besteffort @@ -4866,6 +4881,7 @@ spec: default: {} vacuumEnabled: default: false + description: VacuumEnabled defines if `VACUUM` should be performed during the maintenace. Defaults to false. type: boolean type: object default: {} @@ -5572,6 +5588,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. diff --git a/crds/vshn.appcat.vshn.io_vshnredis.yaml b/crds/vshn.appcat.vshn.io_vshnredis.yaml index 328b2fa5bd..cf976c1e54 100644 --- a/crds/vshn.appcat.vshn.io_vshnredis.yaml +++ b/crds/vshn.appcat.vshn.io_vshnredis.yaml @@ -98,6 +98,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -4803,6 +4818,9 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + currentReleaseTag: + description: CurrentReleaseTag contains the currently deployed image tag. + type: string initialMaintenance: description: |- InitialMaintenance tracks the status of the initial maintenance job, @@ -4972,6 +4990,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. diff --git a/crds/vshn.appcat.vshn.io_xvshnforgejoes.yaml b/crds/vshn.appcat.vshn.io_xvshnforgejoes.yaml index 1710a9066f..c2f88ea654 100644 --- a/crds/vshn.appcat.vshn.io_xvshnforgejoes.yaml +++ b/crds/vshn.appcat.vshn.io_xvshnforgejoes.yaml @@ -140,6 +140,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -5661,6 +5676,10 @@ spec: description: Conditions of the resource. type: array x-kubernetes-list-type: map + currentReleaseTag: + description: CurrentReleaseTag contains the currently deployed image + tag. + type: string initialMaintenance: description: |- InitialMaintenance tracks the status of the initial maintenance job, @@ -5758,6 +5777,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. diff --git a/crds/vshn.appcat.vshn.io_xvshnkeycloaks.yaml b/crds/vshn.appcat.vshn.io_xvshnkeycloaks.yaml index 8c1b087099..e41600ee68 100644 --- a/crds/vshn.appcat.vshn.io_xvshnkeycloaks.yaml +++ b/crds/vshn.appcat.vshn.io_xvshnkeycloaks.yaml @@ -151,6 +151,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -5561,6 +5576,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -11302,8 +11332,9 @@ spec: x-kubernetes-preserve-unknown-fields: true repackEnabled: default: true - description: This is default option if neither repack - or vacuum are selected + description: RepackEnabled defines if `pg_repack` + should be performed during the maintenance. Defaults + to true. type: boolean serviceLevel: default: besteffort @@ -11326,6 +11357,9 @@ spec: type: object vacuumEnabled: default: false + description: VacuumEnabled defines if `VACUUM` should + be performed during the maintenace. Defaults to + false. type: boolean type: object size: @@ -11705,6 +11739,10 @@ spec: description: Conditions of the resource. type: array x-kubernetes-list-type: map + currentReleaseTag: + description: CurrentReleaseTag contains the currently deployed image + tag. + type: string initialMaintenance: description: |- InitialMaintenance tracks the status of the initial maintenance job, @@ -11808,6 +11846,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. diff --git a/crds/vshn.appcat.vshn.io_xvshnmariadbs.yaml b/crds/vshn.appcat.vshn.io_xvshnmariadbs.yaml index 6610b5eb58..860194d903 100644 --- a/crds/vshn.appcat.vshn.io_xvshnmariadbs.yaml +++ b/crds/vshn.appcat.vshn.io_xvshnmariadbs.yaml @@ -157,6 +157,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -5995,6 +6010,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. diff --git a/crds/vshn.appcat.vshn.io_xvshnminios.yaml b/crds/vshn.appcat.vshn.io_xvshnminios.yaml index 4e0b98cd01..6b14f0b19a 100644 --- a/crds/vshn.appcat.vshn.io_xvshnminios.yaml +++ b/crds/vshn.appcat.vshn.io_xvshnminios.yaml @@ -150,6 +150,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -5607,6 +5622,10 @@ spec: description: Conditions of the resource. type: array x-kubernetes-list-type: map + currentReleaseTag: + description: CurrentReleaseTag contains the currently deployed image + tag. + type: string initialMaintenance: description: |- InitialMaintenance tracks the status of the initial maintenance job, @@ -5746,6 +5765,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. diff --git a/crds/vshn.appcat.vshn.io_xvshnnextclouds.yaml b/crds/vshn.appcat.vshn.io_xvshnnextclouds.yaml index ca9c5701b1..94e7c44810 100644 --- a/crds/vshn.appcat.vshn.io_xvshnnextclouds.yaml +++ b/crds/vshn.appcat.vshn.io_xvshnnextclouds.yaml @@ -149,6 +149,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -5455,6 +5470,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -11196,8 +11226,9 @@ spec: x-kubernetes-preserve-unknown-fields: true repackEnabled: default: true - description: This is default option if neither repack - or vacuum are selected + description: RepackEnabled defines if `pg_repack` + should be performed during the maintenance. Defaults + to true. type: boolean serviceLevel: default: besteffort @@ -11220,6 +11251,9 @@ spec: type: object vacuumEnabled: default: false + description: VacuumEnabled defines if `VACUUM` should + be performed during the maintenace. Defaults to + false. type: boolean type: object size: @@ -11593,6 +11627,10 @@ spec: description: Conditions of the resource. type: array x-kubernetes-list-type: map + currentReleaseTag: + description: CurrentReleaseTag contains the currently deployed image + tag. + type: string initialMaintenance: description: |- InitialMaintenance tracks the status of the initial maintenance job, @@ -11690,6 +11728,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. diff --git a/crds/vshn.appcat.vshn.io_xvshnpostgresqls.yaml b/crds/vshn.appcat.vshn.io_xvshnpostgresqls.yaml index 38e35ab843..26b8636c99 100644 --- a/crds/vshn.appcat.vshn.io_xvshnpostgresqls.yaml +++ b/crds/vshn.appcat.vshn.io_xvshnpostgresqls.yaml @@ -160,6 +160,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -5533,8 +5548,8 @@ spec: x-kubernetes-preserve-unknown-fields: true repackEnabled: default: true - description: This is default option if neither repack or vacuum - are selected + description: RepackEnabled defines if `pg_repack` should be + performed during the maintenance. Defaults to true. type: boolean serviceLevel: default: besteffort @@ -5557,6 +5572,8 @@ spec: type: object vacuumEnabled: default: false + description: VacuumEnabled defines if `VACUUM` should be performed + during the maintenace. Defaults to false. type: boolean type: object size: @@ -6348,6 +6365,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. diff --git a/crds/vshn.appcat.vshn.io_xvshnredis.yaml b/crds/vshn.appcat.vshn.io_xvshnredis.yaml index fc2bba5961..35c417ae7a 100644 --- a/crds/vshn.appcat.vshn.io_xvshnredis.yaml +++ b/crds/vshn.appcat.vshn.io_xvshnredis.yaml @@ -152,6 +152,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. @@ -5718,6 +5733,10 @@ spec: description: Conditions of the resource. type: array x-kubernetes-list-type: map + currentReleaseTag: + description: CurrentReleaseTag contains the currently deployed image + tag. + type: string initialMaintenance: description: |- InitialMaintenance tracks the status of the initial maintenance job, @@ -5897,6 +5916,21 @@ spec: - saturday - sunday type: string + disableAppcatRelease: + default: false + description: |- + DisableAppcatRelease disables automatic AppCat composition revision rollouts during maintenance windows. + When enabled, the instance will not automatically receive new AppCat composition revisions + which may contain bug fixes, security patches, and new features. + WARNING: Strongly discouraged - may leave instance without security patches and bug fixes. + type: boolean + pinImageTag: + description: |- + PinImageTag allows pinning the service to a specific image tag. + When set, the exact specified tag will be used, even if it's older than the currently deployed version. + WARNING: User takes full responsibility for version management and security updates. + Downgrades are allowed when pinning - the customer assumes all risk. + type: string timeOfDay: description: |- TimeOfDay for installing updates in UTC. diff --git a/pkg/comp-functions/functions/common/maintenance/maintenance.go b/pkg/comp-functions/functions/common/maintenance/maintenance.go index 8c4096f515..71d5d2e6bb 100644 --- a/pkg/comp-functions/functions/common/maintenance/maintenance.go +++ b/pkg/comp-functions/functions/common/maintenance/maintenance.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "regexp" + "strconv" "time" "github.com/blang/semver/v4" @@ -348,6 +349,14 @@ func (m *Maintenance) buildMaintenancePodTemplateSpec(imageTag, serviceAccount s Name: "MINIMUM_REVISION_AGE", Value: m.svc.Config.Data["minimumRevisionAge"], }, + { + Name: "PIN_IMAGE_TAG", + Value: m.schedule.PinImageTag, + }, + { + Name: "DISABLE_APPCAT_RELEASE", + Value: strconv.FormatBool(m.schedule.DisableAppcatRelease), + }, } return corev1.PodTemplateSpec{ @@ -503,12 +512,18 @@ func (m *Maintenance) createInitialMaintenanceJob(_ context.Context) error { return m.svc.SetDesiredKubeObject(job, m.getInitialMaintenanceJobName(), kubeOpts...) } -// SetReleaseVersion sets the version from the claim if it's a new instance otherwise it is managed by maintenance function +// SetReleaseVersion sets the version from the claim if it's a new instance otherwise it is managed by maintenance function. // It will return the concrete observed version as well. // If the desired values contain a higher version than either the observed or the comp version, it will take precedence. -func SetReleaseVersion(ctx context.Context, version string, desiredValues map[string]interface{}, observedValues map[string]interface{}, fields []string) (string, error) { +func SetReleaseVersion(ctx context.Context, version string, desiredValues map[string]interface{}, observedValues map[string]interface{}, fields []string, pinImageTag string) (string, error) { l := controllerruntime.LoggerFrom(ctx) + // If an image tag is pinned, use it unconditionally, downgrades ARE allowed on user's own risk + if pinImageTag != "" { + l.Info("Using pinned image tag", "pinnedTag", pinImageTag) + return pinImageTag, unstructured.SetNestedField(desiredValues, pinImageTag, fields...) + } + observedValueVersion, _, err := unstructured.NestedString(observedValues, fields...) if err != nil { return "", fmt.Errorf("cannot get image tag from values in release: %v", err) diff --git a/pkg/comp-functions/functions/common/maintenance/maintenance_test.go b/pkg/comp-functions/functions/common/maintenance/maintenance_test.go index cb18d23852..7048cc4b5c 100644 --- a/pkg/comp-functions/functions/common/maintenance/maintenance_test.go +++ b/pkg/comp-functions/functions/common/maintenance/maintenance_test.go @@ -328,3 +328,100 @@ func createMaintenanceSecretTest(instanceNamespace, sgNamespace, resourceName st }, } } + +func TestSetReleaseVersion(t *testing.T) { + ctx := context.TODO() + fields := []string{"image", "tag"} + + tests := []struct { + name string + claimVersion string + desiredVersion string + observedVersion string + pinImageTag string + wantVersion string + wantErr bool + }{ + // Normal maintenance (pinImageTag empty) + { + name: "NormalMaintenance_NoObserved_UseClaimVersion", + claimVersion: "7.0.0", + wantVersion: "7.0.0", + }, + { + name: "NormalMaintenance_ObservedHigher_KeepObserved", + claimVersion: "7.0.0", + desiredVersion: "7.2.0", + observedVersion: "7.2.0", + wantVersion: "7.2.0", + }, + // Pinned image tag scenarios (user manages versions, downgrades allowed) + { + name: "PinnedTag_NoObserved_UsePinnedTag", + claimVersion: "7.0.0", + pinImageTag: "7.2.5", + wantVersion: "7.2.5", + }, + { + name: "PinnedTag_ObservedHigher_AllowDowngrade", + claimVersion: "7.0.0", + observedVersion: "7.4.0", + pinImageTag: "7.2.5", + wantVersion: "7.2.5", + }, + { + name: "PinnedTag_ObservedLower_AllowUpgrade", + claimVersion: "7.0.0", + observedVersion: "7.0.0", + pinImageTag: "7.2.5", + wantVersion: "7.2.5", + }, + { + name: "PinnedTag_IgnoresClaimVersion", + claimVersion: "8.0.0", + observedVersion: "7.2.0", + pinImageTag: "7.2.5", + wantVersion: "7.2.5", + }, + { + name: "PinnedTag_InvalidClaimVersion_StillUsesPinned", + claimVersion: "invalid", + pinImageTag: "7.2.5", + wantVersion: "7.2.5", + }, + // Error cases (only when pinImageTag is empty) + { + name: "InvalidClaimVersion_NoPinnedTag_Error", + claimVersion: "invalid", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + desiredValues := makeVersionMap(tt.desiredVersion) + observedValues := makeVersionMap(tt.observedVersion) + + gotVersion, err := SetReleaseVersion(ctx, tt.claimVersion, desiredValues, observedValues, fields, tt.pinImageTag) + + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.wantVersion, gotVersion) + }) + } +} + +func makeVersionMap(version string) map[string]any { + if version == "" { + return map[string]any{} + } + return map[string]any{ + "image": map[string]any{ + "tag": version, + }, + } +} diff --git a/pkg/comp-functions/functions/vshnforgejo/deploy.go b/pkg/comp-functions/functions/vshnforgejo/deploy.go index ddf877a120..1915754402 100644 --- a/pkg/comp-functions/functions/vshnforgejo/deploy.go +++ b/pkg/comp-functions/functions/vshnforgejo/deploy.go @@ -281,11 +281,19 @@ func addForgejo(ctx context.Context, svc *runtime.ServiceRuntime, comp *vshnv1.V if err != nil { return fmt.Errorf("cannot get observed release values: %w", err) } - _, err = maintenance.SetReleaseVersion(ctx, comp.Spec.Parameters.Service.MajorVersion, values, observedValues, []string{"image", "tag"}) + releaseTag, err := maintenance.SetReleaseVersion(ctx, comp.Spec.Parameters.Service.MajorVersion, values, observedValues, []string{"image", "tag"}, comp.Spec.Parameters.Maintenance.PinImageTag) if err != nil { return fmt.Errorf("cannot set forgejo version for release: %w", err) } + // Update status with current release tag + if releaseTag != "" { + comp.Status.CurrentReleaseTag = releaseTag + if err := svc.SetDesiredCompositeStatus(comp); err != nil { + svc.Log.Error(err, "cannot update CurrentReleaseTag in status") + } + } + release, err := common.NewRelease(ctx, svc, comp, values, comp.GetName()+"-release") if err != nil { return err diff --git a/pkg/comp-functions/functions/vshnforgejo/maintenance.go b/pkg/comp-functions/functions/vshnforgejo/maintenance.go index f3bbfe94cb..886e325204 100644 --- a/pkg/comp-functions/functions/vshnforgejo/maintenance.go +++ b/pkg/comp-functions/functions/vshnforgejo/maintenance.go @@ -14,8 +14,10 @@ import ( // AddMaintenanceJob will add a job to do the maintenance for the instance func AddMaintenanceJob(ctx context.Context, comp *vshnv1.VSHNForgejo, svc *runtime.ServiceRuntime) *xfnproto.Result { - if err := svc.GetObservedComposite(comp); err != nil { - return runtime.NewFatalResult(fmt.Errorf("can't get composite: %w", err)) + if err := svc.GetDesiredComposite(comp); err != nil { + if err := svc.GetObservedComposite(comp); err != nil { + return runtime.NewFatalResult(fmt.Errorf("can't get composite: %w", err)) + } } maintTime := common.SetRandomMaintenanceSchedule(comp) diff --git a/pkg/comp-functions/functions/vshnkeycloak/deploy.go b/pkg/comp-functions/functions/vshnkeycloak/deploy.go index 85ea7182fe..c36c33076e 100644 --- a/pkg/comp-functions/functions/vshnkeycloak/deploy.go +++ b/pkg/comp-functions/functions/vshnkeycloak/deploy.go @@ -853,12 +853,20 @@ func newRelease(ctx context.Context, svc *runtime.ServiceRuntime, comp *vshnv1.V return nil, fmt.Errorf("cannot get observed release values: %w", err) } - observedVersion, err := maintenance.SetReleaseVersion(ctx, comp.Spec.Parameters.Service.Version, values, observedValues, []string{"image", "tag"}) + releaseTag, err := maintenance.SetReleaseVersion(ctx, comp.Spec.Parameters.Service.Version, values, observedValues, []string{"image", "tag"}, comp.Spec.Parameters.Maintenance.PinImageTag) if err != nil { return nil, fmt.Errorf("cannot set keycloak version for release: %w", err) } - err = addInitContainer(comp, values, observedVersion) + // Update status with current release tag + if releaseTag != "" { + comp.Status.CurrentReleaseTag = releaseTag + if err := svc.SetDesiredCompositeStatus(comp); err != nil { + svc.Log.Error(err, "cannot update CurrentReleaseTag in status") + } + } + + err = addInitContainer(comp, values, releaseTag) if err != nil { return nil, err } diff --git a/pkg/comp-functions/functions/vshnmariadb/backup.go b/pkg/comp-functions/functions/vshnmariadb/backup.go index 4595e30b1b..1424403712 100644 --- a/pkg/comp-functions/functions/vshnmariadb/backup.go +++ b/pkg/comp-functions/functions/vshnmariadb/backup.go @@ -27,6 +27,12 @@ func AddBackupMariadb(ctx context.Context, comp *vshnv1.VSHNMariaDB, svc *runtim return runtime.NewFatalResult(fmt.Errorf("failed to parse composite: %w", err)) } + // Setting the version may be lost in other functions, reinforce it here + // TODO Fix status field being overwritten every time SetDesiredCompositeStatus() function is called + if comp.Spec.Parameters.Maintenance.PinImageTag != "" { + comp.Status.MariaDBVersion = comp.Spec.Parameters.Maintenance.PinImageTag + } + maintTime := common.SetRandomMaintenanceSchedule(comp) common.SetRandomBackupSchedule(comp, &maintTime) diff --git a/pkg/comp-functions/functions/vshnmariadb/mariadb_deploy.go b/pkg/comp-functions/functions/vshnmariadb/mariadb_deploy.go index 3b33f8d772..0a17f2530d 100644 --- a/pkg/comp-functions/functions/vshnmariadb/mariadb_deploy.go +++ b/pkg/comp-functions/functions/vshnmariadb/mariadb_deploy.go @@ -122,7 +122,7 @@ func createObjectHelmRelease(ctx context.Context, comp *vshnv1.VSHNMariaDB, svc return fmt.Errorf("cannot get observed release values: %w", err) } - versionTag, err := maintenance.SetReleaseVersion(ctx, comp.Spec.Parameters.Service.Version, values, observedValues, []string{"image", "tag"}) + versionTag, err := maintenance.SetReleaseVersion(ctx, comp.Spec.Parameters.Service.Version, values, observedValues, []string{"image", "tag"}, comp.Spec.Parameters.Maintenance.PinImageTag) if err != nil { return fmt.Errorf("cannot set mariadb version for release: %w", err) } diff --git a/pkg/comp-functions/functions/vshnmariadb/proxysql.go b/pkg/comp-functions/functions/vshnmariadb/proxysql.go index ad0fa7012a..80a67da016 100644 --- a/pkg/comp-functions/functions/vshnmariadb/proxysql.go +++ b/pkg/comp-functions/functions/vshnmariadb/proxysql.go @@ -98,6 +98,12 @@ func AddProxySQL(_ context.Context, comp *vshnv1.VSHNMariaDB, svc *runtime.Servi return runtime.NewWarningResult(fmt.Sprintf("cannot create PDB for ProxySQL: %s", err)) } + // Setting the version may be lost in other functions, reinforce it here + // TODO Fix status field being overwritten every time SetDesiredCompositeStatus() function is called + if comp.Spec.Parameters.Maintenance.PinImageTag != "" { + comp.Status.MariaDBVersion = comp.Spec.Parameters.Maintenance.PinImageTag + } + svc.Log.Info("Updating composite status") err = svc.SetDesiredCompositeStatus(comp) if err != nil { diff --git a/pkg/comp-functions/functions/vshnminio/maintenance.go b/pkg/comp-functions/functions/vshnminio/maintenance.go index 7a6b3e650f..13056ca903 100644 --- a/pkg/comp-functions/functions/vshnminio/maintenance.go +++ b/pkg/comp-functions/functions/vshnminio/maintenance.go @@ -13,9 +13,10 @@ import ( // AddMaintenanceJob will add a job to do the maintenance for the instance func AddMaintenanceJob(ctx context.Context, comp *vshnv1.VSHNMinio, svc *runtime.ServiceRuntime) *xfnproto.Result { - if err := svc.GetObservedComposite(comp); err != nil { - err = fmt.Errorf("cannot get observed composite: %w", err) - return runtime.NewFatalResult(err) + if err := svc.GetDesiredComposite(comp); err != nil { + if err := svc.GetObservedComposite(comp); err != nil { + return runtime.NewFatalResult(fmt.Errorf("can't get composite: %w", err)) + } } maintTime := common.SetRandomMaintenanceSchedule(comp) diff --git a/pkg/comp-functions/functions/vshnnextcloud/deploy.go b/pkg/comp-functions/functions/vshnnextcloud/deploy.go index ddf41bcc45..3ff2ca8af7 100644 --- a/pkg/comp-functions/functions/vshnnextcloud/deploy.go +++ b/pkg/comp-functions/functions/vshnnextcloud/deploy.go @@ -675,9 +675,17 @@ func newRelease(ctx context.Context, svc *runtime.ServiceRuntime, comp *vshnv1.V return nil, fmt.Errorf("cannot get observed release values: %w", err) } - _, err = maintenance.SetReleaseVersion(ctx, comp.Spec.Parameters.Service.Version, values, observedValues, []string{"image", "tag"}) + releaseTag, err := maintenance.SetReleaseVersion(ctx, comp.Spec.Parameters.Service.Version, values, observedValues, []string{"image", "tag"}, comp.Spec.Parameters.Maintenance.PinImageTag) if err != nil { - return nil, fmt.Errorf("cannot set keycloak version for release: %w", err) + return nil, fmt.Errorf("cannot set nextcloud version for release: %w", err) + } + + // Update status with current release tag + if releaseTag != "" { + comp.Status.CurrentReleaseTag = releaseTag + if err := svc.SetDesiredCompositeStatus(comp); err != nil { + svc.Log.Error(err, "cannot update CurrentReleaseTag in status") + } } release, err := common.NewRelease(ctx, svc, comp, values, comp.GetName()+"-release") diff --git a/pkg/comp-functions/functions/vshnnextcloud/maintenance.go b/pkg/comp-functions/functions/vshnnextcloud/maintenance.go index 1f193b6fe0..f87ed385ee 100644 --- a/pkg/comp-functions/functions/vshnnextcloud/maintenance.go +++ b/pkg/comp-functions/functions/vshnnextcloud/maintenance.go @@ -13,8 +13,10 @@ import ( // AddMaintenanceJob will add a job to do the maintenance for the instance func AddMaintenanceJob(ctx context.Context, comp *vshnv1.VSHNNextcloud, svc *runtime.ServiceRuntime) *xfnproto.Result { - if err := svc.GetObservedComposite(comp); err != nil { - return runtime.NewFatalResult(fmt.Errorf("can't get composite: %w", err)) + if err := svc.GetDesiredComposite(comp); err != nil { + if err := svc.GetObservedComposite(comp); err != nil { + return runtime.NewFatalResult(fmt.Errorf("can't get composite: %w", err)) + } } maintTime := common.SetRandomMaintenanceSchedule(comp) diff --git a/pkg/comp-functions/functions/vshnpostgrescnpg/deploy.go b/pkg/comp-functions/functions/vshnpostgrescnpg/deploy.go index ff5988958d..f090faad77 100644 --- a/pkg/comp-functions/functions/vshnpostgrescnpg/deploy.go +++ b/pkg/comp-functions/functions/vshnpostgrescnpg/deploy.go @@ -47,12 +47,6 @@ func DeployPostgreSQL(ctx context.Context, comp *vshnv1.VSHNPostgreSQL, svc *run return runtime.NewWarningResult(fmt.Errorf("cannot bootstrap instance namespace: %w", err).Error()) } - l.Info("Set major version in status") - err = setMajorVersionStatus(comp, svc) - if err != nil { - return runtime.NewWarningResult(fmt.Errorf("cannot set major version: %w", err).Error()) - } - l.Info("Create tls certificate") err = createCerts(comp, svc) if err != nil { @@ -62,16 +56,6 @@ func DeployPostgreSQL(ctx context.Context, comp *vshnv1.VSHNPostgreSQL, svc *run return deployPostgresSQLUsingCNPG(ctx, comp, svc) } -// setMajorVersionStatus sets version in status only when it is provisioned -// The subsequent update of this field is to happen in the MajorUpgrade comp-func -func setMajorVersionStatus(comp *vshnv1.VSHNPostgreSQL, svc *runtime.ServiceRuntime) error { - if comp.Status.CurrentVersion == "" { - comp.Status.CurrentVersion = comp.Spec.Parameters.Service.MajorVersion - return svc.SetDesiredCompositeStatus(comp) - } - return nil -} - func createCerts(comp *vshnv1.VSHNPostgreSQL, svc *runtime.ServiceRuntime) error { selfSignedIssuer := &cmv1.Issuer{ ObjectMeta: metav1.ObjectMeta{ @@ -188,6 +172,42 @@ func createCnpgHelmValues(ctx context.Context, svc *runtime.ServiceRuntime, comp hibernation = "on" } + // Default version mappings (major -> minor version) + defaultVersions := map[string]string{ + "17": "17.5", + "16": "16.9", + "15": "15.9", + } + + // Build ImageCatalog images, respecting pinImageTag if set + majorVersion := comp.Spec.Parameters.Service.MajorVersion + pinImageTag := comp.Spec.Parameters.Maintenance.PinImageTag + currentReleaseTag := defaultVersions[majorVersion] + + // If pinImageTag is set, use it for the user's major version + if pinImageTag != "" { + defaultVersions[majorVersion] = pinImageTag + currentReleaseTag = pinImageTag + svc.Log.Info("Using pinned image tag for PostgreSQL", "majorVersion", majorVersion, "pinnedTag", pinImageTag) + } + + // Update status with current version (full minor version like "17.5") + if currentReleaseTag != "" { + comp.Status.CurrentVersion = currentReleaseTag + if err := svc.SetDesiredCompositeStatus(comp); err != nil { + svc.Log.Error(err, "cannot update CurrentVersion in status") + } + } + + // Build the images list for the ImageCatalog + imageCatalogImages := []map[string]string{} + for major, version := range defaultVersions { + imageCatalogImages = append(imageCatalogImages, map[string]string{ + "image": getPsqlImage(version), + "major": major, + }) + } + values := map[string]any{ "fullnameOverride": "postgresql", "cluster": map[string]any{ @@ -226,23 +246,10 @@ func createCnpgHelmValues(ctx context.Context, svc *runtime.ServiceRuntime, comp "imageCatalog": map[string]any{ "create": true, // Image tags: skopeo list-tags docker://ghcr.io/cloudnative-pg/postgresql - "images": []map[string]string{ - { - "image": getPsqlImage("17.5"), - "major": "17", - }, - { - "image": getPsqlImage("16.9"), - "major": "16", - }, - { - "image": getPsqlImage("15.9"), - "major": "15", - }, - }, + "images": imageCatalogImages, }, "version": map[string]string{ - "postgresql": comp.Spec.Parameters.Service.MajorVersion, + "postgresql": majorVersion, }, } diff --git a/pkg/comp-functions/functions/vshnpostgrescnpg/deploy_test.go b/pkg/comp-functions/functions/vshnpostgrescnpg/deploy_test.go index 5af84d8221..e16c5456dc 100644 --- a/pkg/comp-functions/functions/vshnpostgrescnpg/deploy_test.go +++ b/pkg/comp-functions/functions/vshnpostgrescnpg/deploy_test.go @@ -129,6 +129,118 @@ func Test_sizing(t *testing.T) { assert.Equal(t, ourDiskSize, values["cluster"].(map[string]any)["storage"].(map[string]any)["size"]) } +func Test_pinImageTag(t *testing.T) { + ctx := context.TODO() + + t.Run("no pinImageTag uses default version", func(t *testing.T) { + svc, comp := getSvcCompCnpg(t) + comp.Spec.Parameters.Service.MajorVersion = "15" + comp.Spec.Parameters.Maintenance.PinImageTag = "" + + values, err := createCnpgHelmValues(ctx, svc, comp) + assert.NoError(t, err) + assert.NotNil(t, values) + + // Check ImageCatalog has default version for major 15 + imageCatalog := values["imageCatalog"].(map[string]any) + images := imageCatalog["images"].([]map[string]string) + + var found15 bool + for _, img := range images { + if img["major"] == "15" { + // Should use default version "15.9" + assert.Equal(t, "ghcr.io/cloudnative-pg/postgresql:15.9", img["image"]) + found15 = true + } + } + assert.True(t, found15, "should have image entry for major version 15") + + // Check status is set to default version + assert.Equal(t, "15.9", comp.Status.CurrentVersion) + }) + + t.Run("pinImageTag overrides default version in ImageCatalog", func(t *testing.T) { + svc, comp := getSvcCompCnpg(t) + comp.Spec.Parameters.Service.MajorVersion = "15" + comp.Spec.Parameters.Maintenance.PinImageTag = "15.13" + + values, err := createCnpgHelmValues(ctx, svc, comp) + assert.NoError(t, err) + assert.NotNil(t, values) + + // Check ImageCatalog has pinned version for major 15 + imageCatalog := values["imageCatalog"].(map[string]any) + images := imageCatalog["images"].([]map[string]string) + + var found15 bool + for _, img := range images { + if img["major"] == "15" { + // Should use pinned version "15.13" instead of default "15.9" + assert.Equal(t, "ghcr.io/cloudnative-pg/postgresql:15.13", img["image"]) + found15 = true + } + } + assert.True(t, found15, "should have image entry for major version 15") + + // Check status is set to pinned version + assert.Equal(t, "15.13", comp.Status.CurrentVersion) + }) + + t.Run("pinImageTag only affects specified major version", func(t *testing.T) { + svc, comp := getSvcCompCnpg(t) + comp.Spec.Parameters.Service.MajorVersion = "16" + comp.Spec.Parameters.Maintenance.PinImageTag = "16.4" + + values, err := createCnpgHelmValues(ctx, svc, comp) + assert.NoError(t, err) + assert.NotNil(t, values) + + // Check ImageCatalog + imageCatalog := values["imageCatalog"].(map[string]any) + images := imageCatalog["images"].([]map[string]string) + + for _, img := range images { + switch img["major"] { + case "16": + // Major 16 should use pinned version + assert.Equal(t, "ghcr.io/cloudnative-pg/postgresql:16.4", img["image"]) + case "15": + // Major 15 should still use default version + assert.Equal(t, "ghcr.io/cloudnative-pg/postgresql:15.9", img["image"]) + case "17": + // Major 17 should still use default version + assert.Equal(t, "ghcr.io/cloudnative-pg/postgresql:17.5", img["image"]) + } + } + + // Check status is set to pinned version + assert.Equal(t, "16.4", comp.Status.CurrentVersion) + }) + + t.Run("majorVersion is correctly set in helm values", func(t *testing.T) { + svc, comp := getSvcCompCnpg(t) + comp.Spec.Parameters.Service.MajorVersion = "17" + comp.Spec.Parameters.Maintenance.PinImageTag = "17.2" + + values, err := createCnpgHelmValues(ctx, svc, comp) + assert.NoError(t, err) + assert.NotNil(t, values) + + // Check version in helm values still uses major version + version := values["version"].(map[string]string) + assert.Equal(t, "17", version["postgresql"]) + + // Check imageCatalogRef is set correctly + cluster := values["cluster"].(map[string]any) + imageCatalogRef := cluster["imageCatalogRef"].(map[string]string) + assert.Equal(t, "ImageCatalog", imageCatalogRef["kind"]) + assert.Equal(t, "postgresql", imageCatalogRef["name"]) + + // Check status is set to pinned version + assert.Equal(t, "17.2", comp.Status.CurrentVersion) + }) +} + // Obtain svc and comp for CNPG tests func getSvcCompCnpg(testing *testing.T) (*runtime.ServiceRuntime, *vshnv1.VSHNPostgreSQL) { svc, comp := getPostgreSqlComp(testing, testingPath) diff --git a/pkg/comp-functions/functions/vshnpostgrescnpg/maintenance.go b/pkg/comp-functions/functions/vshnpostgrescnpg/maintenance.go index 8291e03d78..a95474eb99 100644 --- a/pkg/comp-functions/functions/vshnpostgrescnpg/maintenance.go +++ b/pkg/comp-functions/functions/vshnpostgrescnpg/maintenance.go @@ -82,15 +82,22 @@ var ( // addSchedules will add a job to do the maintenance for the instance func addSchedules(ctx context.Context, comp *vshnv1.VSHNPostgreSQL, svc *runtime.ServiceRuntime) *xfnproto.Result { - err := svc.GetObservedComposite(comp) - if err != nil { - return runtime.NewFatalResult(fmt.Errorf("can't get composite: %w", err)) + // Try desired first to preserve status set by deploy step (including CurrentVersion), + // fall back to observed if desired is not available. + if err := svc.GetDesiredComposite(comp); err != nil { + if err := svc.GetObservedComposite(comp); err != nil { + return runtime.NewFatalResult(fmt.Errorf("can't get composite: %w", err)) + } } common.SetRandomMaintenanceSchedule(comp) instanceNamespace := comp.GetInstanceNamespace() schedule := comp.GetFullMaintenanceSchedule() + if err := svc.SetDesiredCompositeStatus(comp); err != nil { + svc.Log.Error(err, "cannot set composite status") + } + // Disable vacuum for suspended instances vacuumEnabled := comp.Spec.Parameters.Service.VacuumEnabled && comp.Spec.Parameters.Instances != 0 diff --git a/pkg/comp-functions/functions/vshnredis/maintenance.go b/pkg/comp-functions/functions/vshnredis/maintenance.go index 083d02f590..7b42d6f621 100644 --- a/pkg/comp-functions/functions/vshnredis/maintenance.go +++ b/pkg/comp-functions/functions/vshnredis/maintenance.go @@ -17,6 +17,12 @@ func AddMaintenanceJob(ctx context.Context, comp *vshnv1.VSHNRedis, svc *runtime return runtime.NewFatalResult(fmt.Errorf("can't get composite: %w", err)) } + // Setting the version may be lost in other functions, reinforce it here + // TODO Fix status field being overwritten every time SetDesiredCompositeStatus() function is called + if comp.Spec.Parameters.Maintenance.PinImageTag != "" { + comp.Status.CurrentReleaseTag = comp.Spec.Parameters.Maintenance.PinImageTag + } + maintTime := common.SetRandomMaintenanceSchedule(comp) common.SetRandomBackupSchedule(comp, &maintTime) diff --git a/pkg/comp-functions/functions/vshnredis/redis_deploy.go b/pkg/comp-functions/functions/vshnredis/redis_deploy.go index c7aca0d2a0..4e2ab1bf2a 100644 --- a/pkg/comp-functions/functions/vshnredis/redis_deploy.go +++ b/pkg/comp-functions/functions/vshnredis/redis_deploy.go @@ -96,11 +96,19 @@ func createObjectHelmRelease(ctx context.Context, comp *vshnv1.VSHNRedis, svc *r return fmt.Errorf("cannot get observed release values: %w", err) } - _, err = maintenance.SetReleaseVersion(ctx, comp.Spec.Parameters.Service.Version, values, observedValues, []string{"image", "tag"}) + releaseTag, err := maintenance.SetReleaseVersion(ctx, comp.Spec.Parameters.Service.Version, values, observedValues, []string{"image", "tag"}, comp.Spec.Parameters.Maintenance.PinImageTag) if err != nil { return fmt.Errorf("cannot set redis version for release: %w", err) } + // Update status with current release tag + if releaseTag != "" { + comp.Status.CurrentReleaseTag = releaseTag + if err := svc.SetDesiredCompositeStatus(comp); err != nil { + svc.Log.Error(err, "cannot update CurrentReleaseTag in status") + } + } + rel, err := newRelease(ctx, svc, values, comp) if err != nil { return err diff --git a/pkg/controller/webhooks/default_handler.go b/pkg/controller/webhooks/default_handler.go index 0b9f975761..c93d13775a 100644 --- a/pkg/controller/webhooks/default_handler.go +++ b/pkg/controller/webhooks/default_handler.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/go-logr/logr" + vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1" "github.com/vshn/appcat/v4/pkg/common/quotas" "github.com/vshn/appcat/v4/pkg/common/utils" "github.com/vshn/appcat/v4/pkg/comp-functions/functions/common" @@ -144,7 +145,8 @@ func (r *DefaultWebhookHandler) ValidateCreate(ctx context.Context, obj runtime. allErrs.Add(err) } - return nil, allErrs.Get() + warn := checkManualVersionManagementWarnings(comp.GetFullMaintenanceSchedule()) + return warn, allErrs.Get() } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type @@ -188,7 +190,8 @@ func (r *DefaultWebhookHandler) ValidateUpdate(ctx context.Context, oldObj, newO } } - return nil, allErrs.Get() + warn := checkManualVersionManagementWarnings(comp.GetFullMaintenanceSchedule()) + return warn, allErrs.Get() } // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type @@ -613,3 +616,21 @@ func (r *DefaultWebhookHandler) getEffectiveDiskSize(ctx context.Context, comp c // No disk size configured return "", nil } + +// checkManualVersionManagementWarnings returns warnings if manual version management flags are enabled +func checkManualVersionManagementWarnings(maintenance vshnv1.VSHNDBaaSMaintenanceScheduleSpec) admission.Warnings { + var warnings admission.Warnings + if maintenance.PinImageTag != "" { + warnings = append(warnings, + fmt.Sprintf("WARNING: Image tag pinned to %q at %s. You are responsible for version management. Downgrades are allowed at your own risk.", + maintenance.PinImageTag, + field.NewPath("spec", "parameters", "maintenance", "pinImageTag").String())) + } + if maintenance.DisableAppcatRelease { + warnings = append(warnings, + "WARNING: AppCat release updates disabled at "+ + field.NewPath("spec", "parameters", "maintenance", "disableAppcatRelease").String()+ + ". This is strongly discouraged and may leave your instance without security patches.") + } + return warnings +} diff --git a/pkg/controller/webhooks/default_handler_test.go b/pkg/controller/webhooks/default_handler_test.go index 1c2898b389..9d3762f67a 100644 --- a/pkg/controller/webhooks/default_handler_test.go +++ b/pkg/controller/webhooks/default_handler_test.go @@ -370,3 +370,55 @@ func createTestProviderConfigNoCredentials(group, version, name string) *unstruc return obj } + +func TestCheckManualVersionManagementWarnings(t *testing.T) { + tests := []struct { + name string + maintenance vshnv1.VSHNDBaaSMaintenanceScheduleSpec + wantWarnings int + wantContains []string + }{ + { + name: "GivenDefaultValues_ThenNoWarnings", + maintenance: vshnv1.VSHNDBaaSMaintenanceScheduleSpec{}, + wantWarnings: 0, + }, + { + name: "GivenPinImageTagOnly_ThenOneWarning", + maintenance: vshnv1.VSHNDBaaSMaintenanceScheduleSpec{ + PinImageTag: "7.2.5", + }, + wantWarnings: 1, + wantContains: []string{"Image tag pinned to"}, + }, + { + name: "GivenDisableAppcatReleaseOnly_ThenOneWarning", + maintenance: vshnv1.VSHNDBaaSMaintenanceScheduleSpec{ + DisableAppcatRelease: true, + }, + wantWarnings: 1, + wantContains: []string{"AppCat release updates disabled"}, + }, + { + name: "GivenBothPinnedAndDisabled_ThenTwoWarnings", + maintenance: vshnv1.VSHNDBaaSMaintenanceScheduleSpec{ + PinImageTag: "7.2.5", + DisableAppcatRelease: true, + }, + wantWarnings: 2, + wantContains: []string{"Image tag pinned to", "AppCat release updates disabled"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + warnings := checkManualVersionManagementWarnings(tt.maintenance) + + assert.Len(t, warnings, tt.wantWarnings) + + for i, expectedSubstr := range tt.wantContains { + assert.Contains(t, string(warnings[i]), expectedSubstr) + } + }) + } +} diff --git a/pkg/controller/webhooks/postgresql.go b/pkg/controller/webhooks/postgresql.go index 8ed09aa945..807e78bf75 100644 --- a/pkg/controller/webhooks/postgresql.go +++ b/pkg/controller/webhooks/postgresql.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "strconv" + "strings" "github.com/vshn/appcat/v4/pkg/common/quotas" "github.com/vshn/appcat/v4/pkg/common/utils" @@ -131,6 +132,14 @@ func (p *PostgreSQLWebhookHandler) validatePostgreSQL(ctx context.Context, newOb // Validate PostgreSQL configuration allErrs.Add(validatePgConf(newPg)...) + // Validate pinImageTag matches majorVersion + if err := validatePinImageTag( + newPg.Spec.Parameters.Maintenance.PinImageTag, + newPg.Spec.Parameters.Service.MajorVersion, + ); err != nil { + allErrs.Add(err) + } + if !isCreate { oldPg, ok := oldObj.(*vshnv1.VSHNPostgreSQL) if !ok { @@ -318,7 +327,13 @@ func validateMajorVersionUpgrade(newPg *vshnv1.VSHNPostgreSQL, oldPg *vshnv1.VSH if oldPg.Status.CurrentVersion == "" { oldVersion = newVersion } else { - oldVersion, err = strconv.Atoi(oldPg.Status.CurrentVersion) + // CurrentVersion can be either major version ("15") or full version ("15.9") + // Extract just the major version part + currentVersion := oldPg.Status.CurrentVersion + if idx := strings.Index(currentVersion, "."); idx > 0 { + currentVersion = currentVersion[:idx] + } + oldVersion, err = strconv.Atoi(currentVersion) if err != nil { errList = append(errList, field.Invalid( field.NewPath("status.currentVersion"), @@ -373,3 +388,26 @@ func validatePostgreSQLEncryptionChanges(newEncryption, oldEncryption *vshnv1.VS } return nil } + +// validatePinImageTag validates that pinImageTag's major version matches the specified majorVersion +func validatePinImageTag(pinImageTag, majorVersion string) *field.Error { + if pinImageTag == "" { + return nil + } + + // Extract major version from pinImageTag (e.g., "15.13" -> "15", "16.4" -> "16") + pinMajor := pinImageTag + if idx := strings.Index(pinImageTag, "."); idx > 0 { + pinMajor = pinImageTag[:idx] + } + + if pinMajor != majorVersion { + return field.Invalid( + field.NewPath("spec", "parameters", "maintenance", "pinImageTag"), + pinImageTag, + fmt.Sprintf("pinImageTag major version %q must match majorVersion %q", pinMajor, majorVersion), + ) + } + + return nil +} diff --git a/pkg/controller/webhooks/postgresql_test.go b/pkg/controller/webhooks/postgresql_test.go index e8f7976b33..d12bea1a0b 100644 --- a/pkg/controller/webhooks/postgresql_test.go +++ b/pkg/controller/webhooks/postgresql_test.go @@ -591,6 +591,147 @@ func TestPostgreSQLWebhookHandler_ValidateEncryptionChanges(t *testing.T) { assert.NoError(t, err) } +func TestValidatePinImageTag(t *testing.T) { + tests := []struct { + name string + pinImageTag string + majorVersion string + expectErr bool + errContains string + }{ + { + name: "GivenEmptyPinImageTag_ThenNoError", + pinImageTag: "", + majorVersion: "15", + expectErr: false, + }, + { + name: "GivenMatchingMajorVersion_ThenNoError", + pinImageTag: "15.13", + majorVersion: "15", + expectErr: false, + }, + { + name: "GivenMatchingMajorVersionDifferentMinor_ThenNoError", + pinImageTag: "15.9", + majorVersion: "15", + expectErr: false, + }, + { + name: "GivenMajorVersion16Match_ThenNoError", + pinImageTag: "16.4", + majorVersion: "16", + expectErr: false, + }, + { + name: "GivenMajorVersion17Match_ThenNoError", + pinImageTag: "17.2", + majorVersion: "17", + expectErr: false, + }, + { + name: "GivenMismatchedMajorVersion_ThenError", + pinImageTag: "16.4", + majorVersion: "15", + expectErr: true, + errContains: "pinImageTag major version \"16\" must match majorVersion \"15\"", + }, + { + name: "GivenPinImageTagWithDifferentMajor_ThenError", + pinImageTag: "15.13", + majorVersion: "16", + expectErr: true, + errContains: "pinImageTag major version \"15\" must match majorVersion \"16\"", + }, + { + name: "GivenPinImageTagWithMajorOnly_ThenNoError", + pinImageTag: "15", + majorVersion: "15", + expectErr: false, + }, + { + name: "GivenPinImageTagWithMajorOnlyMismatch_ThenError", + pinImageTag: "16", + majorVersion: "15", + expectErr: true, + errContains: "pinImageTag major version \"16\" must match majorVersion \"15\"", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validatePinImageTag(tt.pinImageTag, tt.majorVersion) + if tt.expectErr { + assert.NotNil(t, err) + assert.Contains(t, err.Error(), tt.errContains) + } else { + assert.Nil(t, err) + } + }) + } +} + +func TestPostgreSQLWebhookHandler_ValidateCreateWithPinImageTag(t *testing.T) { + ctx := context.TODO() + claimNS := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "claimns", + Labels: map[string]string{ + utils.OrgLabelName: "myorg", + }, + }, + } + fclient := fake.NewClientBuilder(). + WithScheme(pkg.SetupScheme()). + WithObjects(claimNS). + Build() + + handler := PostgreSQLWebhookHandler{ + DefaultWebhookHandler: DefaultWebhookHandler{ + client: fclient, + log: logr.Discard(), + withQuota: false, + obj: &vshnv1.VSHNPostgreSQL{}, + name: "postgresql", + nameLength: 30, + }, + } + + pgBase := &vshnv1.VSHNPostgreSQL{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myinstance", + Namespace: "claimns", + }, + Spec: vshnv1.VSHNPostgreSQLSpec{ + Parameters: vshnv1.VSHNPostgreSQLParameters{ + Service: vshnv1.VSHNPostgreSQLServiceSpec{ + MajorVersion: "15", + RepackEnabled: true, + }, + }, + }, + } + + // Test: Valid pinImageTag matching majorVersion + pgValid := pgBase.DeepCopy() + pgValid.Spec.Parameters.Maintenance.PinImageTag = "15.13" + _, err := handler.ValidateCreate(ctx, pgValid) + assert.NoError(t, err) + + // Test: Invalid pinImageTag not matching majorVersion + pgInvalid := pgBase.DeepCopy() + pgInvalid.Spec.Parameters.Maintenance.PinImageTag = "16.4" + _, err = handler.ValidateCreate(ctx, pgInvalid) + assert.Error(t, err) + assert.Contains(t, err.Error(), "pinImageTag major version") + + // Test: No pinImageTag set (should be valid) + pgNoPin := pgBase.DeepCopy() + pgNoPin.Spec.Parameters.Maintenance.PinImageTag = "" + _, err = handler.ValidateCreate(ctx, pgNoPin) + assert.NoError(t, err) +} + // disabling this temporarily // func TestPostgreSQLWebhookHandler_ValidateMajorVersionUpgrade(t *testing.T) { // tests := []struct { diff --git a/pkg/controller/webhooks/redis.go b/pkg/controller/webhooks/redis.go index 3b4666f3a4..9adedaecce 100644 --- a/pkg/controller/webhooks/redis.go +++ b/pkg/controller/webhooks/redis.go @@ -5,7 +5,6 @@ import ( "fmt" vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1" - "github.com/vshn/appcat/v4/pkg/comp-functions/functions/common" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ctrl "sigs.k8s.io/controller-runtime" @@ -51,12 +50,12 @@ func SetupRedisWebhookHandlerWithManager(mgr ctrl.Manager, withQuota bool) error // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type func (r *RedisWebhookHandler) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - comp, ok := obj.(common.Composite) + redis, ok := obj.(*vshnv1.VSHNRedis) if !ok { return nil, fmt.Errorf("provided manifest is not a valid " + r.gk.Kind + " object") } - allErrs := newFielErrors(comp.GetName(), redisGK) + allErrs := newFielErrors(redis.GetName(), redisGK) // First call the parent validation (includes provider config validation) parentWarnings, parentErr := r.DefaultWebhookHandler.ValidateCreate(ctx, obj) @@ -73,16 +72,16 @@ func (r *RedisWebhookHandler) ValidateCreate(ctx context.Context, obj runtime.Ob // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type func (r *RedisWebhookHandler) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - comp, ok := newObj.(common.Composite) + redis, ok := newObj.(*vshnv1.VSHNRedis) if !ok { return nil, fmt.Errorf("provided manifest is not a valid " + r.gk.Kind + " object") } - if comp.GetDeletionTimestamp() != nil { + if redis.GetDeletionTimestamp() != nil { return nil, nil } - allErrs := newFielErrors(comp.GetName(), redisGK) + allErrs := newFielErrors(redis.GetName(), redisGK) // First call the parent validation (includes provider config validation) parentWarnings, parentErr := r.DefaultWebhookHandler.ValidateUpdate(ctx, oldObj, newObj)