Repository: cloudnative-pg/cloudnative-pg

Issue:
[Feature]: warn users if running a deprecated/unsupported Debian version
### Is there an existing issue already for this feature request/idea?

- [x] I have searched for an existing issue, and could not find anything. I believe this is a new feature request to be evaluated.

### What problem is this feature going to solve? Why should it be added?

The [`postgres-containers` project](https://github.com/cloudnative-pg/postgres-containers) now includes a table that clearly outlines the support status of various Debian versions:

| Name                      | Version | Release Date |     EOL    |     LTS    |   Status   |
| ------------------------- | :-----: | :----------: | :--------: | :--------: | :---------: |
| Trixie (`stable`)         |    13   |  2025-08-09  | 2028-08-09 | 2030-06-30 | Supported  |
| Bookworm (`oldstable`)    |    12   |  2023-06-10  | 2026-06-10 | 2028-06-30 | Supported  |
| Bullseye (`oldoldstable`) |    11   |  2021-08-14  | 2024-08-14 | 2026-08-31 | Deprecated |

We should implement a warning that activates when the cluster starts up, indicating if a version is beyond its End of Life (EOL) status (deprecated) or beyond its Long-Term Support (LTS) period (not supported).

### Describe the solution you'd like

Verify the content of the `/etc/os-release` file which, in the case of `bullseye` reports this:

```
$ kubectl exec -ti cluster-example-1 -c postgres -- cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
NAME="Debian GNU/Linux"
VERSION_ID="11"
VERSION="11 (bullseye)"
VERSION_CODENAME=bullseye
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
```

CloudNativePG could maintain an internal table where the `VERSION` is the key, storing the two dates (`supported_until` and `deprecated_from`). Based on the current date at the time of startup of the PostgreSQL instance, the instance manager can log a warning as explained above.

### Describe alternatives you've considered

N/A

### Additional context

_No response_

### Backport?

Yes

### Are you willing to actively contribute to this feature?

Yes

### Code of Conduct

- [x] I agree to follow this project's Code of Conduct

Discussion:

Solution patch:
diff --git a/go.mod b/go.mod
index 316967e5..f04df2ce 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ toolchain go1.25.1
 require (
 	github.com/DATA-DOG/go-sqlmock v1.5.2
 	github.com/Masterminds/semver/v3 v3.4.0
+	github.com/acobaugh/osrelease v0.1.0
 	github.com/avast/retry-go/v4 v4.6.1
 	github.com/blang/semver v3.5.1+incompatible
 	github.com/cheynewallace/tabby v1.1.1
diff --git a/go.sum b/go.sum
index dd0dd802..6a9dfcc9 100644
--- a/go.sum
+++ b/go.sum
@@ -4,6 +4,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl
 github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
 github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
 github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE=
+github.com/acobaugh/osrelease v0.1.0/go.mod h1:4bFEs0MtgHNHBrmHCt67gNisnabCRAlzdVasCEGHTWY=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
 github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk=
diff --git a/internal/cmd/manager/instance/run/cmd.go b/internal/cmd/manager/instance/run/cmd.go
index e3890579..62dbf065 100644
--- a/internal/cmd/manager/instance/run/cmd.go
+++ b/internal/cmd/manager/instance/run/cmd.go
@@ -17,7 +17,6 @@ limitations under the License.
 SPDX-License-Identifier: Apache-2.0
 */
 
-// Package run implements the "instance run" subcommand of the operator
 package run
 
 import (
@@ -104,6 +103,9 @@ func NewCmd() *cobra.Command {
 				cmd.Context(),
 				log.GetLogger().WithValues("logger", "instance-manager"),
 			)
+
+			checkCurrentOSRelease(ctx)
+
 			instance := postgres.NewInstance().
 				WithPodName(podName).
 				WithClusterName(clusterName).
diff --git a/internal/cmd/manager/instance/run/doc.go b/internal/cmd/manager/instance/run/doc.go
new file mode 100644
index 00000000..165db5d2
--- /dev/null
+++ b/internal/cmd/manager/instance/run/doc.go
@@ -0,0 +1,21 @@
+/*
+Copyright © contributors to CloudNativePG, established as
+CloudNativePG a Series of LF Projects, LLC.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+SPDX-License-Identifier: Apache-2.0
+*/
+
+// Package run implements the "instance run" subcommand of the operator
+package run
diff --git a/internal/cmd/manager/instance/run/osdb.go b/internal/cmd/manager/instance/run/osdb.go
new file mode 100644
index 00000000..1361fc81
--- /dev/null
+++ b/internal/cmd/manager/instance/run/osdb.go
@@ -0,0 +1,100 @@
+/*
+Copyright © contributors to CloudNativePG, established as
+CloudNativePG a Series of LF Projects, LLC.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package run
+
+import "time"
+
+// OSEntry represents an OS version.
+//
+// Between the release date and DeprecatedFrom, the OS Version
+// is **supported** and **not deprecated**.
+//
+// Between DeprecatedFrom and SupportedUntil, the OS Version
+// is **supported** and **deprecated**.
+//
+// After SupportedUntil, the OS Version
+// is **not supported** and **deprecated**.
+type OSEntry struct {
+	// Version identifies the operating system version. This is the same
+	// as the "VERSION" field in the "/etc/osrelease" file.
+	Version string `json:"version"`
+
+	// DeprecatedFrom is the end-of-life date of this OS version
+	DeprecatedFrom time.Time `json:"deprecatedFrom"`
+
+	// SupportedUntil is the end of the OS version long-term-support period,
+	// as defined.
+	SupportedUntil time.Time `json:"supportedUntil"`
+}
+
+// IsSupported checks if the release is supported.
+func (e *OSEntry) IsSupported(now time.Time) bool {
+	return now.Before(e.SupportedUntil)
+}
+
+// IsDeprecated checks if the release is deprecated.
+func (e *OSEntry) IsDeprecated(now time.Time) bool {
+	return now.After(e.DeprecatedFrom)
+}
+
+// OSDB is a set of known OS releases
+type OSDB struct {
+	data map[string]OSEntry
+}
+
+var defaultOSDB OSDB
+
+// Register adds a new know OS entry
+func (db *OSDB) Register(entry OSEntry) {
+	if db.data == nil {
+		db.data = make(map[string]OSEntry)
+	}
+	db.data[entry.Version] = entry
+}
+
+// Get gets the OS entry for a known version
+func (db *OSDB) Get(version string) (OSEntry, bool) {
+	entry, ok := db.data[version]
+	return entry, ok
+}
+
+func init() {
+	// Known Debian releases
+	defaultOSDB.Register(OSEntry{
+		Version:        "10 (buster)",
+		DeprecatedFrom: time.Date(2022, time.September, 10, 0, 0, 0, 0, time.UTC),
+		SupportedUntil: time.Date(2024, time.June, 30, 0, 0, 0, 0, time.UTC),
+	})
+	defaultOSDB.Register(OSEntry{
+		Version:        "11 (bullseye)",
+		DeprecatedFrom: time.Date(2024, time.August, 14, 0, 0, 0, 0, time.UTC),
+		SupportedUntil: time.Date(2026, time.August, 31, 0, 0, 0, 0, time.UTC),
+	})
+	defaultOSDB.Register(OSEntry{
+		Version:        "12 (bookworm)",
+		DeprecatedFrom: time.Date(2026, time.June, 10, 0, 0, 0, 0, time.UTC),
+		SupportedUntil: time.Date(2028, time.June, 30, 0, 0, 0, 0, time.UTC),
+	})
+	defaultOSDB.Register(OSEntry{
+		Version:        "13 (trixie)",
+		DeprecatedFrom: time.Date(2028, time.August, 9, 0, 0, 0, 0, time.UTC),
+		SupportedUntil: time.Date(2030, time.June, 30, 0, 0, 0, 0, time.UTC),
+	})
+}
diff --git a/internal/cmd/manager/instance/run/osrelease.go b/internal/cmd/manager/instance/run/osrelease.go
new file mode 100644
index 00000000..bf9a2083
--- /dev/null
+++ b/internal/cmd/manager/instance/run/osrelease.go
@@ -0,0 +1,84 @@
+/*
+Copyright © contributors to CloudNativePG, established as
+CloudNativePG a Series of LF Projects, LLC.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package run
+
+import (
+	"context"
+	"errors"
+	"io/fs"
+	"time"
+
+	"github.com/acobaugh/osrelease"
+	"github.com/cloudnative-pg/machinery/pkg/log"
+)
+
+// checkCurrentOSRelease inspects the host's OS release information
+// and logs warnings if the distribution is unknown, unsupported, or deprecated.
+// If the OS release file is missing, the check is skipped silently.
+func checkCurrentOSRelease(ctx context.Context) {
+	contextLogger := log.FromContext(ctx)
+
+	data, err := osrelease.Read()
+	if errors.Is(err, fs.ErrNotExist) {
+		// Some Linux distributions do not include an os-release file.
+		// Skipping the OS distribution check in such cases.
+		return
+	}
+	if err != nil {
+		contextLogger.Warning(
+			"Failed to read or parse the os-release file; skipping distribution check",
+			"err", err)
+		return
+	}
+
+	version := data["VERSION"]
+	if version == "" {
+		contextLogger.Warning(
+			"os-release file is missing the VERSION field; skipping distribution check",
+			"data", data)
+		return
+	}
+
+	entry, ok := defaultOSDB.Get(version)
+	if !ok {
+		contextLogger.Warning(
+			"Encountered unknown OS distribution version; skipping check",
+			"version", version)
+		return
+	}
+
+	now := time.Now()
+	switch {
+	case !entry.IsSupported(now):
+		contextLogger.Warning(
+			"OS distribution is not supported",
+			"entry", entry)
+
+	case entry.IsDeprecated(now):
+		contextLogger.Warning(
+			"OS distribution is deprecated; consider upgrading",
+			"entry", entry)
+
+	default:
+		contextLogger.Info(
+			"OS distribution is supported",
+			"entry", entry)
+	}
+}



Test patch:
diff --git a/internal/cmd/manager/instance/run/osdb_test.go b/internal/cmd/manager/instance/run/osdb_test.go
new file mode 100644
index 00000000..62eebfd4
--- /dev/null
+++ b/internal/cmd/manager/instance/run/osdb_test.go
@@ -0,0 +1,55 @@
+/*
+Copyright © contributors to CloudNativePG, established as
+CloudNativePG a Series of LF Projects, LLC.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package run
+
+import (
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("OS database", func() {
+	today := time.Date(2025, time.September, 18, 0, 0, 0, 0, time.UTC)
+
+	DescribeTable(
+		"OS checks",
+		func(distro string, deprecated, supported bool) {
+			entry, ok := defaultOSDB.Get(distro)
+			Expect(ok).To(
+				BeTrue(),
+				"entry for distro %q not found",
+				distro)
+
+			actualSupported := entry.IsSupported(today)
+			actualDeprecated := entry.IsDeprecated(today)
+			Expect(actualDeprecated).To(
+				Equal(deprecated),
+				"expected %s deprecation status to be %t instead of %t", distro, deprecated, actualDeprecated)
+			Expect(actualSupported).To(
+				Equal(supported),
+				"expected %s support status to be %t instead of %t", distro, supported, actualSupported)
+		},
+		Entry("trixie", "13 (trixie)", false, true),
+		Entry("bookworm", "12 (bookworm)", false, true),
+		Entry("bullseye", "11 (bullseye)", true, true),
+		Entry("buster", "10 (buster)", true, false),
+	)
+})