latest_version

This commit is contained in:
Oliver Wallisch 2026-03-13 14:51:30 +01:00
commit 552ea62601
8 changed files with 924 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

11
assets/logo.txt Normal file
View file

@ -0,0 +1,11 @@
@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@
@@@@@@...........:@@
@@@@@@. .........%@@
@@@@@+ .@@@@@@@@@@@
@@@@@. =@@+ .@@@@@
@@@@@@@@@@@. =@@@@@
@@@ .@@@@@@
@@-...........@@@@@@
@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@

62
debug.log Normal file
View file

@ -0,0 +1,62 @@
2026/03/12 10:52:15 App gestartet...
2026/03/12 10:55:25 App gestartet...
2026/03/12 10:55:45 Gefundene Pods: 0
2026/03/12 11:00:10 App gestartet...
2026/03/12 11:00:28 Gefundene Pods: 1
2026/03/12 11:10:12 Gefundene Pods: 1
2026/03/12 12:51:04 App gestartet...
2026/03/12 12:51:27 Gefundene Pods: 1
2026/03/12 13:42:25 App gestartet...
2026/03/12 13:42:52 Gefundene Pods: 1
2026/03/12 14:46:44 App gestartet...
2026/03/12 16:04:08 App gestartet...
2026/03/12 16:17:13 App gestartet...
2026/03/12 16:21:12 App gestartet...
2026/03/12 16:22:40 App gestartet...
2026/03/12 16:25:47 App gestartet...
2026/03/12 16:28:40 App gestartet...
2026/03/12 16:31:06 App gestartet...
2026/03/12 16:35:12 App gestartet...
2026/03/12 16:37:26 App gestartet...
debug 2026/03/12 16:49:44 --- App gestartet ---
debug 2026/03/12 16:52:10 --- App gestartet ---
debug 2026/03/13 11:27:29 --- App gestartet ---
debug 2026/03/13 11:47:40 --- App gestartet ---
debug 2026/03/13 11:54:40 --- App gestartet ---
debug 2026/03/13 11:54:51 ✅ PVC gefunden: pgdata-postgres-c8aaab83-67b4-4a00-b634-a675dd50abae-0 (Bound) - Größe: 32Gi
debug 2026/03/13 11:58:22 --- App gestartet ---
debug 2026/03/13 11:58:25 ✅ PVC gefunden: pgdata-postgres-c71b2665-1341-405c-9903-e7dbe93589f3-0 (Bound) - Größe: 10Gi
debug 2026/03/13 12:08:10 --- App gestartet ---
debug 2026/03/13 12:08:20 ✅ PVC gefunden: pgdata-postgres-c71b2665-1341-405c-9903-e7dbe93589f3-0 (Bound) - Größe: 10Gi
debug 2026/03/13 12:10:02 --- App gestartet ---
debug 2026/03/13 12:10:14 ✅ PVC gefunden: pgdata-postgres-c71b2665-1341-405c-9903-e7dbe93589f3-0 (Bound) - Größe: 10Gi
debug 2026/03/13 12:20:15 --- App gestartet ---
debug 2026/03/13 12:20:23 ✅ PVC geladen: pgdata-postgres-c71b2665-1341-405c-9903-e7dbe93589f3-0 (10Gi)
debug 2026/03/13 12:23:02 --- App gestartet ---
debug 2026/03/13 12:23:05 ✅ PVC geladen: pgdata-postgres-c71b2665-1341-405c-9903-e7dbe93589f3-0 (10Gi)
debug 2026/03/13 13:11:32 --- App gestartet ---
debug 2026/03/13 13:11:39 ✅ PVC geladen: pgdata-postgres-c71b2665-1341-405c-9903-e7dbe93589f3-0 (10Gi)
debug 2026/03/13 13:24:27 --- App gestartet ---
debug 2026/03/13 13:24:32 ✅ PVC geladen: pgdata-postgres-c71b2665-1341-405c-9903-e7dbe93589f3-0 (10Gi)
debug 2026/03/13 13:31:12 --- App gestartet ---
debug 2026/03/13 13:31:22 ✅ PVC geladen: pgdata-postgres-c71b2665-1341-405c-9903-e7dbe93589f3-0 (10Gi)
debug 2026/03/13 13:31:38 Fehler beim Laden der Config: invalid configuration: no configuration has been provided, try setting KUBERNETES_MASTER environment variable
debug 2026/03/13 13:35:41 --- App gestartet ---
debug 2026/03/13 13:36:00 ✅ PVC geladen: pgdata-postgres-c71b2665-1341-405c-9903-e7dbe93589f3-0 (10Gi)
debug 2026/03/13 13:36:06 🔍 Versuche Kubeconfig zu laden: /Users/wallischo/.kube/ske-config/stackit/eu01/postgresql/prod/stackit-eu01-pgprod01.yml
debug 2026/03/13 13:36:06 ❌ Fehler beim Laden der Config: invalid configuration: no configuration has been provided, try setting KUBERNETES_MASTER environment variable
debug 2026/03/13 13:50:28 🔍 Versuche Kubeconfig zu laden: /Users/wallischo/.kube/ske-config/stackit/eu01/postgresql/prod/stackit-eu01-pgprod01.yml
debug 2026/03/13 13:50:28 ❌ Fehler beim Laden der Config: invalid configuration: no configuration has been provided, try setting KUBERNETES_MASTER environment variable
debug 2026/03/13 13:54:26 --- App gestartet ---
debug 2026/03/13 13:54:33 ✅ PVC geladen: pgdata-postgres-c71b2665-1341-405c-9903-e7dbe93589f3-0 (10Gi)
debug 2026/03/13 13:55:11 ✅ PVC geladen: pgdata-postgres-c71b2665-1341-405c-9903-e7dbe93589f3-0 (10Gi)
debug 2026/03/13 13:55:50 --- App gestartet ---
debug 2026/03/13 13:55:54 ✅ PVC geladen: pgdata-postgres-c71b2665-1341-405c-9903-e7dbe93589f3-0 (10Gi)
debug 2026/03/13 13:56:31 --- App gestartet ---
debug 2026/03/13 13:56:34 ✅ PVC geladen: pgdata-postgres-c71b2665-1341-405c-9903-e7dbe93589f3-0 (10Gi)
debug 2026/03/13 13:56:41 🔍 Versuche Kubeconfig zu laden: /Users/wallischo/.kube/ske-config/stackit/eu01/postgresql/prod/stackit-eu01-pgprod01.yml
debug 2026/03/13 13:56:41 ❌ Fehler beim Laden der Config: invalid configuration: no configuration has been provided, try setting KUBERNETES_MASTER environment variable
debug 2026/03/13 14:02:34 --- App gestartet ---
debug 2026/03/13 14:02:46 ✅ PVC geladen: pgdata-postgres-c71b2665-1341-405c-9903-e7dbe93589f3-0 (10Gi)
debug 2026/03/13 14:03:24 🔍 Versuche Kubeconfig zu laden: /Users/wallischo/.kube/ske-config/stackit/eu01/postgresql/prod/stackit-eu01-pgprod01.yml
debug 2026/03/13 14:03:24 ❌ Fehler beim Laden der Config: invalid configuration: no configuration has been provided, try setting KUBERNETES_MASTER environment variable

70
go.mod Normal file
View file

@ -0,0 +1,70 @@
module main
go 1.25.4
require (
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
k8s.io/client-go v0.35.2
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.9.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.35.2 // indirect
k8s.io/apimachinery v0.35.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)

181
go.sum Normal file
View file

@ -0,0 +1,181 @@
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/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=
k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=
k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=
k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=
k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

31
main.go Normal file
View file

@ -0,0 +1,31 @@
package main
import (
"fmt"
"log"
"os"
"main/pkg/tui"
tea "github.com/charmbracelet/bubbletea"
)
func main() {
// DIE BUBBLE TEA MAGIE:
// Erstellt/Öffnet die Datei und leitet das Standard-"log" Paket um.
// Das "debug" Präfix wird vor jede Zeile geschrieben.
f, err := tea.LogToFile("debug.log", "debug")
if err != nil {
fmt.Println("Konnte Log-Datei nicht erstellen:", err)
os.Exit(1)
}
defer f.Close()
log.Println("--- App gestartet ---")
p := tea.NewProgram(tui.InitialModel(), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Printf("Error: %v", err)
os.Exit(1)
}
}

203
pkg/scanner/scanner.go Normal file
View file

@ -0,0 +1,203 @@
package scanner
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
type ScanFinishedMsg []ClusterResult
type PodDetails struct {
Name string
Role string
Ready string
Restarts int32
Age string
}
type ClusterResult struct {
FileName string
InstanceName string
Found bool
Status string
Size string
Flavor string
Age string
ProjectID string
PostgresVersion string
// Neue Felder für das Dashboard
PVCName string
StorageSize string
StorageStatus string
Error error
Pods []PodDetails
}
func FillPVCData(clientset *kubernetes.Clientset, namespace string, r *ClusterResult) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
pvcs, err := clientset.CoreV1().PersistentVolumeClaims(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
log.Printf("❌ Fehler beim Laden des PVCs in %s: %v", namespace, err)
r.StorageSize = "Error"
r.StorageStatus = "N/A"
return
}
if len(pvcs.Items) == 0 {
log.Printf("⚠️ Keine PVCs im Namespace %s gefunden", namespace)
r.PVCName = "N/A"
r.StorageSize = "N/A"
r.StorageStatus = "Kein PVC"
return
}
pvc := pvcs.Items[0]
r.PVCName = pvc.Name
r.StorageSize = pvc.Spec.Resources.Requests.Storage().String()
r.StorageStatus = string(pvc.Status.Phase)
log.Printf("✅ PVC geladen: %s (%s)", r.PVCName, r.StorageSize)
}
func GetPostgresversion(clientset *kubernetes.Clientset, namespace string) string {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
pods, err := clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
log.Printf("Fehler beim Abrufen der Postgres-Version: %v", err)
return "N/A"
}
if len(pods.Items) > 0 {
pod := pods.Items[0]
for _, container := range pod.Spec.Containers {
for _, env := range container.Env {
if env.Name == "PGVERSION" {
return env.Value
}
}
}
}
return "N/A"
}
func CheckCluster(kubeconfigPath string, targetNS string, timeoutSec int) ClusterResult {
res := ClusterResult{
FileName: filepath.Base(kubeconfigPath),
Status: "Not Found",
InstanceName: "-",
PVCName: "-",
StorageSize: "-",
StorageStatus: "-",
}
config, err := clientcmd.LoadFromFile(kubeconfigPath)
if err != nil {
return res
}
if config.CurrentContext == "" && len(config.Contexts) > 0 {
for name := range config.Contexts {
config.CurrentContext = name
break
}
}
clientConfig, err := clientcmd.NewDefaultClientConfig(*config, &clientcmd.ConfigOverrides{}).ClientConfig()
if err != nil {
return res
}
clientset, err := kubernetes.NewForConfig(clientConfig)
if err != nil {
return res
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)
defer cancel()
ns, err := clientset.CoreV1().Namespaces().Get(ctx, targetNS, metav1.GetOptions{})
if err != nil {
return res
}
// Daten sammeln
res.Found = true
res.Status = "Active"
res.PostgresVersion = GetPostgresversion(clientset, targetNS)
res.ProjectID = ns.Labels["stackit.project.id"]
res.Age = fmt.Sprintf("%.0fd", time.Since(ns.CreationTimestamp.Time).Hours()/24)
// PVC Daten über die neue Funktion holen
FillPVCData(clientset, targetNS, &res)
res.Size = res.StorageSize // Für die Tabellenansicht
nodes, err := clientset.CoreV1().Nodes().List(ctx, metav1.ListOptions{Limit: 1})
if err == nil && len(nodes.Items) > 0 {
res.InstanceName = nodes.Items[0].Name
res.Flavor = nodes.Items[0].Labels["node.kubernetes.io/instance-type"]
}
return res
}
// ... GetPodDetails und StartScan bleiben gleich ...
func GetPodDetails(clientset *kubernetes.Clientset, namespace string) []PodDetails {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
var podDetailsList []PodDetails
pods, err := clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
if err != nil || len(pods.Items) == 0 {
return podDetailsList
}
for _, pod := range pods.Items {
var podDetails PodDetails
roles, ok := pod.Labels["spilo-role"]
if !ok {
roles = pod.Labels["postgres-operator/role"]
}
if roles == "" {
roles = "-"
}
podDetails.Role = roles
readyCount := 0
for _, stat := range pod.Status.ContainerStatuses {
if stat.Ready {
readyCount++
}
}
podDetails.Ready = fmt.Sprintf("%d/%d", readyCount, len(pod.Spec.Containers))
if len(pod.Status.ContainerStatuses) > 0 {
podDetails.Restarts = pod.Status.ContainerStatuses[0].RestartCount
}
podDetails.Name = pod.Name
podDetails.Age = fmt.Sprintf("%.0fd", time.Since(pod.CreationTimestamp.Time).Hours()/24)
podDetailsList = append(podDetailsList, podDetails)
}
return podDetailsList
}
func StartScan(dir string, targetNS string) tea.Cmd {
return func() tea.Msg {
files, _ := os.ReadDir(dir)
var results []ClusterResult
for _, f := range files {
if !f.IsDir() && (strings.HasSuffix(f.Name(), ".yaml") || strings.HasSuffix(f.Name(), ".yml")) {
results = append(results, CheckCluster(filepath.Join(dir, f.Name()), targetNS, 2))
}
}
return ScanFinishedMsg(results)
}
}

366
pkg/tui/types.go Normal file
View file

@ -0,0 +1,366 @@
package tui
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"main/pkg/scanner"
)
type Model struct {
results []scanner.ClusterResult
history []scanner.ClusterResult
table table.Model
podTable table.Model
textInput textinput.Model
loading bool
searching bool
namespace string
activeTab int
logoContent string
width int
height int
menuIndex int
showMenu bool
selectedCluster *scanner.ClusterResult
}
// Styles
var (
logoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("39")).MarginRight(4).Padding(1)
activeTabStyle = lipgloss.NewStyle().Background(lipgloss.Color("62")).Foreground(lipgloss.Color("255")).Padding(0, 2).Bold(true).MarginRight(1)
tabStyle = lipgloss.NewStyle().Background(lipgloss.Color("240")).Foreground(lipgloss.Color("252")).Padding(0, 2).MarginRight(1)
footerStyle = lipgloss.NewStyle().Background(lipgloss.Color("33")).Foreground(lipgloss.Color("255")).Bold(true).Padding(0, 1)
popupStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("62")).
Padding(1, 3).
Background(lipgloss.Color("235"))
activeOptionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("255")).Background(lipgloss.Color("62")).Bold(true).Padding(0, 1)
inactiveOptionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245")).Padding(0, 1)
)
func ReadLogo(path string) string {
content, err := os.ReadFile(path)
if err != nil {
return "SCANNER"
}
return string(content)
}
func GeneratePodRows(pods []scanner.PodDetails) []table.Row {
var rows []table.Row
for _, p := range pods {
healthIcon := "○"
healthColor := "240"
if strings.HasPrefix(p.Ready, "1/1") || strings.HasPrefix(p.Ready, "2/2") || strings.HasPrefix(p.Ready, "3/3") {
healthIcon = "●"
healthColor = "42"
}
formattedHealth := lipgloss.NewStyle().Foreground(lipgloss.Color(healthColor)).Render(healthIcon + " " + p.Ready)
roleDisplay := p.Role
if strings.ToLower(p.Role) == "master" || strings.ToLower(p.Role) == "primary" {
roleDisplay = lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Bold(true).Render("Primary")
}
rows = append(rows, table.Row{p.Name, roleDisplay, formattedHealth, fmt.Sprintf("%d", p.Restarts), p.Age})
}
return rows
}
func GenerateRows(results []scanner.ClusterResult) []table.Row {
var rows []table.Row
for _, res := range results {
name := strings.TrimPrefix(res.FileName, "stackit-eu01-")
name = strings.TrimSuffix(name, ".yaml")
if res.Found {
name = "✔ " + name
}
rows = append(rows, table.Row{name, res.InstanceName, res.Status, res.Flavor, res.Size, res.Age, res.ProjectID, res.PostgresVersion})
}
return rows
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
if m.searching {
switch msg.String() {
case "enter":
m.namespace = strings.TrimSpace(m.textInput.Value())
m.searching = false
m.loading = true
m.activeTab = 1
return m, scanner.StartScan("/Users/wallischo/.kube/ske-config/stackit/eu01/postgresql/prod", m.namespace)
case "esc":
m.searching = false
m.textInput.Blur()
return m, nil
}
m.textInput, cmd = m.textInput.Update(msg)
return m, cmd
}
if m.showMenu {
switch msg.String() {
case "up", "k":
m.menuIndex--
if m.menuIndex < 0 {
m.menuIndex = 2
}
case "down", "j":
m.menuIndex++
if m.menuIndex > 2 {
m.menuIndex = 0
}
case "enter":
m.showMenu = false
currIdx := m.table.Cursor()
var selected *scanner.ClusterResult
if m.activeTab == 1 && currIdx >= 0 && currIdx < len(m.results) {
selected = &m.results[currIdx]
} else if m.activeTab == 2 && currIdx >= 0 && currIdx < len(m.history) {
selected = &m.history[currIdx]
}
if selected == nil {
return m, nil
}
switch m.menuIndex {
case 0: // Grafana
url := fmt.Sprintf("https://grafana...", m.namespace)
exec.Command("open", url).Start()
case 1: // k9s
c := exec.Command("k9s", "-n", m.namespace, "--kubeconfig", filepath.Join("/Users/wallischo/.kube/ske-config/stackit/eu01/postgresql/prod", selected.FileName))
return m, tea.ExecProcess(c, func(err error) tea.Msg { return nil })
case 2: // Dashboard
m.activeTab = 3
m.selectedCluster = selected
baseDir := "/Users/wallischo/.kube/ske-config/stackit/eu01/postgresql/prod"
kubePath := filepath.Join(baseDir, selected.FileName)
log.Printf("🔍 Versuche Kubeconfig zu laden: %s", kubePath)
if _, err := os.Stat(kubePath); os.IsNotExist(err) {
log.Printf("❌ DATEI EXISTIERT NICHT: %s", kubePath)
return m, nil
}
config, err := clientcmd.BuildConfigFromFlags("", kubePath)
if err != nil {
log.Printf("❌ Fehler beim Laden der Config: %v", err)
return m, nil
}
clientset, err := kubernetes.NewForConfig(config)
if err == nil {
podDetails := scanner.GetPodDetails(clientset, m.namespace)
m.selectedCluster.Pods = podDetails
m.podTable.SetRows(GeneratePodRows(podDetails))
}
}
case "esc", "q":
m.showMenu = false
}
return m, nil
}
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "enter":
if (m.activeTab == 1 && len(m.results) > 0) || (m.activeTab == 2 && len(m.history) > 0) {
m.showMenu = true
m.menuIndex = 0
}
case "tab":
m.activeTab = (m.activeTab + 1) % 3
if m.activeTab == 1 {
m.table.SetRows(GenerateRows(m.results))
}
if m.activeTab == 2 {
m.table.SetRows(GenerateRows(m.history))
}
case "esc", "h":
if m.activeTab == 3 {
m.activeTab = 1
}
case "ctrl+f":
m.searching = true
m.textInput.Focus()
}
case scanner.ScanFinishedMsg:
res := []scanner.ClusterResult(msg)
m.results = res
m.loading = false
for _, r := range res {
if r.Found {
exists := false
for _, h := range m.history {
if h.FileName == r.FileName {
exists = true
break
}
}
if !exists {
m.history = append(m.history, r)
}
}
}
if m.activeTab == 1 {
m.table.SetRows(GenerateRows(m.results))
}
return m, nil
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
}
if m.activeTab == 3 {
m.podTable, cmd = m.podTable.Update(msg)
} else {
m.table, cmd = m.table.Update(msg)
}
return m, cmd
}
func (m Model) Init() tea.Cmd { return nil }
func InitialModel() Model {
logo := ReadLogo("/Users/wallischo/Documents/GolangProjects/InstanceScanner/assets/logo.txt")
columns := []table.Column{
{Title: "Cluster", Width: 30},
{Title: "Node", Width: 65},
{Title: "Status", Width: 15},
{Title: "Flavor", Width: 12},
{Title: "Size", Width: 8},
{Title: "Age", Width: 8},
{Title: "Project ID", Width: 40},
{Title: "Version", Width: 8},
}
podColumns := []table.Column{
{Title: "POD NAME", Width: 45},
{Title: "ROLE", Width: 12},
{Title: "HEALTH", Width: 12},
{Title: "RESTARTS", Width: 10},
{Title: "AGE", Width: 10},
}
t := table.New(table.WithColumns(columns), table.WithFocused(true), table.WithHeight(15))
pt := table.New(table.WithColumns(podColumns), table.WithFocused(true), table.WithHeight(8))
s := table.DefaultStyles()
s.Header = s.Header.BorderStyle(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color("240")).BorderBottom(true).Bold(true)
s.Selected = s.Selected.Foreground(lipgloss.Color("229")).Background(lipgloss.Color("62")).Bold(true)
t.SetStyles(s)
pt.SetStyles(s)
ti := textinput.New()
ti.Placeholder = "Namespace UUID eingeben..."
return Model{
table: t,
podTable: pt,
textInput: ti,
logoContent: logo,
activeTab: 1,
}
}
func (m Model) View() string {
tabNames := []string{"General Informations", "Instance", "History"}
var renderedTabs []string
for i, name := range tabNames {
isActive := i == m.activeTab || (m.activeTab == 3 && i == 1)
if isActive {
renderedTabs = append(renderedTabs, activeTabStyle.Render(name))
} else {
renderedTabs = append(renderedTabs, tabStyle.Render(name))
}
}
tabs := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...)
header := lipgloss.JoinHorizontal(lipgloss.Center, logoStyle.Render(m.logoContent), tabs)
var content string
if m.loading {
content = "\n 🔄 Suche läuft für Namespace: " + m.namespace
} else {
switch m.activeTab {
case 1, 2:
content = m.table.View()
case 3: // DASHBOARD
if m.selectedCluster != nil {
statusColor := "42"
if m.selectedCluster.Status != "Active" {
statusColor = "196"
}
statusStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(statusColor)).Bold(true)
headerInfo := lipgloss.JoinVertical(lipgloss.Left,
fmt.Sprintf(" INSTANCE STATUS: %s", statusStyle.Render(m.selectedCluster.Status)),
fmt.Sprintf(" Postgres Version: %s", m.selectedCluster.PostgresVersion),
fmt.Sprintf(" Age: %s", m.selectedCluster.Age),
)
headerBox := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("62")).Padding(0, 1).Width(45).Render(headerInfo)
podBox := lipgloss.NewStyle().Border(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color("240")).Padding(0, 1).MarginTop(1).Render(lipgloss.JoinVertical(lipgloss.Left, lipgloss.NewStyle().Bold(true).PaddingLeft(1).Render("[Cluster Nodes]"), m.podTable.View()))
storageInfo := fmt.Sprintf(" Name: %s\n Größe: %s\n Status: %s", m.selectedCluster.PVCName, m.selectedCluster.StorageSize, m.selectedCluster.StorageStatus)
storageBox := lipgloss.NewStyle().Border(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color("240")).Padding(0, 1).MarginTop(1).Width(m.width - 6).Render(lipgloss.JoinVertical(lipgloss.Left, lipgloss.NewStyle().Bold(true).Render("[Persistent Storage]"), storageInfo))
content = lipgloss.JoinVertical(lipgloss.Left, headerBox, podBox, storageBox)
}
default:
content = lipgloss.NewStyle().Margin(2).Foreground(lipgloss.Color("240")).Render("Wähle eine Instanz aus.")
}
}
searchInfo := " [Ctrl+F] Suche | [Enter] Aktion "
if m.searching {
searchInfo = " 🔎 UUID: " + m.textInput.View()
}
footer := footerStyle.Width(m.width).Render(fmt.Sprintf(" %s | NS: %s", searchInfo, m.namespace))
baseView := lipgloss.JoinVertical(lipgloss.Left, header, content, footer)
if !m.showMenu {
return baseView
}
optGrafana := inactiveOptionStyle.Render(" Grafana Dashboard ")
optK9s := inactiveOptionStyle.Render(" k9s Terminal ")
optDetails := inactiveOptionStyle.Render(" View Details ")
if m.menuIndex == 0 {
optGrafana = activeOptionStyle.Render("> Grafana Dashboard ")
}
if m.menuIndex == 1 {
optK9s = activeOptionStyle.Render("> k9s Terminal ")
}
if m.menuIndex == 2 {
optDetails = activeOptionStyle.Render("> View Details ")
}
modal := popupStyle.Render(lipgloss.JoinVertical(lipgloss.Center, lipgloss.NewStyle().Bold(true).MarginBottom(1).Render("AKTION WÄHLEN"), optGrafana, optK9s, optDetails))
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, modal, lipgloss.WithWhitespaceChars(baseView), lipgloss.WithWhitespaceForeground(lipgloss.Color("240")))
}