diff --git a/.github/workflows/clustertool.golangci-lint.yaml b/.github/workflows/clustertool.golangci-lint.yaml
new file mode 100644
index 0000000000000..d7d304c6ab45c
--- /dev/null
+++ b/.github/workflows/clustertool.golangci-lint.yaml
@@ -0,0 +1,34 @@
+name: golangci-lint
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+on:
+ # push:
+ # branches:
+ # - main
+ # tags:
+ # - "*"
+ # paths:
+ # - 'clustertool/**'
+ # - ".github/workflows/clsutertool.test.yaml"
+ #pull_request:
+ # branches:
+ # - main
+ # paths:
+ # - 'clustertool/**'
+ # - ".github/workflows/clsutertool.test.yaml"
+permissions:
+ contents: read
+jobs:
+ golangci:
+ name: lint
+ runs-on: actions-runner-large
+ steps:
+ - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5
+ with:
+ go-version: stable
+ - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
+ - name: golangci-lint
+ uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6
+ with:
+ version: latest
+ args: --timeout 3m0s
diff --git a/.github/workflows/clustertool.release.yaml b/.github/workflows/clustertool.release.yaml
new file mode 100644
index 0000000000000..d032cb34628c7
--- /dev/null
+++ b/.github/workflows/clustertool.release.yaml
@@ -0,0 +1,27 @@
+name: goreleaser
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+on:
+ push:
+ tags:
+ - "*"
+ paths:
+ - 'clustertool/**'
+jobs:
+ goreleaser:
+ runs-on: actions-runner-large
+ steps:
+ - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5
+ with:
+ go-version: stable
+ - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
+ with:
+ fetch-depth: 0
+ - uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6
+ with:
+ args: release --clean
+ distribution: goreleaser # or 'goreleaser-pro'
+ version: "~> v2" # or 'latest', 'nightly', semver
+ env:
+ GITHUB_TOKEN: "${{ secrets.BOT_TOKEN }}"
+
diff --git a/.github/workflows/clustertool.test.yaml b/.github/workflows/clustertool.test.yaml
new file mode 100644
index 0000000000000..386f9199ed8a6
--- /dev/null
+++ b/.github/workflows/clustertool.test.yaml
@@ -0,0 +1,43 @@
+name: clustertool-test
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+on:
+ push:
+ branches:
+ - main
+ tags:
+ - "*"
+ paths:
+ - 'main.go'
+ - 'go.mod'
+ - 'go.sum'
+ - 'pkg/**'
+ - 'cmd/**'
+ - 'clustertool/**'
+ - ".github/workflows/clsutertool.test.yaml"
+ pull_request:
+ branches:
+ - main
+ paths:
+ - 'main.go'
+ - 'go.mod'
+ - 'go.sum'
+ - 'pkg/**'
+ - 'cmd/**'
+ - 'clustertool/**'
+ - ".github/workflows/clsutertool.test.yaml"
+permissions:
+ contents: read
+jobs:
+ build:
+ runs-on: actions-runner-large
+ steps:
+ - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5
+ with:
+ go-version: stable
+ - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
+ - run: |
+ cd ./clustertool
+ go build -o /usr/local/bin/clustertool
+ go test -v ./... -race -covermode=atomic
+ clustertool-dev
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 34f02f21eae72..f77fe671625a3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -37,7 +37,7 @@ repos:
- id: check-added-large-files # prevents giant files from being committed.
exclude: \.(png|jpg|jpeg|svg|yaml|yml|tpl)$
- id: check-yaml # checks yaml files for parseable syntax.
- exclude: (^archive|templates\/.*|crds\/.*|questions.yaml|chart_schema.yaml|test-chart\/.*\.yaml)
+ exclude: (^archive|templates\/.*|crds\/.*|questions.yaml|chart_schema.yaml|test-chart\/.*\.yaml|^clustertool)
- id: detect-private-key # detects the presence of private keys.
exclude: ^archive/
@@ -53,5 +53,5 @@ repos:
rev: v3.1.0
hooks:
- id: prettier
- exclude: (^archive|templates\/.*|crds\/.*|README.md|CHANGELOG.md|questions.yaml|devcontainer.json)
+ exclude: (^archive|templates\/.*|crds\/.*|README.md|CHANGELOG.md|questions.yaml|devcontainer.json|^clustertool)
files: \.(js|ts|jsx|tsx|css|less|html|json|markdown|md|yaml|yml)$
diff --git a/DEVTRIGGER b/DEVTRIGGER
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/charts/incubator/pingvin-share/Chart.yaml b/charts/incubator/pingvin-share/Chart.yaml
index 045211f6f7431..9e3f035491081 100644
--- a/charts/incubator/pingvin-share/Chart.yaml
+++ b/charts/incubator/pingvin-share/Chart.yaml
@@ -6,7 +6,7 @@ annotations:
truecharts.org/min_helm_version: "3.11"
truecharts.org/train: incubator
apiVersion: v2
-appVersion: 1.2.0
+appVersion: 1.2.1
dependencies:
- name: common
version: 25.0.0
@@ -32,4 +32,4 @@ sources:
- https://github.com/truecharts/charts/tree/master/charts/incubator/pingvin-share
- https://hub.docker.com/r/stonith404/pingvin-share
type: application
-version: 2.3.0
+version: 2.3.1
diff --git a/charts/incubator/pingvin-share/values.yaml b/charts/incubator/pingvin-share/values.yaml
index ed9b7709040a1..f03b73940b5f1 100644
--- a/charts/incubator/pingvin-share/values.yaml
+++ b/charts/incubator/pingvin-share/values.yaml
@@ -1,7 +1,7 @@
image:
repository: stonith404/pingvin-share
pullPolicy: IfNotPresent
- tag: v1.2.0@sha256:a016d8fb674e59cb6ef17036113cb3f1b64c4614d79e42f68490fc18768ff3aa
+ tag: v1.2.1@sha256:2639387bc47b8d7a5c0c733e1ad39ba507632d53996ad8714dfbac2622f74b9e
securityContext:
container:
diff --git a/charts/stable/esphome/Chart.yaml b/charts/stable/esphome/Chart.yaml
index 07f847091ce98..69f0c9cb6369f 100644
--- a/charts/stable/esphome/Chart.yaml
+++ b/charts/stable/esphome/Chart.yaml
@@ -6,7 +6,7 @@ annotations:
truecharts.org/min_helm_version: "3.11"
truecharts.org/train: stable
apiVersion: v2
-appVersion: 2024.9.2
+appVersion: 2024.10.0
dependencies:
- name: common
version: 25.0.0
@@ -32,4 +32,4 @@ sources:
- https://github.com/truecharts/charts/tree/master/charts/stable/esphome
- https://hub.docker.com/r/esphome/esphome
type: application
-version: 21.6.0
+version: 21.7.0
diff --git a/charts/stable/esphome/values.yaml b/charts/stable/esphome/values.yaml
index 95e892f329458..f67d62462860f 100644
--- a/charts/stable/esphome/values.yaml
+++ b/charts/stable/esphome/values.yaml
@@ -1,7 +1,7 @@
image:
repository: esphome/esphome
pullPolicy: IfNotPresent
- tag: 2024.9.2@sha256:ec10ec2b28c1afe792cebdccbeadd8e6dd4a3b0be58b5451128fa6bdc60fccf0
+ tag: 2024.10.0@sha256:78f7d125ecff29061cfdc14dc77d60e69532aec6b28680eba19afa72b7ac5640
securityContext:
container:
runAsNonRoot: false
diff --git a/charts/stable/nginx-proxy-manager/Chart.yaml b/charts/stable/nginx-proxy-manager/Chart.yaml
index 7e271ba9b0278..99cfb3fa4b995 100644
--- a/charts/stable/nginx-proxy-manager/Chart.yaml
+++ b/charts/stable/nginx-proxy-manager/Chart.yaml
@@ -6,7 +6,7 @@ annotations:
truecharts.org/min_helm_version: "3.11"
truecharts.org/train: stable
apiVersion: v2
-appVersion: 2.11.3
+appVersion: 2.12.0
dependencies:
- name: common
version: 25.0.0
@@ -42,4 +42,4 @@ sources:
- https://hub.docker.com/r/jc21/nginx-proxy-manager
- https://nginxproxymanager.com/
type: application
-version: 11.6.2
+version: 11.7.0
diff --git a/charts/stable/nginx-proxy-manager/values.yaml b/charts/stable/nginx-proxy-manager/values.yaml
index d7b03a5ab3326..b7c1f5996aceb 100644
--- a/charts/stable/nginx-proxy-manager/values.yaml
+++ b/charts/stable/nginx-proxy-manager/values.yaml
@@ -1,7 +1,7 @@
image:
repository: jc21/nginx-proxy-manager
pullPolicy: IfNotPresent
- tag: 2.11.3@sha256:e81d01ad119208334ba7adb4986b36ebeb7c727a78732f411e5df84f7c6c50ec
+ tag: 2.12.0@sha256:15fb87417a36246e30a6c894b437c3aab031b692f59b77245cb61008397b7890
service:
main:
ports:
diff --git a/charts/stable/watcharr/Chart.yaml b/charts/stable/watcharr/Chart.yaml
index fbdf4fcc7afa4..abd15231f20a5 100644
--- a/charts/stable/watcharr/Chart.yaml
+++ b/charts/stable/watcharr/Chart.yaml
@@ -6,7 +6,7 @@ annotations:
truecharts.org/min_helm_version: "3.11"
truecharts.org/train: stable
apiVersion: v2
-appVersion: 1.44.1
+appVersion: 1.44.2
dependencies:
- name: common
version: 25.0.0
@@ -33,4 +33,4 @@ sources:
- https://github.com/sbondCo/Watcharr
- https://github.com/truecharts/charts/tree/master/charts/stable/watcharr
type: application
-version: 6.7.1
+version: 6.7.2
diff --git a/charts/stable/watcharr/values.yaml b/charts/stable/watcharr/values.yaml
index c103b990a9765..297ef23b97f48 100644
--- a/charts/stable/watcharr/values.yaml
+++ b/charts/stable/watcharr/values.yaml
@@ -1,6 +1,6 @@
image:
repository: ghcr.io/sbondco/watcharr
- tag: v1.44.1@sha256:90c2c58cd0d2d74ada7eaaf0350fb7c928b0d89f0031dc1c93a0bae653fcbe8c
+ tag: v1.44.2@sha256:85df9a7f4e7cb73d89edf546c9458a096d40d2641c22e36d6ad41aa15da47625
pullPolicy: IfNotPresent
securityContext:
diff --git a/charts/system/lvm-disk-watcher/LICENSE b/charts/system/lvm-disk-watcher/LICENSE
new file mode 100644
index 0000000000000..33a8cbb23f017
--- /dev/null
+++ b/charts/system/lvm-disk-watcher/LICENSE
@@ -0,0 +1,106 @@
+Business Source License 1.1
+
+Parameters
+
+Licensor: The TrueCharts Project, it's owner and it's contributors
+Licensed Work: The TrueCharts "Blocky" Helm Chart
+Additional Use Grant: You may use the licensed work in production, as long
+ as it is directly sourced from a TrueCharts provided
+ official repository, catalog or source. You may also make private
+ modification to the directly sourced licenced work,
+ when used in production.
+
+ The following cases are, due to their nature, also
+ defined as 'production use' and explicitly prohibited:
+ - Bundling, including or displaying the licensed work
+ with(in) another work intended for production use,
+ with the apparent intend of facilitating and/or
+ promoting production use by third parties in
+ violation of this license.
+
+Change Date: 2050-01-01
+
+Change License: 3-clause BSD license
+
+For information about alternative licensing arrangements for the Software,
+please contact: legal@truecharts.org
+
+Notice
+
+The Business Source License (this document, or the “License”) is not an Open
+Source license. However, the Licensed Work will eventually be made available
+under an Open Source License, as stated in this License.
+
+License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
+“Business Source License” is a trademark of MariaDB Corporation Ab.
+
+-----------------------------------------------------------------------------
+
+Business Source License 1.1
+
+Terms
+
+The Licensor hereby grants you the right to copy, modify, create derivative
+works, redistribute, and make non-production use of the Licensed Work. The
+Licensor may make an Additional Use Grant, above, permitting limited
+production use.
+
+Effective on the Change Date, or the fourth anniversary of the first publicly
+available distribution of a specific version of the Licensed Work under this
+License, whichever comes first, the Licensor hereby grants you rights under
+the terms of the Change License, and the rights granted in the paragraph
+above terminate.
+
+If your use of the Licensed Work does not comply with the requirements
+currently in effect as described in this License, you must purchase a
+commercial license from the Licensor, its affiliated entities, or authorized
+resellers, or you must refrain from using the Licensed Work.
+
+All copies of the original and modified Licensed Work, and derivative works
+of the Licensed Work, are subject to this License. This License applies
+separately for each version of the Licensed Work and the Change Date may vary
+for each version of the Licensed Work released by Licensor.
+
+You must conspicuously display this License on each original or modified copy
+of the Licensed Work. If you receive the Licensed Work in original or
+modified form from a third party, the terms and conditions set forth in this
+License apply to your use of that work.
+
+Any use of the Licensed Work in violation of this License will automatically
+terminate your rights under this License for the current and all other
+versions of the Licensed Work.
+
+This License does not grant you any right in any trademark or logo of
+Licensor or its affiliates (provided that you may use a trademark or logo of
+Licensor as expressly required by this License).
+
+TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
+AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
+EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
+TITLE.
+
+MariaDB hereby grants you permission to use this License’s text to license
+your works, and to refer to it using the trademark “Business Source License”,
+as long as you comply with the Covenants of Licensor below.
+
+Covenants of Licensor
+
+In consideration of the right to use this License’s text and the “Business
+Source License” name and trademark, Licensor covenants to MariaDB, and to all
+other recipients of the licensed work to be provided by Licensor:
+
+1. To specify as the Change License the GPL Version 2.0 or any later version,
+ or a license that is compatible with GPL Version 2.0 or a later version,
+ where “compatible” means that software provided under the Change License can
+ be included in a program with software provided under GPL Version 2.0 or a
+ later version. Licensor may specify additional Change Licenses without
+ limitation.
+
+2. To either: (a) specify an additional grant of rights to use that does not
+ impose any additional restriction on the right granted in this License, as
+ the Additional Use Grant; or (b) insert the text “None”.
+
+3. To specify a Change Date.
+
+4. Not to modify this License in any other way.
diff --git a/charttool.LICENSE b/charttool.LICENSE
index 8501143454020..33a8cbb23f017 100644
--- a/charttool.LICENSE
+++ b/charttool.LICENSE
@@ -1,5 +1,106 @@
-ChartTool:
-All rights reserved Kjeld Schouten
+Business Source License 1.1
-This tool can freely be used for development of Helm charts including the TrueCharts common chart.
-This tool cannot be used for commercial purposses without prior explicit permission
+Parameters
+
+Licensor: The TrueCharts Project, it's owner and it's contributors
+Licensed Work: The TrueCharts "Blocky" Helm Chart
+Additional Use Grant: You may use the licensed work in production, as long
+ as it is directly sourced from a TrueCharts provided
+ official repository, catalog or source. You may also make private
+ modification to the directly sourced licenced work,
+ when used in production.
+
+ The following cases are, due to their nature, also
+ defined as 'production use' and explicitly prohibited:
+ - Bundling, including or displaying the licensed work
+ with(in) another work intended for production use,
+ with the apparent intend of facilitating and/or
+ promoting production use by third parties in
+ violation of this license.
+
+Change Date: 2050-01-01
+
+Change License: 3-clause BSD license
+
+For information about alternative licensing arrangements for the Software,
+please contact: legal@truecharts.org
+
+Notice
+
+The Business Source License (this document, or the “License”) is not an Open
+Source license. However, the Licensed Work will eventually be made available
+under an Open Source License, as stated in this License.
+
+License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
+“Business Source License” is a trademark of MariaDB Corporation Ab.
+
+-----------------------------------------------------------------------------
+
+Business Source License 1.1
+
+Terms
+
+The Licensor hereby grants you the right to copy, modify, create derivative
+works, redistribute, and make non-production use of the Licensed Work. The
+Licensor may make an Additional Use Grant, above, permitting limited
+production use.
+
+Effective on the Change Date, or the fourth anniversary of the first publicly
+available distribution of a specific version of the Licensed Work under this
+License, whichever comes first, the Licensor hereby grants you rights under
+the terms of the Change License, and the rights granted in the paragraph
+above terminate.
+
+If your use of the Licensed Work does not comply with the requirements
+currently in effect as described in this License, you must purchase a
+commercial license from the Licensor, its affiliated entities, or authorized
+resellers, or you must refrain from using the Licensed Work.
+
+All copies of the original and modified Licensed Work, and derivative works
+of the Licensed Work, are subject to this License. This License applies
+separately for each version of the Licensed Work and the Change Date may vary
+for each version of the Licensed Work released by Licensor.
+
+You must conspicuously display this License on each original or modified copy
+of the Licensed Work. If you receive the Licensed Work in original or
+modified form from a third party, the terms and conditions set forth in this
+License apply to your use of that work.
+
+Any use of the Licensed Work in violation of this License will automatically
+terminate your rights under this License for the current and all other
+versions of the Licensed Work.
+
+This License does not grant you any right in any trademark or logo of
+Licensor or its affiliates (provided that you may use a trademark or logo of
+Licensor as expressly required by this License).
+
+TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
+AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
+EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
+TITLE.
+
+MariaDB hereby grants you permission to use this License’s text to license
+your works, and to refer to it using the trademark “Business Source License”,
+as long as you comply with the Covenants of Licensor below.
+
+Covenants of Licensor
+
+In consideration of the right to use this License’s text and the “Business
+Source License” name and trademark, Licensor covenants to MariaDB, and to all
+other recipients of the licensed work to be provided by Licensor:
+
+1. To specify as the Change License the GPL Version 2.0 or any later version,
+ or a license that is compatible with GPL Version 2.0 or a later version,
+ where “compatible” means that software provided under the Change License can
+ be included in a program with software provided under GPL Version 2.0 or a
+ later version. Licensor may specify additional Change Licenses without
+ limitation.
+
+2. To either: (a) specify an additional grant of rights to use that does not
+ impose any additional restriction on the right granted in this License, as
+ the Additional Use Grant; or (b) insert the text “None”.
+
+3. To specify a Change Date.
+
+4. Not to modify this License in any other way.
diff --git a/clustertool.LICENSE b/clustertool.LICENSE
new file mode 100644
index 0000000000000..33a8cbb23f017
--- /dev/null
+++ b/clustertool.LICENSE
@@ -0,0 +1,106 @@
+Business Source License 1.1
+
+Parameters
+
+Licensor: The TrueCharts Project, it's owner and it's contributors
+Licensed Work: The TrueCharts "Blocky" Helm Chart
+Additional Use Grant: You may use the licensed work in production, as long
+ as it is directly sourced from a TrueCharts provided
+ official repository, catalog or source. You may also make private
+ modification to the directly sourced licenced work,
+ when used in production.
+
+ The following cases are, due to their nature, also
+ defined as 'production use' and explicitly prohibited:
+ - Bundling, including or displaying the licensed work
+ with(in) another work intended for production use,
+ with the apparent intend of facilitating and/or
+ promoting production use by third parties in
+ violation of this license.
+
+Change Date: 2050-01-01
+
+Change License: 3-clause BSD license
+
+For information about alternative licensing arrangements for the Software,
+please contact: legal@truecharts.org
+
+Notice
+
+The Business Source License (this document, or the “License”) is not an Open
+Source license. However, the Licensed Work will eventually be made available
+under an Open Source License, as stated in this License.
+
+License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
+“Business Source License” is a trademark of MariaDB Corporation Ab.
+
+-----------------------------------------------------------------------------
+
+Business Source License 1.1
+
+Terms
+
+The Licensor hereby grants you the right to copy, modify, create derivative
+works, redistribute, and make non-production use of the Licensed Work. The
+Licensor may make an Additional Use Grant, above, permitting limited
+production use.
+
+Effective on the Change Date, or the fourth anniversary of the first publicly
+available distribution of a specific version of the Licensed Work under this
+License, whichever comes first, the Licensor hereby grants you rights under
+the terms of the Change License, and the rights granted in the paragraph
+above terminate.
+
+If your use of the Licensed Work does not comply with the requirements
+currently in effect as described in this License, you must purchase a
+commercial license from the Licensor, its affiliated entities, or authorized
+resellers, or you must refrain from using the Licensed Work.
+
+All copies of the original and modified Licensed Work, and derivative works
+of the Licensed Work, are subject to this License. This License applies
+separately for each version of the Licensed Work and the Change Date may vary
+for each version of the Licensed Work released by Licensor.
+
+You must conspicuously display this License on each original or modified copy
+of the Licensed Work. If you receive the Licensed Work in original or
+modified form from a third party, the terms and conditions set forth in this
+License apply to your use of that work.
+
+Any use of the Licensed Work in violation of this License will automatically
+terminate your rights under this License for the current and all other
+versions of the Licensed Work.
+
+This License does not grant you any right in any trademark or logo of
+Licensor or its affiliates (provided that you may use a trademark or logo of
+Licensor as expressly required by this License).
+
+TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
+AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
+EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
+TITLE.
+
+MariaDB hereby grants you permission to use this License’s text to license
+your works, and to refer to it using the trademark “Business Source License”,
+as long as you comply with the Covenants of Licensor below.
+
+Covenants of Licensor
+
+In consideration of the right to use this License’s text and the “Business
+Source License” name and trademark, Licensor covenants to MariaDB, and to all
+other recipients of the licensed work to be provided by Licensor:
+
+1. To specify as the Change License the GPL Version 2.0 or any later version,
+ or a license that is compatible with GPL Version 2.0 or a later version,
+ where “compatible” means that software provided under the Change License can
+ be included in a program with software provided under GPL Version 2.0 or a
+ later version. Licensor may specify additional Change Licenses without
+ limitation.
+
+2. To either: (a) specify an additional grant of rights to use that does not
+ impose any additional restriction on the right granted in this License, as
+ the Additional Use Grant; or (b) insert the text “None”.
+
+3. To specify a Change Date.
+
+4. Not to modify this License in any other way.
diff --git a/clustertool/.gitignore b/clustertool/.gitignore
new file mode 100644
index 0000000000000..557670c158d3c
--- /dev/null
+++ b/clustertool/.gitignore
@@ -0,0 +1,20 @@
+.direnv
+dist/
+.cr-gpg
+tgz_cache
+index_cache
+clustertool
+talconfig.json
+/talconfig.yaml
+/talenv.yaml
+talsecret.yaml
+.sops.yaml
+/clusterconfig/
+age.agekey
+/clusters
+/repositories
+ssh-public-key.txt
+*DS_Store
+.DS_Store
+/lvm-operator
+*.DS_Store
\ No newline at end of file
diff --git a/clustertool/.vscode/extensions.json b/clustertool/.vscode/extensions.json
new file mode 100644
index 0000000000000..bfa3665723424
--- /dev/null
+++ b/clustertool/.vscode/extensions.json
@@ -0,0 +1,23 @@
+{
+ "recommendations": [
+ "golang.go",
+ "redhat.vscode-yaml",
+ "formulahendry.code-runner",
+ "streetsidesoftware.code-spell-checker",
+ "Codium.codium",
+ "mrmlnc.vscode-duplicate",
+ "IgorSbitnev.error-gutters",
+ "usernamehw.errorlens",
+ "mhutchie.git-graph",
+ "eamodio.gitlens",
+ "vivaldy22.go-auto-struct-tag",
+ "msyrus.go-doc",
+ "766b.go-outliner",
+ "premparihar.gotestexplorer",
+ "yzhang.markdown-all-in-one",
+ "searKing.preview-vscode",
+ "zxh404.vscode-proto3",
+ "DavidAnson.vscode-markdownlint",
+ "GitHub.copilot"
+ ]
+}
\ No newline at end of file
diff --git a/clustertool/.vscode/settings.json b/clustertool/.vscode/settings.json
new file mode 100644
index 0000000000000..0e0dcd235c497
--- /dev/null
+++ b/clustertool/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+
+}
\ No newline at end of file
diff --git a/clustertool/.vscode/tasks.json b/clustertool/.vscode/tasks.json
new file mode 100644
index 0000000000000..eaf4171780a08
--- /dev/null
+++ b/clustertool/.vscode/tasks.json
@@ -0,0 +1,22 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Install All Recommended Extensions",
+ "type": "shell",
+ "windows": {
+ "command": "foreach ($ext in (Get-Content -Raw .vscode/extensions.json | ConvertFrom-Json).recommendations) { Write-Host Installing $ext; code --install-extension $ext; }"
+ },
+ "linux": {
+ "command": "cat .vscode/extensions.json | jq .recommendations[] | xargs -n 1 code . --install-extension"
+ },
+ "runOptions": {
+ "runOn": "folderOpen"
+ },
+ "presentation": {
+ "reveal": "silent"
+ },
+ "problemMatcher" : []
+ },
+ ]
+}
\ No newline at end of file
diff --git a/clustertool/DEVTRIGGER b/clustertool/DEVTRIGGER
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/clustertool/LICENSE b/clustertool/LICENSE
new file mode 100644
index 0000000000000..33a8cbb23f017
--- /dev/null
+++ b/clustertool/LICENSE
@@ -0,0 +1,106 @@
+Business Source License 1.1
+
+Parameters
+
+Licensor: The TrueCharts Project, it's owner and it's contributors
+Licensed Work: The TrueCharts "Blocky" Helm Chart
+Additional Use Grant: You may use the licensed work in production, as long
+ as it is directly sourced from a TrueCharts provided
+ official repository, catalog or source. You may also make private
+ modification to the directly sourced licenced work,
+ when used in production.
+
+ The following cases are, due to their nature, also
+ defined as 'production use' and explicitly prohibited:
+ - Bundling, including or displaying the licensed work
+ with(in) another work intended for production use,
+ with the apparent intend of facilitating and/or
+ promoting production use by third parties in
+ violation of this license.
+
+Change Date: 2050-01-01
+
+Change License: 3-clause BSD license
+
+For information about alternative licensing arrangements for the Software,
+please contact: legal@truecharts.org
+
+Notice
+
+The Business Source License (this document, or the “License”) is not an Open
+Source license. However, the Licensed Work will eventually be made available
+under an Open Source License, as stated in this License.
+
+License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
+“Business Source License” is a trademark of MariaDB Corporation Ab.
+
+-----------------------------------------------------------------------------
+
+Business Source License 1.1
+
+Terms
+
+The Licensor hereby grants you the right to copy, modify, create derivative
+works, redistribute, and make non-production use of the Licensed Work. The
+Licensor may make an Additional Use Grant, above, permitting limited
+production use.
+
+Effective on the Change Date, or the fourth anniversary of the first publicly
+available distribution of a specific version of the Licensed Work under this
+License, whichever comes first, the Licensor hereby grants you rights under
+the terms of the Change License, and the rights granted in the paragraph
+above terminate.
+
+If your use of the Licensed Work does not comply with the requirements
+currently in effect as described in this License, you must purchase a
+commercial license from the Licensor, its affiliated entities, or authorized
+resellers, or you must refrain from using the Licensed Work.
+
+All copies of the original and modified Licensed Work, and derivative works
+of the Licensed Work, are subject to this License. This License applies
+separately for each version of the Licensed Work and the Change Date may vary
+for each version of the Licensed Work released by Licensor.
+
+You must conspicuously display this License on each original or modified copy
+of the Licensed Work. If you receive the Licensed Work in original or
+modified form from a third party, the terms and conditions set forth in this
+License apply to your use of that work.
+
+Any use of the Licensed Work in violation of this License will automatically
+terminate your rights under this License for the current and all other
+versions of the Licensed Work.
+
+This License does not grant you any right in any trademark or logo of
+Licensor or its affiliates (provided that you may use a trademark or logo of
+Licensor as expressly required by this License).
+
+TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
+AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
+EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
+TITLE.
+
+MariaDB hereby grants you permission to use this License’s text to license
+your works, and to refer to it using the trademark “Business Source License”,
+as long as you comply with the Covenants of Licensor below.
+
+Covenants of Licensor
+
+In consideration of the right to use this License’s text and the “Business
+Source License” name and trademark, Licensor covenants to MariaDB, and to all
+other recipients of the licensed work to be provided by Licensor:
+
+1. To specify as the Change License the GPL Version 2.0 or any later version,
+ or a license that is compatible with GPL Version 2.0 or a later version,
+ where “compatible” means that software provided under the Change License can
+ be included in a program with software provided under GPL Version 2.0 or a
+ later version. Licensor may specify additional Change Licenses without
+ limitation.
+
+2. To either: (a) specify an additional grant of rights to use that does not
+ impose any additional restriction on the right granted in this License, as
+ the Additional Use Grant; or (b) insert the text “None”.
+
+3. To specify a Change Date.
+
+4. Not to modify this License in any other way.
diff --git a/clustertool/README.md b/clustertool/README.md
new file mode 100644
index 0000000000000..2790f812c763f
--- /dev/null
+++ b/clustertool/README.md
@@ -0,0 +1,33 @@
+
+
+
clustertool
+
+ [![GitHub release (release name instead of tag name)](https://img.shields.io/github/v/release/truecharts/clustertool?include_prereleases)](https://github.com/truecharts/private/clustertool/releases)
+ [![GitHub issues](https://img.shields.io/github/issues/truecharts/clustertool)](https://github.com/truecharts/private/clustertool/issues)
+
+
+ A helper tool to help building TrueCharts helm charts
+
+ ·
+ Report Bug
+ ·
+ Request Feature
+
+
+
+## About The Project
+
+## CLA: Before contributing
+
+Our CLA applies to this repository as well.
+
+Please ensure it's signed before submitting a PR:
+https://cla-assistant.io/truecharts/charts
+
+*We use the truecharts/charts link, as CLA assistant does not allow for creating a custom link to a private repository.
+However, they all sign the same CLA that applies project-wide*
+
+## License
+
+This tool is released as "All Rights reserved" and hence it's code should NOT be shared. This includes binaries that are modified to remove TrueCharts specific code, unless approved by Kjeld Schouten "The Owner".
+Those that are invited to have access to repository, are free to edit and use this code for testing purposes, as long as its sourcecode is not shared or saved in-the-cloud.
diff --git a/clustertool/changelog.tmpl b/clustertool/changelog.tmpl
new file mode 100644
index 0000000000000..031154498272c
--- /dev/null
+++ b/clustertool/changelog.tmpl
@@ -0,0 +1,18 @@
+---
+title: Changelog
+pagefind: false
+---
+
+All history information can be found at [Github History](https://github.com/truecharts/charts/commits/master/charts/{{ .Train }}/{{ .Name }})
+
+:::tip
+
+If you need more than 2 scrolls to find your current version, please consider updating the chart as soon as possible.
+
+:::
+{{ range $key := .SortedVersions }}
+## {{ $key }} • [Train: {{ (index $.Versions $key).Train }}]
+{{ range $commit := (index $.Versions $key).SortedCommits }}
+- {{ printf "%s • [`%s`](https://github.com/truecharts/charts/commit/%s) • [@%s] (%s)" $commit.Message (slice $commit.CommitHash 0 7) $commit.CommitHash $commit.Author.Name $commit.Author.Date -}}
+{{ end }}
+{{ end -}}
diff --git a/clustertool/cmd/adv.go b/clustertool/cmd/adv.go
new file mode 100644
index 0000000000000..dddb2c6667090
--- /dev/null
+++ b/clustertool/cmd/adv.go
@@ -0,0 +1,24 @@
+package cmd
+
+import (
+ "strings"
+
+ "github.com/spf13/cobra"
+)
+
+var advLongHelp = strings.TrimSpace(`
+These are all advanced commands that should generally not be needed
+
+`)
+
+var adv = &cobra.Command{
+ Use: "adv",
+ Short: "Advanced cluster maintanence commands",
+ Long: advLongHelp,
+ SilenceUsage: true,
+ SilenceErrors: true,
+}
+
+func init() {
+ RootCmd.AddCommand(adv)
+}
diff --git a/clustertool/cmd/adv_bootstrap.go b/clustertool/cmd/adv_bootstrap.go
new file mode 100644
index 0000000000000..e9aa033342b26
--- /dev/null
+++ b/clustertool/cmd/adv_bootstrap.go
@@ -0,0 +1,20 @@
+package cmd
+
+import (
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/gencmd"
+)
+
+var bootstrap = &cobra.Command{
+ Use: "bootstrap",
+ Short: "bootstrap first Talos Node",
+ Run: bootstrapfunc,
+}
+
+func bootstrapfunc(cmd *cobra.Command, args []string) {
+ gencmd.RunBootstrap(args)
+}
+
+func init() {
+ adv.AddCommand(bootstrap)
+}
diff --git a/clustertool/cmd/adv_fluxbootstrap.go b/clustertool/cmd/adv_fluxbootstrap.go
new file mode 100644
index 0000000000000..8af2a45d3c810
--- /dev/null
+++ b/clustertool/cmd/adv_fluxbootstrap.go
@@ -0,0 +1,21 @@
+package cmd
+
+import (
+ "context"
+
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/fluxhandler"
+)
+
+var fluxbootstrap = &cobra.Command{
+ Use: "fluxbootstrap",
+ Short: "Manually bootstrap fluxcd on existing cluster",
+ Run: func(cmd *cobra.Command, args []string) {
+ ctx := context.Background()
+ fluxhandler.FluxBootstrap(ctx)
+ },
+}
+
+func init() {
+ adv.AddCommand(fluxbootstrap)
+}
diff --git a/clustertool/cmd/adv_health.go b/clustertool/cmd/adv_health.go
new file mode 100644
index 0000000000000..1c494b22bf8ce
--- /dev/null
+++ b/clustertool/cmd/adv_health.go
@@ -0,0 +1,26 @@
+package cmd
+
+import (
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/gencmd"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+ "github.com/truecharts/private/clustertool/pkg/sops"
+)
+
+var health = &cobra.Command{
+ Use: "health",
+ Short: "Check Talos Cluster Health",
+ Run: func(cmd *cobra.Command, args []string) {
+ if err := sops.DecryptFiles(); err != nil {
+ log.Info().Msgf("Error decrypting files: %v\n", err)
+ }
+ log.Info().Msg("Running Cluster HealthCheck")
+ healthcmd := gencmd.GenHealth(helper.TalEnv["VIP_IP"])
+ gencmd.ExecCmd(healthcmd)
+ },
+}
+
+func init() {
+ adv.AddCommand(health)
+}
diff --git a/clustertool/cmd/adv_precommit.go b/clustertool/cmd/adv_precommit.go
new file mode 100644
index 0000000000000..c6c877b03eef6
--- /dev/null
+++ b/clustertool/cmd/adv_precommit.go
@@ -0,0 +1,24 @@
+package cmd
+
+import (
+ "os"
+
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/sops"
+)
+
+var precommit = &cobra.Command{
+ Use: "precommit",
+ Short: "Runs the PreCommit encryption check",
+ Run: func(cmd *cobra.Command, args []string) {
+ if err := sops.CheckFilesAndReportEncryption(true, true); err != nil {
+ log.Info().Msgf("Error checking files: %v\n", err)
+ os.Exit(1)
+ }
+ },
+}
+
+func init() {
+ adv.AddCommand(precommit)
+}
diff --git a/clustertool/cmd/adv_reset.go b/clustertool/cmd/adv_reset.go
new file mode 100644
index 0000000000000..af6a20fc11e7f
--- /dev/null
+++ b/clustertool/cmd/adv_reset.go
@@ -0,0 +1,41 @@
+package cmd
+
+import (
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/gencmd"
+ "github.com/truecharts/private/clustertool/pkg/sops"
+)
+
+var reset = &cobra.Command{
+ Use: "reset",
+ Short: "Reset Talos Nodes and Kubernetes",
+ Run: func(cmd *cobra.Command, args []string) {
+ var extraArgs []string
+ node := ""
+
+ if len(args) > 1 {
+ extraArgs = args[1:]
+ }
+ if len(args) >= 1 {
+ node = args[0]
+ if args[0] == "all" {
+ node = ""
+ }
+ }
+
+ if err := sops.DecryptFiles(); err != nil {
+ log.Info().Msgf("Error decrypting files: %v\n", err)
+ }
+
+ log.Info().Msg("Running Cluster node Reset")
+
+ taloscmds := gencmd.GenReset(node, extraArgs)
+ gencmd.ExecCmds(taloscmds, true)
+
+ },
+}
+
+func init() {
+ adv.AddCommand(reset)
+}
diff --git a/clustertool/cmd/adv_scaleexport.go b/clustertool/cmd/adv_scaleexport.go
new file mode 100644
index 0000000000000..c510d0dbf14c4
--- /dev/null
+++ b/clustertool/cmd/adv_scaleexport.go
@@ -0,0 +1,18 @@
+package cmd
+
+import (
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/scale"
+)
+
+var scaleexport = &cobra.Command{
+ Use: "scaleexport",
+ Short: "Export SCALE Apps to file",
+ Run: func(cmd *cobra.Command, args []string) {
+ scale.ExportApps()
+ },
+}
+
+func init() {
+ adv.AddCommand(scaleexport)
+}
diff --git a/clustertool/cmd/adv_scalemigrate.go b/clustertool/cmd/adv_scalemigrate.go
new file mode 100644
index 0000000000000..201b604e0881c
--- /dev/null
+++ b/clustertool/cmd/adv_scalemigrate.go
@@ -0,0 +1,22 @@
+package cmd
+
+import (
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/scale"
+)
+
+var scalemigrate = &cobra.Command{
+ Use: "scalemigrate",
+ Short: "Migrate exported SCALE Apps to the Talos Cluster",
+ Run: func(cmd *cobra.Command, args []string) {
+ err := scale.ProcessJSONFiles("./truenas_exports")
+ if err != nil {
+ log.Info().Msgf("Error: %v", err)
+ }
+ },
+}
+
+func init() {
+ adv.AddCommand(scalemigrate)
+}
diff --git a/clustertool/cmd/adv_testcmd.go b/clustertool/cmd/adv_testcmd.go
new file mode 100644
index 0000000000000..4c4686a328852
--- /dev/null
+++ b/clustertool/cmd/adv_testcmd.go
@@ -0,0 +1,26 @@
+package cmd
+
+import (
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/gencmd"
+ "github.com/truecharts/private/clustertool/pkg/initfiles"
+)
+
+var testcmd = &cobra.Command{
+ Use: "test",
+ Short: "test run",
+ Run: func(cmd *cobra.Command, args []string) {
+ initfiles.LoadTalEnv()
+ // err := fluxhandler.ProcessJSONFiles("./testdata/truenas_exports")
+ // if err != nil {
+ // log.Info().Msg("Error:", err)
+ // }
+ something := gencmd.GenBootstrap("", []string{})
+ log.Info().Msgf("test %v", something)
+ },
+}
+
+func init() {
+ adv.AddCommand(testcmd)
+}
diff --git a/clustertool/cmd/apply.go b/clustertool/cmd/apply.go
new file mode 100644
index 0000000000000..ee5facd531710
--- /dev/null
+++ b/clustertool/cmd/apply.go
@@ -0,0 +1,78 @@
+package cmd
+
+import (
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/gencmd"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+ "github.com/truecharts/private/clustertool/pkg/nodestatus"
+ "github.com/truecharts/private/clustertool/pkg/sops"
+)
+
+var apply = &cobra.Command{
+ Use: "apply",
+ Short: "apply TalosConfig",
+ Run: func(cmd *cobra.Command, args []string) {
+ var extraArgs []string
+ node := ""
+
+ if len(args) > 1 {
+ extraArgs = args[1:]
+ }
+ if len(args) >= 1 {
+ node = args[0]
+ if args[0] == "all" {
+ node = ""
+ }
+ }
+
+ if err := sops.DecryptFiles(); err != nil {
+ log.Info().Msgf("Error decrypting files: %v\n", err)
+ }
+
+ bootstrapcmds := gencmd.GenBootstrap("", extraArgs)
+ bootstrapNode := helper.ExtractNode(bootstrapcmds)
+
+ log.Info().Msgf("Checking if first node is ready to recieve anything... %s", bootstrapNode)
+ status, err := nodestatus.WaitForHealth(bootstrapNode, []string{"running", "maintenance"})
+ if err != nil {
+
+ } else if status == "maintenance" {
+ bootstrapNeeded, err := nodestatus.CheckNeedBootstrap(bootstrapNode)
+ if err != nil {
+
+ } else if bootstrapNeeded {
+ log.Info().Msg("First Node requires to be bootstrapped before it can be used.")
+ if helper.GetYesOrNo("Do you want to bootstrap now? (yes/no) [y/n]: ") {
+ gencmd.RunBootstrap(extraArgs)
+ if helper.GetYesOrNo("Do you want to apply config to all remaining clusternodes as well? (yes/no) [y/n]: ") {
+ RunApply("", extraArgs)
+ }
+ } else {
+ log.Info().Msg("Exiting bootstrap, as apply is not possible...")
+ }
+
+ } else {
+ log.Info().Msg("Detected maintenance mode, but first node does not require to be bootrapped.")
+ log.Info().Msg("Assuming apply is requested... continuing with Apply...")
+ RunApply(node, extraArgs)
+ }
+
+ } else if status == "running" {
+ log.Info().Msg("Apply: running first controlnode detected, continuing...")
+ RunApply(node, extraArgs)
+ }
+ },
+}
+
+func RunApply(node string, extraArgs []string) {
+ taloscmds := gencmd.GenApply(node, extraArgs)
+ gencmd.ExecCmds(taloscmds, true)
+
+ kubeconfigcmds := gencmd.GenKubeConfig(helper.TalEnv["VIP_IP"])
+ gencmd.ExecCmd(kubeconfigcmds)
+}
+
+func init() {
+ RootCmd.AddCommand(apply)
+}
diff --git a/clustertool/cmd/charts.go b/clustertool/cmd/charts.go
new file mode 100644
index 0000000000000..db653d2c2e644
--- /dev/null
+++ b/clustertool/cmd/charts.go
@@ -0,0 +1,31 @@
+package cmd
+
+import (
+ "strings"
+
+ "github.com/spf13/cobra"
+)
+
+var chartsLongHelp = strings.TrimSpace(`
+charttool is a tool to help you build TrueCharts Charts
+
+Workflow:
+ Create talconfig.yaml file defining your nodes information like so:
+
+ Available commands
+ > charttool bump 1.2.3 patch
+ > charttool tagclean soemtag@somedigest
+
+`)
+
+var charts = &cobra.Command{
+ Use: "charts",
+ Short: "A tool to help with creating Talos cluster",
+ Long: chartsLongHelp,
+ SilenceUsage: true,
+ SilenceErrors: true,
+}
+
+func init() {
+ RootCmd.AddCommand(charts)
+}
diff --git a/clustertool/cmd/charts_bump.go b/clustertool/cmd/charts_bump.go
new file mode 100644
index 0000000000000..fcbe987eb61ae
--- /dev/null
+++ b/clustertool/cmd/charts_bump.go
@@ -0,0 +1,23 @@
+package cmd
+
+import (
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/charts/version"
+)
+
+var bumper = &cobra.Command{
+ Use: "bump",
+ Short: "generate a bumped image version",
+ Example: "charttool bump ",
+ Args: cobra.ExactArgs(2),
+ Run: func(cmd *cobra.Command, args []string) {
+ if err := version.Bump(args[0], args[1]); err != nil {
+ log.Fatal().Err(err).Msg("failed to bump version")
+ }
+ },
+}
+
+func init() {
+ charts.AddCommand(bumper)
+}
diff --git a/clustertool/cmd/charts_deps.go b/clustertool/cmd/charts_deps.go
new file mode 100644
index 0000000000000..81faa9472fc99
--- /dev/null
+++ b/clustertool/cmd/charts_deps.go
@@ -0,0 +1,29 @@
+package cmd
+
+import (
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/charts/deps"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+var depsCmd = &cobra.Command{
+ Use: "deps",
+ Short: "Download, Update and Verify Helm dependencies",
+ Example: "charttool deps ",
+ Run: func(cmd *cobra.Command, args []string) {
+ if err := deps.LoadGPGKey(); err != nil {
+ log.Fatal().Err(err).Msg("failed to load gpg key")
+ }
+
+ // Specify the mode (SyncMode or AsyncMode)
+ mode := helper.SyncMode // Change to helper.SyncMode for synchronous processing
+ if err := helper.WalkCharts(args, deps.DownloadDeps, "", mode); err != nil {
+ log.Fatal().Err(err).Msg("failed to update Chart.yaml")
+ }
+ },
+}
+
+func init() {
+ charts.AddCommand(depsCmd)
+}
diff --git a/clustertool/cmd/charts_genchangelog.go b/clustertool/cmd/charts_genchangelog.go
new file mode 100644
index 0000000000000..2de816597c15e
--- /dev/null
+++ b/clustertool/cmd/charts_genchangelog.go
@@ -0,0 +1,39 @@
+package cmd
+
+import (
+ "github.com/rs/zerolog/log"
+
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/charts/changelog"
+)
+
+var genChangelogCmd = &cobra.Command{
+ Use: "genchangelog",
+ Short: "Generate changelog for charts",
+ Example: "charttool genchangelog ",
+ Run: func(cmd *cobra.Command, args []string) {
+ if len(args) < 3 {
+ log.Fatal().Msg("Missing required arguments. Please provide the repo path, template path and charts directory.")
+ }
+ opts := &changelog.ChangelogOptions{
+ RepoPath: args[0],
+ TemplatePath: args[1],
+ ChartsDir: args[2],
+ ChangelogFileName: "CHANGELOG.md",
+ JSONOutputPath: "./changelog.json",
+ PrettyJSON: true,
+ StatusUpdateInterval: 5,
+ SkipCommitsWithBadMessage: false,
+ }
+ if err := opts.Generate(); err != nil {
+ log.Fatal().Err(err)
+ }
+ if err := opts.Render(); err != nil {
+ log.Fatal().Err(err)
+ }
+ },
+}
+
+func init() {
+ charts.AddCommand(genChangelogCmd)
+}
diff --git a/clustertool/cmd/charts_genchartlist.go b/clustertool/cmd/charts_genchartlist.go
new file mode 100644
index 0000000000000..6cbe61b2e64f5
--- /dev/null
+++ b/clustertool/cmd/charts_genchartlist.go
@@ -0,0 +1,34 @@
+package cmd
+
+import (
+ "github.com/rs/zerolog/log"
+
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/charts/website"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+var genChartListCmd = &cobra.Command{
+ Use: "genchartlist",
+ Short: "Generate chart list json file",
+ Example: "charttool genchartlist ",
+ Run: func(cmd *cobra.Command, args []string) {
+ opts := &website.ChartListOptions{
+ OutputPath: "./charts.json",
+ TrainFilter: []string{}, // We can filter by train later if needed
+ }
+
+ if err := helper.WalkCharts2(args, opts.GetChartData, helper.AsyncMode); err != nil {
+ log.Fatal().Err(err).Msg("failed to generate chart list json file:")
+ }
+
+ if err := opts.WriteChartList(); err != nil {
+ log.Fatal().Err(err).Msg("failed to write chart list json file:")
+ }
+
+ },
+}
+
+func init() {
+ charts.AddCommand(genChartListCmd)
+}
diff --git a/clustertool/cmd/charts_genmeta.go b/clustertool/cmd/charts_genmeta.go
new file mode 100644
index 0000000000000..b2fd413bd2af7
--- /dev/null
+++ b/clustertool/cmd/charts_genmeta.go
@@ -0,0 +1,36 @@
+package cmd
+
+import (
+ "github.com/rs/zerolog/log"
+
+ "slices"
+
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/charts/chartFile"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+var genMetaCmd = &cobra.Command{
+ Use: "genmeta",
+ Short: "Generate and update Chart.yaml metadata",
+ Run: func(cmd *cobra.Command, args []string) {
+ bump := ""
+ if len(args) > 0 && slices.Contains([]string{"patch", "minor", "major"}, args[0]) {
+ bump = args[0]
+ args = args[1:]
+ }
+
+ // Specify the mode (SyncMode or AsyncMode)
+ // Async Mode showed concurrency issues (Stavros)
+ mode := helper.SyncMode
+
+ err := helper.WalkCharts(args, chartFile.UpdateChartFile, bump, mode)
+ if err != nil {
+ log.Fatal().Err(err).Msg("failed to update Chart.yaml:")
+ }
+ },
+}
+
+func init() {
+ charts.AddCommand(genMetaCmd)
+}
diff --git a/clustertool/cmd/charts_tagclean.go b/clustertool/cmd/charts_tagclean.go
new file mode 100644
index 0000000000000..f9cb4dbc87b24
--- /dev/null
+++ b/clustertool/cmd/charts_tagclean.go
@@ -0,0 +1,25 @@
+package cmd
+
+import (
+ "github.com/rs/zerolog/log"
+
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/charts/image"
+)
+
+var tagCleaner = &cobra.Command{
+ Use: "tagcleaner",
+ Short: "Creates a clean version tag from a container digest",
+ Example: "charttool tagcleaner ",
+ Args: cobra.ExactArgs(1),
+ Run: func(cmd *cobra.Command, args []string) {
+ err := image.Clean(args[0])
+ if err != nil {
+ log.Fatal().Err(err).Msg("failed to clean tag")
+ }
+ },
+}
+
+func init() {
+ charts.AddCommand(tagCleaner)
+}
diff --git a/clustertool/cmd/checkcrypt.go b/clustertool/cmd/checkcrypt.go
new file mode 100644
index 0000000000000..d97fe6266bff6
--- /dev/null
+++ b/clustertool/cmd/checkcrypt.go
@@ -0,0 +1,24 @@
+package cmd
+
+import (
+ "os"
+
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/sops"
+)
+
+var checkcrypt = &cobra.Command{
+ Use: "checkcrypt",
+ Short: "Checks if all files are encrypted correctly in accordance with .sops.yaml",
+ Run: func(cmd *cobra.Command, args []string) {
+ if err := sops.CheckFilesAndReportEncryption(false, false); err != nil {
+ log.Info().Msgf("Error checking files: %v\n", err)
+ os.Exit(1)
+ }
+ },
+}
+
+func init() {
+ RootCmd.AddCommand(checkcrypt)
+}
diff --git a/clustertool/cmd/decrypt.go b/clustertool/cmd/decrypt.go
new file mode 100644
index 0000000000000..269220b3ee70c
--- /dev/null
+++ b/clustertool/cmd/decrypt.go
@@ -0,0 +1,21 @@
+package cmd
+
+import (
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/sops"
+)
+
+var decrypt = &cobra.Command{
+ Use: "decrypt",
+ Short: "Decrypt all high-risk data using sops",
+ Run: func(cmd *cobra.Command, args []string) {
+ if err := sops.DecryptFiles(); err != nil {
+ log.Info().Msgf("Error decrypting files: %v\n", err)
+ }
+ },
+}
+
+func init() {
+ RootCmd.AddCommand(decrypt)
+}
diff --git a/clustertool/cmd/encrypt.go b/clustertool/cmd/encrypt.go
new file mode 100644
index 0000000000000..1d13719a4e8cf
--- /dev/null
+++ b/clustertool/cmd/encrypt.go
@@ -0,0 +1,21 @@
+package cmd
+
+import (
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/sops"
+)
+
+var encrypt = &cobra.Command{
+ Use: "encrypt",
+ Short: "Encrypt all high-risk data using sops",
+ Run: func(cmd *cobra.Command, args []string) {
+ if err := sops.EncryptAllFiles(); err != nil {
+ log.Info().Msgf("Error encrypting files: %v\n", err)
+ }
+ },
+}
+
+func init() {
+ RootCmd.AddCommand(encrypt)
+}
diff --git a/clustertool/cmd/fluxbootstrap.go b/clustertool/cmd/fluxbootstrap.go
new file mode 100644
index 0000000000000..682e63877b0ed
--- /dev/null
+++ b/clustertool/cmd/fluxbootstrap.go
@@ -0,0 +1,14 @@
+package cmd
+
+import (
+ "github.com/spf13/cobra"
+)
+
+var fluxBootstrap = &cobra.Command{
+ Use: "fluxBootstrap",
+ Short: "bootstrapFluxCD",
+}
+
+func init() {
+ RootCmd.AddCommand(fluxBootstrap)
+}
diff --git a/clustertool/cmd/genconfig.go b/clustertool/cmd/genconfig.go
new file mode 100644
index 0000000000000..60f4193c6d129
--- /dev/null
+++ b/clustertool/cmd/genconfig.go
@@ -0,0 +1,47 @@
+package cmd
+
+import (
+ "strings"
+
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/gencmd"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+ "github.com/truecharts/private/clustertool/pkg/nodestatus"
+ "github.com/truecharts/private/clustertool/pkg/sops"
+)
+
+var genConfigLongHelp = strings.TrimSpace(`
+ClusterTool after all your settings are entered into talconfig.yaml and talenv.yaml, Clustertool generates a complete clusterconfiguration using TalHelper and various other tools.
+
+Its important to note, that running clustertool genconfig, again after each settings change, is absolutely imperative to be able to deploy said settings to your cluster.
+
+Powered by TalHelper (https://budimanjojo.github.io/talhelper/)
+
+`)
+
+var genConfig = &cobra.Command{
+ Use: "genconfig",
+ Short: "generate Configuration files",
+ Long: genConfigLongHelp,
+ Example: "clustertool genconfig",
+ Run: func(cmd *cobra.Command, args []string) {
+ if err := sops.DecryptFiles(); err != nil {
+ log.Info().Msgf("Error decrypting files: %v\n", err)
+ }
+
+ gencmd.GenConfig(args)
+ err := nodestatus.CheckHealth(helper.TalEnv["VIP_IP"], "", true)
+ if err == nil {
+ log.Info().Msg("Running Cluster Detected, setting KubeConfig...")
+ kubeconfigcmds := gencmd.GenKubeConfig(helper.TalEnv["VIP_IP"])
+ gencmd.ExecCmd(kubeconfigcmds)
+ } else {
+ log.Info().Msg("No Running Cluster Detected, Skipping KubeConfig...")
+ }
+ },
+}
+
+func init() {
+ RootCmd.AddCommand(genConfig)
+}
diff --git a/clustertool/cmd/helmrelease.go b/clustertool/cmd/helmrelease.go
new file mode 100644
index 0000000000000..6f1318f21765d
--- /dev/null
+++ b/clustertool/cmd/helmrelease.go
@@ -0,0 +1,24 @@
+package cmd
+
+import (
+ "strings"
+
+ "github.com/spf13/cobra"
+)
+
+var helmreleaseHelp = strings.TrimSpace(`
+A toolkit to load helm-release files onto a cluster without flux
+
+`)
+
+var helmrelease = &cobra.Command{
+ Use: "helmrelease",
+ Short: "A toolkit to load helm-release files onto a cluster without flux",
+ Long: advLongHelp,
+ SilenceUsage: true,
+ SilenceErrors: true,
+}
+
+func init() {
+ RootCmd.AddCommand(helmrelease)
+}
diff --git a/clustertool/cmd/helmrelease_install.go b/clustertool/cmd/helmrelease_install.go
new file mode 100644
index 0000000000000..e780198067fe6
--- /dev/null
+++ b/clustertool/cmd/helmrelease_install.go
@@ -0,0 +1,37 @@
+package cmd
+
+import (
+ "path/filepath"
+
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/fluxhandler"
+ "github.com/truecharts/private/clustertool/pkg/initfiles"
+)
+
+var hrinstall = &cobra.Command{
+ Use: "install",
+ Short: "install a helm-release file without flux, helm-release file needs to be called helm-release.yaml",
+ Run: func(cmd *cobra.Command, args []string) {
+ initfiles.LoadTalEnv()
+
+ var dir string
+
+ // Check if args[0] includes a filename
+ if filename := filepath.Base(args[0]); filename != "" && filename != "." && filename != "/" {
+ dir = filepath.Dir(args[0])
+ } else {
+ dir = args[0] // Assuming args[0] is just a directory path without a filename
+ }
+ helmRepoPath := filepath.Join("./repositories", "helm")
+ helmRepos, _ := fluxhandler.LoadAllHelmRepos(helmRepoPath)
+ intermediateCharts := []fluxhandler.HelmChart{
+ {dir, false, true},
+ }
+
+ fluxhandler.InstallCharts(intermediateCharts, helmRepos, false)
+ },
+}
+
+func init() {
+ helmrelease.AddCommand(hrinstall)
+}
diff --git a/clustertool/cmd/helmrelease_upgrade.go b/clustertool/cmd/helmrelease_upgrade.go
new file mode 100644
index 0000000000000..3ffeeb4dd1f79
--- /dev/null
+++ b/clustertool/cmd/helmrelease_upgrade.go
@@ -0,0 +1,37 @@
+package cmd
+
+import (
+ "path/filepath"
+
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/fluxhandler"
+ "github.com/truecharts/private/clustertool/pkg/initfiles"
+)
+
+var hrupgrade = &cobra.Command{
+ Use: "upgrade",
+ Short: "run helm-upgrade using a helm-release file without flux",
+ Run: func(cmd *cobra.Command, args []string) {
+ initfiles.LoadTalEnv()
+
+ var dir string
+
+ // Check if args[0] includes a filename
+ if filename := filepath.Base(args[0]); filename != "" && filename != "." && filename != "/" {
+ dir = filepath.Dir(args[0])
+ } else {
+ dir = args[0] // Assuming args[0] is just a directory path without a filename
+ }
+ helmRepoPath := filepath.Join("./repositories", "helm")
+ helmRepos, _ := fluxhandler.LoadAllHelmRepos(helmRepoPath)
+ intermediateCharts := []fluxhandler.HelmChart{
+ {dir, false, true},
+ }
+
+ fluxhandler.UpgradeCharts(intermediateCharts, helmRepos, false)
+ },
+}
+
+func init() {
+ helmrelease.AddCommand(hrupgrade)
+}
diff --git a/clustertool/cmd/info.go b/clustertool/cmd/info.go
new file mode 100644
index 0000000000000..359cad02fc359
--- /dev/null
+++ b/clustertool/cmd/info.go
@@ -0,0 +1,19 @@
+package cmd
+
+import (
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/info"
+)
+
+var infoCmd = &cobra.Command{
+ Use: "info",
+ Short: "Prints information about the clustertool binary",
+ Example: "clustertool info",
+ Run: func(cmd *cobra.Command, args []string) {
+ info.NewInfo().Print()
+ },
+}
+
+func init() {
+ RootCmd.AddCommand(infoCmd)
+}
diff --git a/clustertool/cmd/init.go b/clustertool/cmd/init.go
new file mode 100644
index 0000000000000..aff275cc2a1f9
--- /dev/null
+++ b/clustertool/cmd/init.go
@@ -0,0 +1,38 @@
+package cmd
+
+import (
+ "strings"
+
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/initfiles"
+ "github.com/truecharts/private/clustertool/pkg/sops"
+)
+
+var initLongHelp = strings.TrimSpace(`
+ClusterTool requires a specific directory layout to ensure smooth operators and standardised environments.
+
+To ensure smooth deployment, the init function can pre-generate all required files in the right places.
+Afterwards you can edit talconfig.yaml and talenv.yaml to reflect your personal settings.
+
+When done, please run clustertool genconfig to generate all configarion based on your personal settings
+
+Powered by TalHelper (https://budimanjojo.github.io/talhelper/)
+
+`)
+
+var initFiles = &cobra.Command{
+ Use: "init",
+ Short: "generate Basic ClusterTool file-and-folder structure in current folder",
+ Long: initLongHelp,
+ Example: "clustertool init",
+ Run: func(cmd *cobra.Command, args []string) {
+
+ sops.DecryptFiles()
+
+ initfiles.InitFiles()
+ },
+}
+
+func init() {
+ RootCmd.AddCommand(initFiles)
+}
diff --git a/clustertool/cmd/root.go b/clustertool/cmd/root.go
new file mode 100644
index 0000000000000..6b52429e048b9
--- /dev/null
+++ b/clustertool/cmd/root.go
@@ -0,0 +1,62 @@
+package cmd
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+var thisversion string
+
+var rootLongHelp = strings.TrimSpace(`
+clustertool is a tool to help you easily deploy and maintain a Talos Kubernetes Cluster.
+
+
+Workflow:
+ Create talconfig.yaml file defining your nodes information like so:
+
+ Available commands
+ > clustertool init
+ > clustertool genconfig
+
+ Powered by TalHelper (https://budimanjojo.github.io/talhelper/)
+
+`)
+
+var RootCmd = &cobra.Command{
+ Use: "clustertool",
+ Short: "A tool to help with creating Talos cluster",
+ Long: rootLongHelp,
+ SilenceUsage: true,
+ SilenceErrors: true,
+ Version: thisversion,
+}
+
+func init() {
+ // Define the --cluster flag
+ RootCmd.PersistentFlags().StringVar(&helper.ClusterName, "cluster", "main", "Cluster name")
+}
+
+func Execute() error {
+ // Parse only the persistent flags (like --cluster) before executing any command
+ RootCmd.PersistentFlags().Parse(os.Args[1:])
+
+ // You can now access the helper.ClusterName variable
+ if helper.ClusterName != "" {
+ log.Info().Msgf("Cluster name: %s\n", helper.ClusterName)
+ helper.ClusterPath = filepath.Join("./clusters", helper.ClusterName)
+ helper.ClusterEnvFile = filepath.Join(helper.ClusterPath, "/clusterenv.yaml")
+ helper.TalConfigFile = filepath.Join(helper.ClusterPath, "/talos", "talconfig.yaml")
+ helper.TalosPath = filepath.Join(helper.ClusterPath, "/talos")
+ helper.TalosGenerated = filepath.Join(helper.TalosPath, "/generated")
+ helper.TalosConfigFile = filepath.Join(helper.TalosGenerated, "talosconfig")
+ helper.TalSecretFile = filepath.Join(helper.TalosGenerated, "talsecret.yaml")
+ }
+
+ // Execute the root command and all subcommands
+ return RootCmd.Execute()
+}
diff --git a/clustertool/cmd/upgrade.go b/clustertool/cmd/upgrade.go
new file mode 100644
index 0000000000000..84bf58383d700
--- /dev/null
+++ b/clustertool/cmd/upgrade.go
@@ -0,0 +1,50 @@
+package cmd
+
+import (
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+ "github.com/truecharts/private/clustertool/pkg/gencmd"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+ "github.com/truecharts/private/clustertool/pkg/sops"
+)
+
+var upgrade = &cobra.Command{
+ Use: "upgrade",
+ Short: "Upgrade Talos Nodes and Kubernetes",
+ Run: func(cmd *cobra.Command, args []string) {
+ var extraArgs []string
+ node := ""
+
+ if len(args) > 1 {
+ extraArgs = args[1:]
+ }
+ if len(args) >= 1 {
+ node = args[0]
+ if args[0] == "all" {
+ node = ""
+ }
+ }
+
+ if err := sops.DecryptFiles(); err != nil {
+ log.Info().Msgf("Error decrypting files: %v\n", err)
+ }
+
+ log.Info().Msg("Running Cluster Upgrade")
+
+ taloscmds := gencmd.GenUpgrade(node, extraArgs)
+ gencmd.ExecCmds(taloscmds, true)
+
+ log.Info().Msg("Running Kubernetes Upgrade")
+ kubeUpgradeCmd := gencmd.GenKubeUpgrade(helper.TalEnv["VIP_IP"])
+ gencmd.ExecCmd(kubeUpgradeCmd)
+
+ log.Info().Msg("(re)Loading KubeConfig)")
+ kubeconfigcmds := gencmd.GenKubeConfig(helper.TalEnv["VIP_IP"])
+ gencmd.ExecCmd(kubeconfigcmds)
+
+ },
+}
+
+func init() {
+ RootCmd.AddCommand(upgrade)
+}
diff --git a/clustertool/cspell.config.yaml b/clustertool/cspell.config.yaml
new file mode 100644
index 0000000000000..88cc64a9cf099
--- /dev/null
+++ b/clustertool/cspell.config.yaml
@@ -0,0 +1,17 @@
+words:
+ - azurecr
+ - certman
+ - CHARTPLACEHOLDER
+ - clustertool-dev
+ - devcontainer
+ - helmignore
+ - keybase
+ - koanf
+ - lscr
+ - Msgf
+ - ocir
+ - pubring
+ - tccr
+ - TRAINPLACEHOLDER
+ - truecharts
+ - unmarshalling
diff --git a/clustertool/embed/darwin_amd64/talosctl-darwin-amd64 b/clustertool/embed/darwin_amd64/talosctl-darwin-amd64
new file mode 100644
index 0000000000000..c9d8d098ff9b8
Binary files /dev/null and b/clustertool/embed/darwin_amd64/talosctl-darwin-amd64 differ
diff --git a/clustertool/embed/darwin_arm64/talosctl-darwin-arm64 b/clustertool/embed/darwin_arm64/talosctl-darwin-arm64
new file mode 100644
index 0000000000000..f388a25a951f8
Binary files /dev/null and b/clustertool/embed/darwin_arm64/talosctl-darwin-arm64 differ
diff --git a/clustertool/embed/download_talosctl.sh b/clustertool/embed/download_talosctl.sh
new file mode 100755
index 0000000000000..3a2bd0e5f8099
--- /dev/null
+++ b/clustertool/embed/download_talosctl.sh
@@ -0,0 +1,65 @@
+#!/bin/bash
+
+# Ensure the script exits on errors and undefined variables
+set -euo pipefail
+
+# Define the version
+version="v1.8.0" # Example version
+
+# Define the OS and architecture combinations
+combinations=(
+ "linux amd64"
+ "linux arm64"
+ "darwin amd64"
+ "darwin arm64"
+ "windows amd64"
+ "freebsd amd64"
+ "freebsd arm64"
+)
+
+# Base URL for downloading the file
+base_url="https://github.com/siderolabs/talos/releases/download/${version}"
+
+# Iterate over each combination
+for combo in "${combinations[@]}"; do
+ # Split the combination into OS and architecture
+ os=$(echo "$combo" | cut -d ' ' -f 1)
+ arch=$(echo "$combo" | cut -d ' ' -f 2)
+
+ # Determine the file name and download URL based on OS and architecture
+ if [ "$os" == "windows" ]; then
+ file_name="talosctl-${os}-${arch}.exe"
+ file_extension="exe"
+ elif [ "$os" == "darwin" ] || [ "$os" == "freebsd" ]; then
+ file_name="talosctl-${os}-${arch}"
+ file_extension="bin"
+ else
+ file_name="talosctl-${os}-${arch}"
+ file_extension="bin"
+ fi
+
+ # Construct the download URL and target directory
+ download_url="${base_url}/${file_name}"
+ target_dir="./clustertool/embed/${os}_${arch}"
+
+ # Create target directory if it doesn't exist
+ mkdir -p "${target_dir}"
+
+ # Download the file
+ echo "Downloading ${download_url}..."
+ curl -L -o "${file_name}" "${download_url}"
+
+ # Handle different file types
+ if [ "$file_extension" == "exe" ]; then
+ # For Windows executables, just move the file to the target directory
+ echo "Moving ${file_name} to ${target_dir}..."
+ mv "${file_name}" "${target_dir}/"
+ elif [ "$os" == "linux" ] || [ "$os" == "darwin" ] || [ "$os" == "freebsd" ]; then
+ # For Linux, Darwin, and FreeBSD binaries
+ echo "Moving ${file_name} to ${target_dir}..."
+ mv "${file_name}" "${target_dir}/"
+ fi
+
+ # Print success message
+ echo "Talosctl ${version} for ${os}-${arch} has been downloaded and moved to ${target_dir}"
+done
diff --git a/clustertool/embed/embed.go b/clustertool/embed/embed.go
new file mode 100644
index 0000000000000..2e92c16028efa
--- /dev/null
+++ b/clustertool/embed/embed.go
@@ -0,0 +1,107 @@
+package embed
+
+import (
+ "embed"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "runtime"
+
+ "github.com/rs/zerolog/log"
+
+ "github.com/leaanthony/debme"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+//go:embed generic/*
+var GenericFiles embed.FS
+var TalosExec string
+
+func AllToCache() {
+ err := os.RemoveAll(helper.CacheDir)
+ if err != nil {
+ log.Fatal().Err(err)
+ }
+ GOOSARCH := runtime.GOOS + "_" + runtime.GOARCH
+ filesToCache(StaticFiles, GOOSARCH)
+ filesToCache(GenericFiles, "generic")
+}
+
+func filesToCache(embededfs embed.FS, sub string) {
+
+ // Ensure the base cache directory exists
+ if err := os.MkdirAll(helper.CacheDir, os.ModePerm); err != nil {
+ log.Info().Msgf("Error creating base cache directory: %v", err)
+ return
+ }
+
+ root, _ := debme.FS(embededfs, sub)
+ fs.WalkDir(root, ".", func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if d.Name() != sub {
+ if d.IsDir() {
+ // If it's a directory, create the corresponding directory in the cache
+ writePath := filepath.Join(helper.CacheDir, path)
+ if err := os.MkdirAll(writePath, os.ModePerm); err != nil {
+ log.Info().Msgf("Error creating directory in cache: %v", err)
+ return err
+ }
+ } else {
+
+ // If it's a file, read and write it to the cache
+ data, err := root.ReadFile(path)
+ if err != nil {
+ log.Info().Msgf("Error reading file: %v", err)
+ return err
+ }
+ writePath := filepath.Join(helper.CacheDir, path)
+ if err := os.WriteFile(writePath, data, 0755); err != nil {
+ log.Info().Msgf("Error writing file to cache: %v", err)
+ return err
+ }
+ }
+ }
+ return nil
+ })
+}
+
+func GetTalosExec() string {
+ execName := ""
+ if runtime.GOOS == "windows" {
+ if runtime.GOARCH == "amd64" {
+ execName = "talosctl-windows-amd64.exe"
+ } else {
+ execName = "talosctl-windows-arm64.exe"
+ }
+
+ }
+ if runtime.GOOS == "linux" {
+ if runtime.GOARCH == "amd64" {
+ execName = "talosctl-linux-amd64"
+ } else {
+ execName = "talosctl-linux-arm64"
+ }
+
+ }
+ if runtime.GOOS == "darwin" {
+ if runtime.GOARCH == "amd64" {
+ execName = "talosctl-darwin-amd64"
+ } else {
+ execName = "talosctl-darwin-arm64"
+ }
+
+ }
+ if runtime.GOOS == "freebsd" {
+ if runtime.GOARCH == "amd64" {
+ execName = "talosctl-freebsd-amd64"
+ } else {
+ execName = "talosctl-freebsd-arm64"
+ }
+
+ }
+
+ return filepath.Join(helper.CacheDir, execName)
+
+}
diff --git a/clustertool/embed/embedDarwin_amd64.go b/clustertool/embed/embedDarwin_amd64.go
new file mode 100644
index 0000000000000..5ce9c4e76bb0d
--- /dev/null
+++ b/clustertool/embed/embedDarwin_amd64.go
@@ -0,0 +1,11 @@
+//go:build freebsd && amd64
+// +build freebsd,amd64
+
+package embed
+
+import (
+ "embed"
+)
+
+//go:embed freebsd_amd64
+var StaticFiles embed.FS
diff --git a/clustertool/embed/embedDarwin_arm64.go b/clustertool/embed/embedDarwin_arm64.go
new file mode 100644
index 0000000000000..e94cf00bad34e
--- /dev/null
+++ b/clustertool/embed/embedDarwin_arm64.go
@@ -0,0 +1,11 @@
+//go:build darwin && arm64
+// +build darwin,arm64
+
+package embed
+
+import (
+ "embed"
+)
+
+//go:embed darwin_arm64
+var StaticFiles embed.FS
diff --git a/clustertool/embed/embedLinux_amd64.go b/clustertool/embed/embedLinux_amd64.go
new file mode 100644
index 0000000000000..f5aa2619f6229
--- /dev/null
+++ b/clustertool/embed/embedLinux_amd64.go
@@ -0,0 +1,11 @@
+//go:build linux && amd64
+// +build linux,amd64
+
+package embed
+
+import (
+ "embed"
+)
+
+//go:embed linux_amd64
+var StaticFiles embed.FS
diff --git a/clustertool/embed/embedLinux_arm64.go b/clustertool/embed/embedLinux_arm64.go
new file mode 100644
index 0000000000000..1d4a8fe61a0cf
--- /dev/null
+++ b/clustertool/embed/embedLinux_arm64.go
@@ -0,0 +1,11 @@
+//go:build linux && arm64
+// +build linux,arm64
+
+package embed
+
+import (
+ "embed"
+)
+
+//go:embed linux_arm64
+var StaticFiles embed.FS
diff --git a/clustertool/embed/embedWindows_amd64.go b/clustertool/embed/embedWindows_amd64.go
new file mode 100644
index 0000000000000..da8411052d487
--- /dev/null
+++ b/clustertool/embed/embedWindows_amd64.go
@@ -0,0 +1,11 @@
+//go:build windows && amd64
+// +build windows,amd64
+
+package embed
+
+import (
+ "embed"
+)
+
+//go:embed windows_amd64/*
+var StaticFiles embed.FS
diff --git a/clustertool/embed/embedWindows_arm64.go b/clustertool/embed/embedWindows_arm64.go
new file mode 100644
index 0000000000000..ed3eff9566e32
--- /dev/null
+++ b/clustertool/embed/embedWindows_arm64.go
@@ -0,0 +1,11 @@
+//go:build windows && arm64
+// +build windows,arm64
+
+package embed
+
+import (
+ "embed"
+)
+
+//go:embed windows_arm64/*
+var StaticFiles embed.FS
diff --git a/clustertool/embed/freebsd_amd64.go b/clustertool/embed/freebsd_amd64.go
new file mode 100644
index 0000000000000..8a43473dde18b
--- /dev/null
+++ b/clustertool/embed/freebsd_amd64.go
@@ -0,0 +1,11 @@
+//go:build darwin && amd64
+// +build darwin,amd64
+
+package embed
+
+import (
+ "embed"
+)
+
+//go:embed darwin_amd64
+var StaticFiles embed.FS
diff --git a/clustertool/embed/freebsd_amd64/talosctl-freebsd-amd64 b/clustertool/embed/freebsd_amd64/talosctl-freebsd-amd64
new file mode 100644
index 0000000000000..6bdf27970ada1
Binary files /dev/null and b/clustertool/embed/freebsd_amd64/talosctl-freebsd-amd64 differ
diff --git a/clustertool/embed/freebsd_arm64.go b/clustertool/embed/freebsd_arm64.go
new file mode 100644
index 0000000000000..87b16c114adec
--- /dev/null
+++ b/clustertool/embed/freebsd_arm64.go
@@ -0,0 +1,11 @@
+//go:build freebsd && arm64
+// +build freebsd,arm64
+
+package embed
+
+import (
+ "embed"
+)
+
+//go:embed freebsd_arm64
+var StaticFiles embed.FS
diff --git a/clustertool/embed/freebsd_arm64/talosctl-freebsd-arm64 b/clustertool/embed/freebsd_arm64/talosctl-freebsd-arm64
new file mode 100644
index 0000000000000..c9d789acb884a
Binary files /dev/null and b/clustertool/embed/freebsd_arm64/talosctl-freebsd-arm64 differ
diff --git a/clustertool/embed/generic/base/DOTREPLACEgitignore b/clustertool/embed/generic/base/DOTREPLACEgitignore
new file mode 100644
index 0000000000000..2f2460c890cef
--- /dev/null
+++ b/clustertool/embed/generic/base/DOTREPLACEgitignore
@@ -0,0 +1,9 @@
+talconfig.json
+clusterconfig
+patches/sopssecret.yaml
+cluster/main/kubernetes/**/bootstrap-values.yaml.ct
+*kubeconfig
+
+
+
+
diff --git a/clustertool/embed/generic/base/clusterenv.yaml b/clustertool/embed/generic/base/clusterenv.yaml
new file mode 100644
index 0000000000000..c5e138dfc1dbe
--- /dev/null
+++ b/clustertool/embed/generic/base/clusterenv.yaml
@@ -0,0 +1,17 @@
+## The Following are required by ClusterTool and CANNOT be removed
+# Ensure VIP is different from all master IPs
+VIP: 192.168.20.200
+# Defines the MasterNode IP
+MASTER1IP: 192.168.20.210
+# Defines the gateway for all nodes
+GATEWAY: 192.168.20.1
+# Defines the ip range metallb is allowed to use
+METALLB_RANGE: 192.168.20.211-192.168.20.219
+# Sets the Kubernetes Dashboard IP. Has to be within METALLB_RANGE and not in use
+DASHBOARD_IP: 192.168.20.211
+# Used to automatically generate a sshkey-pair for FluxCD
+# Has to start with ssh://
+GITHUB_REPOSITORY: ""
+# DO NOT ALTER
+PODNET: 172.16.0.0/16
+SVCNET: 172.17.0.0/16
diff --git a/clustertool/embed/generic/base/talos/talconfig.yaml b/clustertool/embed/generic/base/talos/talconfig.yaml
new file mode 100644
index 0000000000000..498cb276727d8
--- /dev/null
+++ b/clustertool/embed/generic/base/talos/talconfig.yaml
@@ -0,0 +1,82 @@
+clusterName: ${CLUSTERNAME}
+# renovate: datasource=docker depName=ghcr.io/siderolabs/installer
+talosVersion: v1.8.1
+# renovate: datasource=docker depName=ghcr.io/siderolabs/kubelet
+kubernetesVersion: v1.31.1
+endpoint: https://${VIP}:6443
+allowSchedulingOnControlPlanes: true
+additionalMachineCertSans:
+ - ${VIP}
+additionalApiServerCertSans:
+ - ${VIP}
+# Warning: Also used in Cilium CNI values!
+clusterPodNets:
+ - ${PODNET}
+clusterSvcNets:
+ - ${SVCNET}
+cniConfig:
+ name: none
+patches:
+ - '@./patches/all.yaml'
+nodes:
+ # We would adivce to always stick to a "k8s-something-1" style naming scheme
+ - hostname: k8s-control-1
+ ipAddress: ${MASTER1IP_IP}
+ controlPlane: true
+ nameservers:
+ - 1.1.1.1
+ - 8.8.8.8
+ installDiskSelector:
+ size: <= 1600GB
+ networkInterfaces:
+ # suffix is the adapter mac adres.
+ - interface: eth0
+ addresses:
+ - ${MASTER1IP_CIDR}
+ routes:
+ - network: 0.0.0.0/0
+ gateway: ${GATEWAY}
+ vip:
+ ip: ${VIP}
+controlPlane:
+ patches:
+ - '@./patches/controlplane.yaml'
+ - '@./patches/sopssecret.yaml'
+ - '@./patches/manifests.yaml'
+ # - '@./patches/nvidia.yaml'
+ schematic:
+ customization:
+ extraKernelArgs:
+ - net.ifnames=0
+ systemExtensions:
+ officialExtensions:
+ - siderolabs/util-linux-tools
+ - siderolabs/iscsi-tools
+ - siderolabs/qemu-guest-agent
+ # Enable where needed
+ # - siderolabs/amd-ucode
+ # - siderolabs/bnx2-bnx2x
+ # - siderolabs/drbd
+ # - siderolabs/gasket-driver
+ # - siderolabs/i915-ucode
+ # - siderolabs/intel-ucode
+ # - siderolabs/thunderbolt
+worker:
+ patches:
+ - '@./patches/worker.yaml'
+ # - '@./patches/nvidia.yaml'
+ schematic:
+ customization:
+ systemExtensions:
+ officialExtensions:
+ - siderolabs/util-linux-tools
+ - siderolabs/iscsi-tools
+ - siderolabs/qemu-guest-agent
+ # Enable where needed
+ # - siderolabs/amd-ucode
+ # - siderolabs/bnx2-bnx2x
+ # - siderolabs/drbd
+ # - siderolabs/gasket-driver
+ # - siderolabs/i915-ucode
+ # - siderolabs/intel-ucode
+ # - siderolabs/thunderbolt
diff --git a/clustertool/embed/generic/kubernetes/apps/kubernetes-dashboard/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/apps/kubernetes-dashboard/app/helm-release.yaml
new file mode 100644
index 0000000000000..8cdb2fffb17c6
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/apps/kubernetes-dashboard/app/helm-release.yaml
@@ -0,0 +1,37 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: kubernetes-dashboard
+ namespace: kubernetes-dashboard
+spec:
+ interval: 15m
+ chart:
+ spec:
+ chart: kubernetes-dashboard
+ version: 1.8.0
+ sourceRef:
+ kind: HelmRepository
+ name: truecharts
+ namespace: flux-system
+ interval: 15m
+ timeout: 20m
+ maxHistory: 3
+ install:
+ createNamespace: true
+ remediation:
+ retries: 3
+ upgrade:
+ cleanupOnFail: true
+ remediation:
+ retries: 3
+ uninstall:
+ keepHistory: false
+ values:
+ service:
+ main:
+ type: LoadBalancer
+ loadBalancerIP: ${DASHBOARD_IP}
+ ports:
+ main:
+ port: 80
diff --git a/clustertool/embed/generic/kubernetes/apps/kubernetes-dashboard/app/namespace.yaml b/clustertool/embed/generic/kubernetes/apps/kubernetes-dashboard/app/namespace.yaml
new file mode 100644
index 0000000000000..997c30f1e0fb0
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/apps/kubernetes-dashboard/app/namespace.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: kubernetes-dashboard
+ labels:
+ pod-security.kubernetes.io/enforce: privileged
+ topolvm.io/webhook: ignore
diff --git a/clustertool/embed/generic/kubernetes/core/kyverno-policies/app/kustomization.yaml b/clustertool/embed/generic/kubernetes/core/kyverno-policies/app/kustomization.yaml
new file mode 100644
index 0000000000000..ef5ecf242e468
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/core/kyverno-policies/app/kustomization.yaml
@@ -0,0 +1,5 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+ - schematic-to-pod.yaml
diff --git a/clustertool/embed/generic/kubernetes/core/kyverno-policies/app/schematic-to-pod.yaml b/clustertool/embed/generic/kubernetes/core/kyverno-policies/app/schematic-to-pod.yaml
new file mode 100644
index 0000000000000..80bc6af260988
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/core/kyverno-policies/app/schematic-to-pod.yaml
@@ -0,0 +1,38 @@
+---
+apiVersion: kyverno.io/v2beta1
+kind: ClusterPolicy
+metadata:
+ name: mutate-pod-binding
+ annotations:
+ pod-policies.kyverno.io/autogen-controllers: none
+ policies.kyverno.io/title: Mutate Pod Add Schematic
+ policies.kyverno.io/category: Other
+ policies.kyverno.io/subject: Pod
+ kyverno.io/kyverno-version: 1.10.0
+ policies.kyverno.io/minversion: 1.10.0
+ kyverno.io/kubernetes-version: "1.30"
+spec:
+ background: false
+ rules:
+ - name: project-foo
+ match:
+ any:
+ - resources:
+ kinds:
+ - Pod/binding
+ names:
+ - apply-talos*
+ context:
+ - name: node
+ variable:
+ jmesPath: request.object.target.name
+ default: ''
+ - name: schematic
+ apiCall:
+ urlPath: "/api/v1/nodes/{{node}}"
+ jmesPath: "metadata.annotations.\"extensions.talos.dev/schematic\" || 'empty'"
+ mutate:
+ patchStrategicMerge:
+ metadata:
+ annotations:
+ extensions.talos.dev/schematic: "{{ schematic }}"
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/core/kyverno-policies/ks.yaml b/clustertool/embed/generic/kubernetes/core/kyverno-policies/ks.yaml
new file mode 100644
index 0000000000000..018674922cb56
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/core/kyverno-policies/ks.yaml
@@ -0,0 +1,12 @@
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+ name: kyverno-policies
+ namespace: flux-system
+spec:
+ interval: 10m
+ path: clusters/main/kubernetes/core/kyverno-policies/app
+ prune: true
+ sourceRef:
+ kind: GitRepository
+ name: cluster
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/core/metallb-config/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/core/metallb-config/app/helm-release.yaml
new file mode 100644
index 0000000000000..046bd40dc3d32
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/core/metallb-config/app/helm-release.yaml
@@ -0,0 +1,40 @@
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: metallb-config
+ namespace: metallb-config
+spec:
+ interval: 15m
+ chart:
+ spec:
+ chart: metallb-config
+ version: 8.2.1
+ sourceRef:
+ kind: HelmRepository
+ name: truecharts
+ namespace: flux-system
+ interval: 15m
+ timeout: 20m
+ maxHistory: 3
+ install:
+ createNamespace: true
+ remediation:
+ retries: 3
+ upgrade:
+ cleanupOnFail: true
+ remediation:
+ retries: 3
+ uninstall:
+ keepHistory: false
+ values:
+ L2Advertisements:
+ - name: main
+ addressPools:
+ - main
+ ipAddressPools:
+ - name: main
+ autoAssign: false
+ avoidBuggyIPs: true
+ addresses:
+ - ${METALLB_RANGE}
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/core/metallb-config/app/namespace.yaml b/clustertool/embed/generic/kubernetes/core/metallb-config/app/namespace.yaml
new file mode 100644
index 0000000000000..6329a91c4d341
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/core/metallb-config/app/namespace.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: metallb-config
+ labels:
+ pod-security.kubernetes.io/enforce: privileged
+ topolvm.io/webhook: ignore
diff --git a/clustertool/embed/generic/kubernetes/core/system-upgrade-controller-plans/app/kubernetes.yaml b/clustertool/embed/generic/kubernetes/core/system-upgrade-controller-plans/app/kubernetes.yaml
new file mode 100644
index 0000000000000..c8b4aac9bfa21
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/core/system-upgrade-controller-plans/app/kubernetes.yaml
@@ -0,0 +1,51 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/upgrade.cattle.io/plan_v1.json
+apiVersion: upgrade.cattle.io/v1
+kind: Plan
+metadata:
+ name: kubernetes
+spec:
+ version: ${KUBERNETES_VERSION}
+ serviceAccountName: system-upgrade
+ secrets:
+ - name: talos
+ path: /var/run/secrets/talos.dev
+ ignoreUpdates: true
+ concurrency: 1
+ exclusive: true
+ nodeSelector:
+ matchExpressions:
+ - key: feature.node.kubernetes.io/system-os_release.ID
+ operator: In
+ values: ["talos"]
+ - key: node-role.kubernetes.io/control-plane
+ operator: Exists
+ - key: feature.node.kubernetes.io/system-os_release.VERSION_ID
+ operator: In
+ values: ["${TALOS_VERSION}"]
+ - key: kubernetes.io/hostname
+ operator: In
+ values: ["k8s-control-1"]
+ tolerations:
+ - key: CriticalAddonsOnly
+ operator: Exists
+ - key: node-role.kubernetes.io/control-plane
+ operator: Exists
+ effect: NoSchedule
+ prepare: &prepare
+ image: ghcr.io/siderolabs/talosctl:${TALOS_VERSION}
+ envs:
+ - name: NODE_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.hostIP
+ args:
+ - --nodes=$(NODE_IP)
+ - health
+ - --server=false
+ upgrade:
+ <<: *prepare
+ args:
+ - --nodes=$(NODE_IP)
+ - upgrade-k8s
+ - --to=$(SYSTEM_UPGRADE_PLAN_LATEST_VERSION)
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/core/system-upgrade-controller-plans/app/kustomization.yaml b/clustertool/embed/generic/kubernetes/core/system-upgrade-controller-plans/app/kustomization.yaml
new file mode 100644
index 0000000000000..3b2a26e369457
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/core/system-upgrade-controller-plans/app/kustomization.yaml
@@ -0,0 +1,8 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+ # - schematics.yaml
+ - kubernetes.yaml
+ - talos.yaml
diff --git a/clustertool/embed/generic/kubernetes/core/system-upgrade-controller-plans/app/talos.yaml b/clustertool/embed/generic/kubernetes/core/system-upgrade-controller-plans/app/talos.yaml
new file mode 100644
index 0000000000000..c9e0b0aed5d45
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/core/system-upgrade-controller-plans/app/talos.yaml
@@ -0,0 +1,52 @@
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/upgrade.cattle.io/plan_v1.json
+apiVersion: upgrade.cattle.io/v1
+kind: Plan
+metadata:
+ name: talos
+spec:
+ version: ${TALOS_VERSION}
+ serviceAccountName: system-upgrade
+ secrets:
+ - name: talos
+ path: /var/run/secrets/talos.dev
+ ignoreUpdates: true
+ concurrency: 1
+ exclusive: true
+ nodeSelector:
+ matchExpressions:
+ - key: feature.node.kubernetes.io/system-os_release.ID
+ operator: In
+ values: ["talos"]
+ - key: feature.node.kubernetes.io/system-os_release.VERSION_ID
+ operator: NotIn
+ values: ["${TALOS_VERSION}"]
+ tolerations:
+ - key: CriticalAddonsOnly
+ operator: Exists
+ - key: node-role.kubernetes.io/control-plane
+ operator: Exists
+ effect: NoSchedule
+ prepare: &prepare
+ image: ghcr.io/siderolabs/talosctl:${TALOS_VERSION}
+ envs:
+ - name: NODE_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.hostIP
+ - name: SCHEMATIC
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.annotations['extensions.talos.dev/schematic']
+ args:
+ - --nodes=$(NODE_IP)
+ - health
+ - --server=false
+ upgrade:
+ <<: *prepare
+ args:
+ - --nodes=$(NODE_IP)
+ - upgrade
+ - --image=factory.talos.dev/installer/$(SCHEMATIC):$(SYSTEM_UPGRADE_PLAN_LATEST_VERSION)
+ - --preserve=true
+ - --wait=false
+ - --force
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/core/system-upgrade-controller-plans/ks.yaml b/clustertool/embed/generic/kubernetes/core/system-upgrade-controller-plans/ks.yaml
new file mode 100644
index 0000000000000..16b422cab9374
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/core/system-upgrade-controller-plans/ks.yaml
@@ -0,0 +1,19 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+ name: system-upgrade-controller-plans
+ namespace: flux-system
+spec:
+ interval: 10m
+ path: clusters/main/kubernetes/core/system-upgrade-controller-plans/app
+ prune: true
+ sourceRef:
+ kind: GitRepository
+ name: cluster
+ targetNamespace: system-upgrade
+ dependsOn:
+ - name: system-upgrade-controller
+ wait: false
+ retryInterval: 1m
+ timeout: 5m
diff --git a/clustertool/embed/generic/kubernetes/flux-entry.yaml b/clustertool/embed/generic/kubernetes/flux-entry.yaml
new file mode 100644
index 0000000000000..d2fd88aea8e66
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/flux-entry.yaml
@@ -0,0 +1,43 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+ name: flux-entry
+ namespace: flux-system
+spec:
+ interval: 10m
+ path: ./clusters/REPLACEWITHCLUSTERNAME/kubernetes
+ prune: true
+ sourceRef:
+ kind: GitRepository
+ name: cluster
+ decryption:
+ provider: sops
+ secretRef:
+ name: sops-age
+ postBuild:
+ substituteFrom:
+ - kind: ConfigMap
+ name: cluster-config
+ patches:
+ - patch: |-
+ apiVersion: kustomize.toolkit.fluxcd.io/v1
+ kind: Kustomization
+ metadata:
+ name: not-used
+ spec:
+ decryption:
+ provider: sops
+ secretRef:
+ name: sops-age
+ postBuild:
+ substituteFrom:
+ - kind: ConfigMap
+ name: cluster-config
+ - kind: ConfigMap
+ name: upgrade-settings
+ target:
+ group: kustomize.toolkit.fluxcd.io
+ kind: Kustomization
+ labelSelector: substitution.flux.home.arpa/disabled notin (true)
diff --git a/clustertool/embed/generic/kubernetes/flux-system/flux/bootstrap.yaml.ct b/clustertool/embed/generic/kubernetes/flux-system/flux/bootstrap.yaml.ct
new file mode 100644
index 0000000000000..2d0293adcaa42
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/flux-system/flux/bootstrap.yaml.ct
@@ -0,0 +1,62 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+ - github.com/fluxcd/flux2/manifests/install?ref=v2.3.0
+ - ./deploykey.secret.yaml
+ - ./clustersettings.secret.yaml
+patches:
+ # Remove the built-in network policies
+ - target:
+ group: networking.k8s.io
+ kind: NetworkPolicy
+ patch: |
+ $patch: delete
+ apiVersion: networking.k8s.io/v1
+ kind: NetworkPolicy
+ metadata:
+ name: not-used
+ # Resources renamed to match those installed by oci://ghcr.io/fluxcd/flux-manifests
+ - target:
+ kind: ResourceQuota
+ name: critical-pods
+ patch: |
+ - op: replace
+ path: /metadata/name
+ value: critical-pods-flux-system
+ - target:
+ kind: ClusterRoleBinding
+ name: cluster-reconciler
+ patch: |
+ - op: replace
+ path: /metadata/name
+ value: cluster-reconciler-flux-system
+ - target:
+ kind: ClusterRoleBinding
+ name: crd-controller
+ patch: |
+ - op: replace
+ path: /metadata/name
+ value: crd-controller-flux-system
+ - target:
+ kind: ClusterRole
+ name: crd-controller
+ patch: |
+ - op: replace
+ path: /metadata/name
+ value: crd-controller-flux-system
+ - target:
+ kind: ClusterRole
+ name: flux-edit
+ patch: |
+ - op: replace
+ path: /metadata/name
+ value: flux-edit-flux-system
+ - target:
+ kind: ClusterRole
+ name: flux-view
+ patch: |
+ - op: replace
+ path: /metadata/name
+ value: flux-view-flux-system
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/flux-system/flux/clustersettings.secret.yaml b/clustertool/embed/generic/kubernetes/flux-system/flux/clustersettings.secret.yaml
new file mode 100644
index 0000000000000..f6ded4a2329ee
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/flux-system/flux/clustersettings.secret.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: cluster-config
+ namespace: flux-system
+data:
+REPLACEWITHENV
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/flux-system/flux/flux.yaml b/clustertool/embed/generic/kubernetes/flux-system/flux/flux.yaml
new file mode 100644
index 0000000000000..70cbebbd02535
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/flux-system/flux/flux.yaml
@@ -0,0 +1,87 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+ name: flux
+ namespace: flux-system
+spec:
+ interval: 10m
+ path: ./
+ prune: true
+ wait: true
+ sourceRef:
+ kind: OCIRepository
+ name: flux-manifests
+ patches:
+ # Remove the network policies that does not work with k3s
+ - patch: |
+ $patch: delete
+ apiVersion: networking.k8s.io/v1
+ kind: NetworkPolicy
+ metadata:
+ name: not-used
+ target:
+ group: networking.k8s.io
+ kind: NetworkPolicy
+ # Increase the number of reconciliations that can be performed in parallel and bump the resources limits
+ # Ref: https://fluxcd.io/flux/cheatsheets/bootstrap/#increase-the-number-of-workers
+ - patch: |
+ - op: add
+ path: /spec/template/spec/containers/0/args/-
+ value: --concurrent=12
+ - op: add
+ path: /spec/template/spec/containers/0/args/-
+ value: --kube-api-qps=500
+ - op: add
+ path: /spec/template/spec/containers/0/args/-
+ value: --kube-api-burst=1000
+ - op: add
+ path: /spec/template/spec/containers/0/args/-
+ value: --requeue-dependency=5s
+ target:
+ kind: Deployment
+ name: (kustomize-controller|helm-controller|source-controller)
+ - patch: |
+ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ name: not-used
+ spec:
+ template:
+ spec:
+ containers:
+ - name: manager
+ resources:
+ limits:
+ memory: 2Gi
+ target:
+ kind: Deployment
+ name: (kustomize-controller|helm-controller|source-controller)
+ # Enable in-memory-kustomize builds
+ # Ref: https://fluxcd.io/flux/installation/configuration/vertical-scaling/#enable-in-memory-kustomize-builds
+ - patch: |
+ - op: replace
+ path: /spec/template/spec/volumes/0
+ value:
+ name: temp
+ emptyDir:
+ medium: Memory
+ target:
+ kind: Deployment
+ name: kustomize-controller
+ # Enable Helm near OOM detection
+ # Ref: https://fluxcd.io/flux/cheatsheets/bootstrap/#enable-helm-near-oom-detection
+ - patch: |
+ - op: add
+ path: /spec/template/spec/containers/0/args/-
+ value: --feature-gates=OOMWatch=true
+ - op: add
+ path: /spec/template/spec/containers/0/args/-
+ value: --oom-watch-memory-threshold=95
+ - op: add
+ path: /spec/template/spec/containers/0/args/-
+ value: --oom-watch-interval=500ms
+ target:
+ kind: Deployment
+ name: helm-controller
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/flux-system/flux/kustomization.yaml b/clustertool/embed/generic/kubernetes/flux-system/flux/kustomization.yaml
new file mode 100644
index 0000000000000..39c76eeec15d3
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/flux-system/flux/kustomization.yaml
@@ -0,0 +1,10 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+ - ./deploykey.secret.yaml
+ - ./clustersettings.secret.yaml
+ - ./flux.yaml
+ - ./upgradesettings.yaml
+ - ./namespace.yaml
diff --git a/clustertool/embed/generic/kubernetes/flux-system/flux/namespace.yaml b/clustertool/embed/generic/kubernetes/flux-system/flux/namespace.yaml
new file mode 100644
index 0000000000000..d4dc22010b5b0
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/flux-system/flux/namespace.yaml
@@ -0,0 +1,8 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: flux-system
+ labels:
+ pod-security.kubernetes.io/enforce: privileged
+ topolvm.io/webhook: ignore
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/flux-system/flux/upgradesettings.yaml b/clustertool/embed/generic/kubernetes/flux-system/flux/upgradesettings.yaml
new file mode 100644
index 0000000000000..f93c731cb568f
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/flux-system/flux/upgradesettings.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: upgrade-settings
+ namespace: flux-system
+data:
+ # renovate: datasource=docker depName=ghcr.io/siderolabs/installer
+ TALOS_VERSION: v1.8.1
+ # renovate: datasource=docker depName=ghcr.io/siderolabs/kubelet
+ KUBERNETES_VERSION: v1.31.1
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/kube-system/cilium/app/bootstrap-values.yaml.ct b/clustertool/embed/generic/kubernetes/kube-system/cilium/app/bootstrap-values.yaml.ct
new file mode 100644
index 0000000000000..d355e289a3855
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/kube-system/cilium/app/bootstrap-values.yaml.ct
@@ -0,0 +1,3 @@
+## DO NOT ALTER THIS FILE, CHANGE DO NOT PERSIST. Alter TalEnv.yaml instead.
+hubble:
+ enabled: false
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/kube-system/cilium/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/kube-system/cilium/app/helm-release.yaml
new file mode 100644
index 0000000000000..8fe4e5920b270
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/kube-system/cilium/app/helm-release.yaml
@@ -0,0 +1,78 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: cilium
+ namespace: kube-system
+ annotations:
+ meta.helm.sh/release-name: cilium
+ meta.helm.sh/release-namespace: kube-system
+ labels:
+ app.kubernetes.io/managed-by: Helm
+spec:
+ interval: 15m
+ chart:
+ spec:
+ chart: cilium
+ version: 1.16.2
+ sourceRef:
+ kind: HelmRepository
+ name: cilium
+ namespace: flux-system
+ interval: 15m
+ timeout: 20m
+ maxHistory: 3
+ install:
+ remediation:
+ retries: 3
+ upgrade:
+ cleanupOnFail: true
+ remediation:
+ retries: 3
+ remediateLastFailure: true
+ uninstall:
+ keepHistory: false
+ values:
+ # autoDirectNodeRoutes: true
+ # routingMode: native
+ hubble:
+ enabled: false
+ cluster:
+ name: ${CLUSTERNAME}
+ id: 1
+ ipv4NativeRoutingCIDR: ${PODNET}
+ securityContext:
+ privileged: true
+ capabilities:
+ ciliumAgent:
+ - CHOWN
+ - KILL
+ - NET_ADMIN
+ - NET_RAW
+ - IPC_LOCK
+ - SYS_ADMIN
+ - SYS_RESOURCE
+ - DAC_OVERRIDE
+ - FOWNER
+ - SETGID
+ - SETUID
+ cleanCiliumState:
+ - NET_ADMIN
+ - SYS_ADMIN
+ - SYS_RESOURCE
+ cgroup:
+ automount:
+ enabled: false
+ hostRoot: /sys/fs/cgroup
+ enableRuntimeDeviceDetection: true
+ endpointRoutes:
+ enabled: true
+ ipam:
+ mode: kubernetes
+ k8sServiceHost: 127.0.0.1
+ k8sServicePort: 7445
+ kubeProxyReplacement: true
+ kubeProxyReplacementHealthzBindAddr: 0.0.0.0:10256
+ localRedirectPolicy: true
+ operator:
+ rollOutPods: true
+ rollOutCiliumPods: true
diff --git a/clustertool/embed/generic/kubernetes/kube-system/descheduler/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/kube-system/descheduler/app/helm-release.yaml
new file mode 100644
index 0000000000000..914ccfae5cd82
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/kube-system/descheduler/app/helm-release.yaml
@@ -0,0 +1,66 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: descheduler
+ namespace: kube-system
+spec:
+ interval: 15m
+ chart:
+ spec:
+ chart: descheduler
+ version: 0.1.0
+ sourceRef:
+ kind: HelmRepository
+ name: truecharts
+ namespace: flux-system
+ interval: 15m
+ timeout: 20m
+ maxHistory: 3
+ install:
+ createNamespace: true
+ remediation:
+ retries: 3
+ upgrade:
+ cleanupOnFail: true
+ remediation:
+ retries: 3
+ uninstall:
+ keepHistory: false
+ values:
+
+ kind: Deployment
+ deschedulerPolicy:
+ strategies:
+ RemoveDuplicates:
+ enabled: true
+ RemovePodsViolatingNodeTaints:
+ enabled: true
+ RemovePodsViolatingNodeAffinity:
+ enabled: true
+ params:
+ nodeAffinityType:
+ - requiredDuringSchedulingIgnoredDuringExecution
+ RemovePodsViolatingTopologySpreadConstraint:
+ enabled: true
+ params:
+ includeSoftConstraints: true
+ RemovePodsViolatingInterPodAntiAffinity:
+ enabled: true
+ params:
+ nodeFit: true
+ LowNodeUtilization:
+ enabled: false
+ RemoveFailedPods:
+ enabled: true
+ params:
+ failedPods:
+ includingInitContainers: true
+ excludeOwnerKinds:
+ - Job
+ minPodLifetimeSeconds: 3600
+ RemovePodsHavingTooManyRestarts:
+ enabled: true
+ params:
+ podsHavingTooManyRestarts:
+ podRestartThreshold: 100
+ includingInitContainers: true
diff --git a/clustertool/embed/generic/kubernetes/kube-system/kubelet-csr-approver/app/bootstrap-values.yaml.ct b/clustertool/embed/generic/kubernetes/kube-system/kubelet-csr-approver/app/bootstrap-values.yaml.ct
new file mode 100644
index 0000000000000..0f877343c012c
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/kube-system/kubelet-csr-approver/app/bootstrap-values.yaml.ct
@@ -0,0 +1,3 @@
+metrics:
+ main:
+ enabled: false
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/kube-system/kubelet-csr-approver/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/kube-system/kubelet-csr-approver/app/helm-release.yaml
new file mode 100644
index 0000000000000..43954d5795f25
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/kube-system/kubelet-csr-approver/app/helm-release.yaml
@@ -0,0 +1,18 @@
+# yaml-language-server: $schema=https://kubernetes-schemas.zinn.ca/helm.toolkit.fluxcd.io/helmrelease_v2beta1.json
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: kubelet-csr-approver
+ namespace: kube-system
+spec:
+ interval: 30m
+ chart:
+ spec:
+ chart: kubelet-csr-approver
+ version: 1.1.0
+ sourceRef:
+ kind: HelmRepository
+ name: truecharts
+ namespace: flux-system
+ interval: 30m
+ values: {}
diff --git a/clustertool/embed/generic/kubernetes/kube-system/metrics-server/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/kube-system/metrics-server/app/helm-release.yaml
new file mode 100644
index 0000000000000..16ce424b7cf23
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/kube-system/metrics-server/app/helm-release.yaml
@@ -0,0 +1,29 @@
+# yaml-language-server: $schema=https://kubernetes-schemas.zinn.ca/helm.toolkit.fluxcd.io/helmrelease_v2beta1.json
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: metrics-server
+ namespace: kube-system
+spec:
+ interval: 15m
+ chart:
+ spec:
+ chart: metrics-server
+ version: 0.1.0
+ sourceRef:
+ kind: HelmRepository
+ name: truecharts
+ namespace: flux-system
+ interval: 15m
+ timeout: 20m
+ maxHistory: 3
+ install:
+ createNamespace: true
+ remediation:
+ retries: 3
+ upgrade:
+ remediation:
+ retries: 3
+ uninstall:
+ keepHistory: false
+ values:
diff --git a/clustertool/embed/generic/kubernetes/kube-system/namespace.yaml b/clustertool/embed/generic/kubernetes/kube-system/namespace.yaml
new file mode 100644
index 0000000000000..b78f22ddae476
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/kube-system/namespace.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: kube-system
+ labels:
+ pod-security.kubernetes.io/enforce: privileged
+ topolvm.io/webhook: ignore
diff --git a/clustertool/embed/generic/kubernetes/kube-system/node-feature-discovery/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/kube-system/node-feature-discovery/app/helm-release.yaml
new file mode 100644
index 0000000000000..153dd84cba15a
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/kube-system/node-feature-discovery/app/helm-release.yaml
@@ -0,0 +1,34 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: node-feature-discovery
+ namespace: kube-system
+spec:
+ interval: 30m
+ chart:
+ spec:
+ chart: node-feature-discovery
+ version: 0.1.0
+ sourceRef:
+ kind: HelmRepository
+ name: truecharts
+ namespace: flux-system
+ install:
+ crds: CreateReplace
+ remediation:
+ retries: 3
+ upgrade:
+ cleanupOnFail: true
+ crds: CreateReplace
+ remediation:
+ strategy: rollback
+ retries: 3
+ values:
+ worker:
+ config:
+ core:
+ sources: ["pci", "system", "usb"]
+ prometheus:
+ enable: true
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/kube-system/spegel/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/kube-system/spegel/app/helm-release.yaml
new file mode 100644
index 0000000000000..994fd219b48d2
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/kube-system/spegel/app/helm-release.yaml
@@ -0,0 +1,49 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: spegel
+ namespace: kube-system
+spec:
+ interval: 30m
+ chart:
+ spec:
+ chart: spegel
+ version: v0.0.27
+ sourceRef:
+ kind: HelmRepository
+ name: spegel
+ namespace: flux-system
+ install:
+ remediation:
+ retries: 3
+ upgrade:
+ cleanupOnFail: true
+ remediation:
+ strategy: rollback
+ retries: 3
+ values:
+ grafanaDashboard:
+ enabled: true
+ serviceMonitor:
+ enabled: false
+ spegel:
+ containerdSock: /run/containerd/containerd.sock
+ containerdRegistryConfigPath: /etc/cri/conf.d/hosts
+ appendMirrors: true
+ registries:
+ - https://tccr.io
+ - https://cgr.dev
+ - https://docker.io
+ - https://ghcr.io
+ - https://quay.io
+ - https://mcr.microsoft.com
+ - https://public.ecr.aws
+ - https://gcr.io
+ - https://registry.k8s.io
+ - https://k8s.gcr.io
+ - https://lscr.io
+ service:
+ registry:
+ hostPort: 29999
diff --git a/clustertool/embed/generic/kubernetes/system/cert-manager/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/system/cert-manager/app/helm-release.yaml
new file mode 100644
index 0000000000000..8351cac11c8b1
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/cert-manager/app/helm-release.yaml
@@ -0,0 +1,27 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: cert-manager
+ namespace: cert-manager
+spec:
+ interval: 5m
+ chart:
+ spec:
+
+ chart: cert-manager
+ version: 6.2.0
+ sourceRef:
+ kind: HelmRepository
+ name: truecharts
+ namespace: flux-system
+ interval: 5m
+ install:
+ createNamespace: true
+ crds: CreateReplace
+ remediation:
+ retries: 3
+ upgrade:
+ crds: CreateReplace
+ remediation:
+ retries: 3
+ values:
diff --git a/clustertool/embed/generic/kubernetes/system/cloudnative-pg/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/system/cloudnative-pg/app/helm-release.yaml
new file mode 100644
index 0000000000000..0d870a6cee588
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/cloudnative-pg/app/helm-release.yaml
@@ -0,0 +1,27 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: cloudnative-pg
+ namespace: cloudnative-pg
+spec:
+ interval: 5m
+ chart:
+ spec:
+
+ chart: cloudnative-pg
+ version: 8.2.0
+ sourceRef:
+ kind: HelmRepository
+ name: truecharts
+ namespace: flux-system
+ interval: 5m
+ install:
+ createNamespace: true
+ crds: CreateReplace
+ remediation:
+ retries: 3
+ upgrade:
+ crds: CreateReplace
+ remediation:
+ retries: 3
+ values:
diff --git a/clustertool/embed/generic/kubernetes/system/csi-driver-nfs/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/system/csi-driver-nfs/app/helm-release.yaml
new file mode 100644
index 0000000000000..da17b225712d0
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/csi-driver-nfs/app/helm-release.yaml
@@ -0,0 +1,26 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: csi-driver-nfs
+ namespace: kube-system
+spec:
+ interval: 30m
+ chart:
+ spec:
+ chart: csi-driver-nfs
+ version: 5.2.0
+ sourceRef:
+ kind: HelmRepository
+ name: truecharts
+ namespace: flux-system
+ install:
+ remediation:
+ retries: 3
+ upgrade:
+ cleanupOnFail: true
+ remediation:
+ strategy: rollback
+ retries: 3
+ values:
diff --git a/clustertool/embed/generic/kubernetes/system/csi-driver-smb/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/system/csi-driver-smb/app/helm-release.yaml
new file mode 100644
index 0000000000000..c68b351d66c82
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/csi-driver-smb/app/helm-release.yaml
@@ -0,0 +1,26 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: csi-driver-smb
+ namespace: kube-system
+spec:
+ interval: 30m
+ chart:
+ spec:
+ chart: csi-driver-smb
+ version: 5.2.0
+ sourceRef:
+ kind: HelmRepository
+ name: truecharts
+ namespace: flux-system
+ install:
+ remediation:
+ retries: 3
+ upgrade:
+ cleanupOnFail: true
+ remediation:
+ strategy: rollback
+ retries: 3
+ values:
diff --git a/clustertool/embed/generic/kubernetes/system/kubernetes-reflector/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/system/kubernetes-reflector/app/helm-release.yaml
new file mode 100644
index 0000000000000..d7ffbb9d86456
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/kubernetes-reflector/app/helm-release.yaml
@@ -0,0 +1,29 @@
+# yaml-language-server: $schema=https://kubernetes-schemas.zinn.ca/helm.toolkit.fluxcd.io/helmrelease_v2beta1.json
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: kubernetes-reflector
+ namespace: snapshot-controller
+spec:
+ interval: 15m
+ chart:
+ spec:
+ chart: kubernetes-reflector
+ version: 6.2.0
+ sourceRef:
+ kind: HelmRepository
+ name: truecharts
+ namespace: flux-system
+ interval: 15m
+ timeout: 20m
+ maxHistory: 3
+ install:
+ createNamespace: true
+ remediation:
+ retries: 3
+ upgrade:
+ cleanupOnFail: true
+ remediation:
+ retries: 3
+ uninstall:
+ keepHistory: false
diff --git a/clustertool/embed/generic/kubernetes/system/kyverno/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/system/kyverno/app/helm-release.yaml
new file mode 100644
index 0000000000000..e20731c6bf190
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/kyverno/app/helm-release.yaml
@@ -0,0 +1,195 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.zinn.ca/helm.toolkit.fluxcd.io/helmrelease_v2beta1.json
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: kyverno
+ namespace: kyverno
+spec:
+ interval: 15m
+ chart:
+ spec:
+ chart: kyverno
+ version: 3.2.7
+ sourceRef:
+ kind: HelmRepository
+ name: kyverno
+ namespace: flux-system
+ interval: 15m
+ timeout: 20m
+ maxHistory: 3
+ install:
+ createNamespace: true
+ remediation:
+ retries: 3
+ upgrade:
+ cleanupOnFail: true
+ remediation:
+ retries: 3
+ uninstall:
+ keepHistory: false
+ values:
+
+ crds:
+ install: true
+ grafana:
+ enabled: true
+ annotations:
+ grafana_folder: System
+ backgroundController:
+ serviceMonitor:
+ enabled: true
+ rbac:
+ clusterRole:
+ extraResources:
+ - apiGroups:
+ - ""
+ resources:
+ - ingresses
+ - pods
+ - nodes
+ verbs:
+ - create
+ - update
+ - patch
+ - delete
+ - get
+ - list
+ cleanupController:
+ serviceMonitor:
+ enabled: true
+ reportsController:
+ serviceMonitor:
+ enabled: true
+ admissionController:
+ replicas: 3
+ serviceMonitor:
+ enabled: true
+ rbac:
+ clusterRole:
+ extraResources:
+ - apiGroups:
+ - ""
+ resources:
+ - ingresses
+ - pods
+ - nodes
+ verbs:
+ - create
+ - update
+ - delete
+ topologySpreadConstraints:
+ - maxSkew: 1
+ topologyKey: kubernetes.io/hostname
+ whenUnsatisfiable: DoNotSchedule
+ labelSelector:
+ matchLabels:
+ app.kubernetes.io/instance: kyverno
+ app.kubernetes.io/component: kyverno
+
+ config:
+ # -- Resource types to be skipped by the Kyverno policy engine.
+ # Make sure to surround each entry in quotes so that it doesn't get parsed as a nested YAML list.
+ # These are joined together without spaces, run through `tpl`, and the result is set in the config map.
+ # @default -- See [values.yaml](values.yaml)
+ resourceFilters:
+ - '[Event,*,*]'
+ - '[*/*,kube-system,*]'
+ - '[*/*,kube-public,*]'
+ - '[*/*,kube-node-lease,*]'
+ - '[Node,*,*]'
+ - '[Node/*,*,*]'
+ - '[APIService,*,*]'
+ - '[APIService/*,*,*]'
+ - '[TokenReview,*,*]'
+ - '[SubjectAccessReview,*,*]'
+ - '[SelfSubjectAccessReview,*,*]'
+ - '[ReplicaSet,*,*]'
+ - '[ReplicaSet/*,*,*]'
+ # exclude resources from the chart
+ - '[ClusterRole,*,{{ template "kyverno.admission-controller.roleName" . }}]'
+ - '[ClusterRole,*,{{ template "kyverno.admission-controller.roleName" . }}:core]'
+ - '[ClusterRole,*,{{ template "kyverno.admission-controller.roleName" . }}:additional]'
+ - '[ClusterRole,*,{{ template "kyverno.background-controller.roleName" . }}]'
+ - '[ClusterRole,*,{{ template "kyverno.background-controller.roleName" . }}:core]'
+ - '[ClusterRole,*,{{ template "kyverno.background-controller.roleName" . }}:additional]'
+ - '[ClusterRole,*,{{ template "kyverno.cleanup-controller.roleName" . }}]'
+ - '[ClusterRole,*,{{ template "kyverno.cleanup-controller.roleName" . }}:core]'
+ - '[ClusterRole,*,{{ template "kyverno.cleanup-controller.roleName" . }}:additional]'
+ - '[ClusterRole,*,{{ template "kyverno.reports-controller.roleName" . }}]'
+ - '[ClusterRole,*,{{ template "kyverno.reports-controller.roleName" . }}:core]'
+ - '[ClusterRole,*,{{ template "kyverno.reports-controller.roleName" . }}:additional]'
+ - '[ClusterRoleBinding,*,{{ template "kyverno.admission-controller.roleName" . }}]'
+ - '[ClusterRoleBinding,*,{{ template "kyverno.background-controller.roleName" . }}]'
+ - '[ClusterRoleBinding,*,{{ template "kyverno.cleanup-controller.roleName" . }}]'
+ - '[ClusterRoleBinding,*,{{ template "kyverno.reports-controller.roleName" . }}]'
+ - '[ServiceAccount,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.serviceAccountName" . }}]'
+ - '[ServiceAccount/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.serviceAccountName" . }}]'
+ - '[ServiceAccount,{{ include "kyverno.namespace" . }},{{ template "kyverno.background-controller.serviceAccountName" . }}]'
+ - '[ServiceAccount/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.background-controller.serviceAccountName" . }}]'
+ - '[ServiceAccount,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.serviceAccountName" . }}]'
+ - '[ServiceAccount/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.serviceAccountName" . }}]'
+ - '[ServiceAccount,{{ include "kyverno.namespace" . }},{{ template "kyverno.reports-controller.serviceAccountName" . }}]'
+ - '[ServiceAccount/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.reports-controller.serviceAccountName" . }}]'
+ - '[Role,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.roleName" . }}]'
+ - '[Role,{{ include "kyverno.namespace" . }},{{ template "kyverno.background-controller.roleName" . }}]'
+ - '[Role,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.roleName" . }}]'
+ - '[Role,{{ include "kyverno.namespace" . }},{{ template "kyverno.reports-controller.roleName" . }}]'
+ - '[RoleBinding,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.roleName" . }}]'
+ - '[RoleBinding,{{ include "kyverno.namespace" . }},{{ template "kyverno.background-controller.roleName" . }}]'
+ - '[RoleBinding,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.roleName" . }}]'
+ - '[RoleBinding,{{ include "kyverno.namespace" . }},{{ template "kyverno.reports-controller.roleName" . }}]'
+ - '[ConfigMap,{{ include "kyverno.namespace" . }},{{ template "kyverno.config.configMapName" . }}]'
+ - '[ConfigMap,{{ include "kyverno.namespace" . }},{{ template "kyverno.config.metricsConfigMapName" . }}]'
+ - '[Deployment,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.name" . }}]'
+ - '[Deployment/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.name" . }}]'
+ - '[Deployment,{{ include "kyverno.namespace" . }},{{ template "kyverno.background-controller.name" . }}]'
+ - '[Deployment/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.background-controller.name" . }}]'
+ - '[Deployment,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.name" . }}]'
+ - '[Deployment/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.name" . }}]'
+ - '[Deployment,{{ include "kyverno.namespace" . }},{{ template "kyverno.reports-controller.name" . }}]'
+ - '[Deployment/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.reports-controller.name" . }}]'
+ - '[Pod,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.name" . }}-*]'
+ - '[Pod/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.name" . }}-*]'
+ - '[Pod,{{ include "kyverno.namespace" . }},{{ template "kyverno.background-controller.name" . }}-*]'
+ - '[Pod/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.background-controller.name" . }}-*]'
+ - '[Pod,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.name" . }}-*]'
+ - '[Pod/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.name" . }}-*]'
+ - '[Pod,{{ include "kyverno.namespace" . }},{{ template "kyverno.reports-controller.name" . }}-*]'
+ - '[Pod/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.reports-controller.name" . }}-*]'
+ - '[Job,{{ include "kyverno.namespace" . }},{{ template "kyverno.fullname" . }}-hook-pre-delete]'
+ - '[Job/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.fullname" . }}-hook-pre-delete]'
+ - '[NetworkPolicy,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.name" . }}]'
+ - '[NetworkPolicy/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.name" . }}]'
+ - '[NetworkPolicy,{{ include "kyverno.namespace" . }},{{ template "kyverno.background-controller.name" . }}]'
+ - '[NetworkPolicy/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.background-controller.name" . }}]'
+ - '[NetworkPolicy,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.name" . }}]'
+ - '[NetworkPolicy/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.name" . }}]'
+ - '[NetworkPolicy,{{ include "kyverno.namespace" . }},{{ template "kyverno.reports-controller.name" . }}]'
+ - '[NetworkPolicy/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.reports-controller.name" . }}]'
+ - '[PodDisruptionBudget,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.name" . }}]'
+ - '[PodDisruptionBudget/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.name" . }}]'
+ - '[PodDisruptionBudget,{{ include "kyverno.namespace" . }},{{ template "kyverno.background-controller.name" . }}]'
+ - '[PodDisruptionBudget/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.background-controller.name" . }}]'
+ - '[PodDisruptionBudget,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.name" . }}]'
+ - '[PodDisruptionBudget/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.name" . }}]'
+ - '[PodDisruptionBudget,{{ include "kyverno.namespace" . }},{{ template "kyverno.reports-controller.name" . }}]'
+ - '[PodDisruptionBudget/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.reports-controller.name" . }}]'
+ - '[Service,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.serviceName" . }}]'
+ - '[Service/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.serviceName" . }}]'
+ - '[Service,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.serviceName" . }}-metrics]'
+ - '[Service/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.serviceName" . }}-metrics]'
+ - '[Service,{{ include "kyverno.namespace" . }},{{ template "kyverno.background-controller.name" . }}-metrics]'
+ - '[Service/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.background-controller.name" . }}-metrics]'
+ - '[Service,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.name" . }}]'
+ - '[Service/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.name" . }}]'
+ - '[Service,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.name" . }}-metrics]'
+ - '[Service/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.name" . }}-metrics]'
+ - '[Service,{{ include "kyverno.namespace" . }},{{ template "kyverno.reports-controller.name" . }}-metrics]'
+ - '[Service/*,{{ include "kyverno.namespace" . }},{{ template "kyverno.reports-controller.name" . }}-metrics]'
+ - '[ServiceMonitor,{{ if .Values.admissionController.serviceMonitor.namespace }}{{ .Values.admissionController.serviceMonitor.namespace }}{{ else }}{{ template "kyverno.namespace" . }}{{ end }},{{ template "kyverno.admission-controller.name" . }}]'
+ - '[ServiceMonitor,{{ if .Values.admissionController.serviceMonitor.namespace }}{{ .Values.admissionController.serviceMonitor.namespace }}{{ else }}{{ template "kyverno.namespace" . }}{{ end }},{{ template "kyverno.background-controller.name" . }}]'
+ - '[ServiceMonitor,{{ if .Values.admissionController.serviceMonitor.namespace }}{{ .Values.admissionController.serviceMonitor.namespace }}{{ else }}{{ template "kyverno.namespace" . }}{{ end }},{{ template "kyverno.cleanup-controller.name" . }}]'
+ - '[ServiceMonitor,{{ if .Values.admissionController.serviceMonitor.namespace }}{{ .Values.admissionController.serviceMonitor.namespace }}{{ else }}{{ template "kyverno.namespace" . }}{{ end }},{{ template "kyverno.reports-controller.name" . }}]'
+ - '[Secret,{{ include "kyverno.namespace" . }},{{ template "kyverno.admission-controller.serviceName" . }}.{{ template "kyverno.namespace" . }}.svc.*]'
+ - '[Secret,{{ include "kyverno.namespace" . }},{{ template "kyverno.cleanup-controller.name" . }}.{{ template "kyverno.namespace" . }}.svc.*]'
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/system/kyverno/app/kustomization.yaml b/clustertool/embed/generic/kubernetes/system/kyverno/app/kustomization.yaml
new file mode 100644
index 0000000000000..4c6cb03f05bb8
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/kyverno/app/kustomization.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+ - helm-release.yaml
+ - namespace.yaml
diff --git a/clustertool/embed/generic/kubernetes/system/kyverno/app/namespace.yaml b/clustertool/embed/generic/kubernetes/system/kyverno/app/namespace.yaml
new file mode 100644
index 0000000000000..9dda69583f1c8
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/kyverno/app/namespace.yaml
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: kyverno
+ labels:
+ kustomize.toolkit.fluxcd.io/prune: disabled
+ goldilocks.fairwinds.com/enabled: "true"
+ pod-security.kubernetes.io/enforce: privileged
diff --git a/clustertool/embed/generic/kubernetes/system/kyverno/ks.yaml b/clustertool/embed/generic/kubernetes/system/kyverno/ks.yaml
new file mode 100644
index 0000000000000..c96e462bfa92f
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/kyverno/ks.yaml
@@ -0,0 +1,12 @@
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+ name: kyverno
+ namespace: flux-system
+spec:
+ interval: 10m
+ path: clusters/main/kubernetes/system/kyverno/app
+ prune: true
+ sourceRef:
+ kind: GitRepository
+ name: cluster
diff --git a/clustertool/embed/generic/kubernetes/system/longhorn/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/system/longhorn/app/helm-release.yaml
new file mode 100644
index 0000000000000..0690d2af2122d
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/longhorn/app/helm-release.yaml
@@ -0,0 +1,38 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: longhorn
+ namespace: longhorn-system
+spec:
+ interval: 5m
+ releaseName: longhorn
+ chart:
+ spec:
+
+ chart: longhorn
+ version: 1.7.1
+ sourceRef:
+ kind: HelmRepository
+ name: longhorn
+ namespace: flux-system
+ install:
+ createNamespace: true
+ crds: CreateReplace
+ remediation:
+ retries: 3
+ upgrade:
+ crds: CreateReplace
+ remediation:
+ retries: 3
+ values:
+ defaultSettings:
+ # Increase to 3 for a multi-node cluster
+ defaultReplicaCount: 1
+ # Overprovisioning might be needed when using volsync
+ storageOverProvisioningPercentage: 100000
+ # v2DataEngine: true
+ persistence:
+ # Set to false to pick another CSI as default
+ defaultClass: true
+ # Increase to 3 for a multi-node cluster
+ defaultClassReplicaCount: 1
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/system/longhorn/app/namespace.yaml b/clustertool/embed/generic/kubernetes/system/longhorn/app/namespace.yaml
new file mode 100644
index 0000000000000..e8a73700c2c13
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/longhorn/app/namespace.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: longhorn-system
+ labels:
+ pod-security.kubernetes.io/enforce: privileged
+ topolvm.io/webhook: ignore
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/system/longhorn/app/volumeSnapshotClass.yaml b/clustertool/embed/generic/kubernetes/system/longhorn/app/volumeSnapshotClass.yaml
new file mode 100644
index 0000000000000..189600461107e
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/longhorn/app/volumeSnapshotClass.yaml
@@ -0,0 +1,11 @@
+---
+kind: VolumeSnapshotClass
+apiVersion: snapshot.storage.k8s.io/v1
+metadata:
+ name: longhorn-snapshot-vsc
+ annotations:
+ snapshot.storage.kubernetes.io/is-default-class: 'true'
+driver: driver.longhorn.io
+deletionPolicy: Delete
+parameters:
+ type: snap
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/system/metallb/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/system/metallb/app/helm-release.yaml
new file mode 100644
index 0000000000000..0efd48559a434
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/metallb/app/helm-release.yaml
@@ -0,0 +1,26 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: metallb
+ namespace: metallb
+spec:
+ interval: 5m
+ chart:
+ spec:
+ chart: metallb
+ version: 0.14.8
+ sourceRef:
+ kind: HelmRepository
+ name: metallb
+ namespace: flux-system
+ interval: 5m
+ install:
+ createNamespace: true
+ remediation:
+ retries: 3
+ upgrade:
+ remediation:
+ retries: 3
+ values:
+ speaker:
+ ignoreExcludeLB: true
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/system/metallb/app/namespace.yaml b/clustertool/embed/generic/kubernetes/system/metallb/app/namespace.yaml
new file mode 100644
index 0000000000000..8fa433d39c05a
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/metallb/app/namespace.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: metallb
+ labels:
+ pod-security.kubernetes.io/enforce: privileged
+ topolvm.io/webhook: ignore
diff --git a/clustertool/embed/generic/kubernetes/system/openebs/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/system/openebs/app/helm-release.yaml
new file mode 100644
index 0000000000000..4706b2ca61431
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/openebs/app/helm-release.yaml
@@ -0,0 +1,45 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: openebs
+ namespace: openebs
+spec:
+ interval: 5m
+ releaseName: openebs
+ chart:
+ spec:
+
+ chart: openebs
+ version: 4.1.1
+ sourceRef:
+ kind: HelmRepository
+ name: openebs
+ namespace: flux-system
+ install:
+ createNamespace: true
+ crds: CreateReplace
+ remediation:
+ retries: 3
+ upgrade:
+ crds: CreateReplace
+ remediation:
+ retries: 3
+ values:
+ openebs-crds:
+ csi:
+ volumeSnapshots:
+ enabled: false
+ keep: false
+
+ engines:
+ local:
+ # The lvm-localpv backend contains a duplicate of snapshot controller which will cause conflicts
+ lvm:
+ enabled: false
+ # The ZFS backend contains a duplicate of snapshot controller which will cause conflicts
+ zfs:
+ enabled: false
+ replicated:
+ # The Mayastor backend contains a duplicate of snapshot controller which will cause conflicts
+ mayastor:
+ enabled: false
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/system/openebs/app/namespace.yaml b/clustertool/embed/generic/kubernetes/system/openebs/app/namespace.yaml
new file mode 100644
index 0000000000000..bb543052adb0e
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/openebs/app/namespace.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: openebs
+ labels:
+ pod-security.kubernetes.io/enforce: privileged
+ topolvm.io/webhook: ignore
diff --git a/clustertool/embed/generic/kubernetes/system/prometheus-operator/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/system/prometheus-operator/app/helm-release.yaml
new file mode 100644
index 0000000000000..7d9dd60d9c595
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/prometheus-operator/app/helm-release.yaml
@@ -0,0 +1,27 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: prometheus-operator
+ namespace: prometheus-operator
+spec:
+ interval: 5m
+ chart:
+ spec:
+
+ chart: prometheus-operator
+ version: 8.5.0
+ sourceRef:
+ kind: HelmRepository
+ name: truecharts
+ namespace: flux-system
+ interval: 5m
+ install:
+ createNamespace: true
+ crds: CreateReplace
+ remediation:
+ retries: 3
+ upgrade:
+ crds: CreateReplace
+ remediation:
+ retries: 3
+ values: {}
diff --git a/clustertool/embed/generic/kubernetes/system/snapshot-controller/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/system/snapshot-controller/app/helm-release.yaml
new file mode 100644
index 0000000000000..761472893f32d
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/snapshot-controller/app/helm-release.yaml
@@ -0,0 +1,27 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: snapshot-controller
+ namespace: snapshot-controller
+spec:
+ interval: 5m
+ chart:
+ spec:
+ chart: snapshot-controller
+ version: 3.3.0
+ sourceRef:
+ kind: HelmRepository
+ name: truecharts
+ namespace: flux-system
+ interval: 5m
+ install:
+ createNamespace: true
+ remediation:
+ retries: 3
+ upgrade:
+ cleanupOnFail: true
+ crds: CreateReplace
+ remediation:
+ strategy: rollback
+ retries: 3
+ values:
diff --git a/clustertool/embed/generic/kubernetes/system/system-upgrade-controller/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/system/system-upgrade-controller/app/helm-release.yaml
new file mode 100644
index 0000000000000..46ca8de76b4ee
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/system-upgrade-controller/app/helm-release.yaml
@@ -0,0 +1,102 @@
+---
+# yaml-language-server: $schema=https://raw.githubusercontent.com/bjw-s/helm-charts/main/charts/other/app-template/schemas/helmrelease-helm-v2.schema.json
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: system-upgrade-controller
+ namespace: system-upgrade
+spec:
+ interval: 30m
+ chart:
+ spec:
+ chart: app-template
+ version: 3.5.1
+ sourceRef:
+ kind: HelmRepository
+ name: bjw-s
+ namespace: flux-system
+ install:
+ remediation:
+ retries: 3
+ upgrade:
+ cleanupOnFail: true
+ remediation:
+ strategy: rollback
+ retries: 3
+ values:
+ controllers:
+ system-upgrade-controller:
+ strategy: RollingUpdate
+ containers:
+ app:
+ image:
+ repository: docker.io/rancher/system-upgrade-controller
+ tag: v0.14.1@sha256:7e13a9b2b984f0c0fd6328439b575348723cc6954b91db3453057fcb784e2d29
+ env:
+ SYSTEM_UPGRADE_CONTROLLER_DEBUG: false
+ SYSTEM_UPGRADE_CONTROLLER_THREADS: 2
+ SYSTEM_UPGRADE_JOB_ACTIVE_DEADLINE_SECONDS: 900
+ SYSTEM_UPGRADE_JOB_BACKOFF_LIMIT: 99
+ SYSTEM_UPGRADE_JOB_IMAGE_PULL_POLICY: IfNotPresent
+ SYSTEM_UPGRADE_JOB_KUBECTL_IMAGE: registry.k8s.io/kubectl:v1.31.1
+ SYSTEM_UPGRADE_JOB_POD_REPLACEMENT_POLICY: Failed
+ SYSTEM_UPGRADE_JOB_PRIVILEGED: true
+ SYSTEM_UPGRADE_JOB_TTL_SECONDS_AFTER_FINISH: 900
+ SYSTEM_UPGRADE_PLAN_POLLING_INTERVAL: 15m
+ SYSTEM_UPGRADE_CONTROLLER_NAME: system-update-controller
+ SYSTEM_UPGRADE_CONTROLLER_NAMESPACE:
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: true
+ capabilities: { drop: ["ALL"] }
+ seccompProfile:
+ type: RuntimeDefault
+ defaultPodOptions:
+ securityContext:
+ runAsNonRoot: true
+ runAsUser: 65534
+ runAsGroup: 65534
+ seccompProfile: { type: RuntimeDefault }
+ affinity:
+ nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: node-role.kubernetes.io/control-plane
+ operator: Exists
+ tolerations:
+ - key: CriticalAddonsOnly
+ operator: Exists
+ - key: node-role.kubernetes.io/control-plane
+ operator: Exists
+ effect: NoSchedule
+ - key: node-role.kubernetes.io/master
+ operator: Exists
+ effect: NoSchedule
+ serviceAccount:
+ create: true
+ name: system-upgrade
+ persistence:
+ tmp:
+ type: emptyDir
+ etc-ssl:
+ type: hostPath
+ hostPath: /etc/ssl
+ hostPathType: DirectoryOrCreate
+ globalMounts:
+ - readOnly: true
+ etc-pki:
+ type: hostPath
+ hostPath: /etc/pki
+ hostPathType: DirectoryOrCreate
+ globalMounts:
+ - readOnly: true
+ etc-ca-certificates:
+ type: hostPath
+ hostPath: /etc/ca-certificates
+ hostPathType: DirectoryOrCreate
+ globalMounts:
+ - readOnly: true
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/system/system-upgrade-controller/app/kustomization.yaml b/clustertool/embed/generic/kubernetes/system/system-upgrade-controller/app/kustomization.yaml
new file mode 100644
index 0000000000000..a6851bf1119b8
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/system-upgrade-controller/app/kustomization.yaml
@@ -0,0 +1,8 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+ - namespace.yaml
+ - helm-release.yaml
+ - rbac.yaml
diff --git a/clustertool/embed/generic/kubernetes/system/system-upgrade-controller/app/namespace.yaml b/clustertool/embed/generic/kubernetes/system/system-upgrade-controller/app/namespace.yaml
new file mode 100644
index 0000000000000..1a1d3c4216261
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/system-upgrade-controller/app/namespace.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: system-upgrade
+ annotations:
+ volsync.backube/privileged-movers: "true"
+ labels:
+ kustomize.toolkit.fluxcd.io/prune: disabled
+ goldilocks.fairwinds.com/enabled: "true"
+ pod-security.kubernetes.io/enforce: privileged
diff --git a/clustertool/embed/generic/kubernetes/system/system-upgrade-controller/app/rbac.yaml b/clustertool/embed/generic/kubernetes/system/system-upgrade-controller/app/rbac.yaml
new file mode 100644
index 0000000000000..3396e1197f39f
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/system-upgrade-controller/app/rbac.yaml
@@ -0,0 +1,22 @@
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: system-upgrade
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: cluster-admin
+subjects:
+ - kind: ServiceAccount
+ name: system-upgrade
+ namespace: system-upgrade
+---
+apiVersion: talos.dev/v1alpha1
+kind: ServiceAccount
+metadata:
+ name: talos
+ namespace: system-upgrade
+spec:
+ roles:
+ - os:admin
diff --git a/clustertool/embed/generic/kubernetes/system/system-upgrade-controller/ks.yaml b/clustertool/embed/generic/kubernetes/system/system-upgrade-controller/ks.yaml
new file mode 100644
index 0000000000000..a5c4af1fd3819
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/system-upgrade-controller/ks.yaml
@@ -0,0 +1,13 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+ name: system-upgrade-controller
+ namespace: flux-system
+spec:
+ interval: 10m
+ path: clusters/main/kubernetes/system/system-upgrade-controller/app
+ prune: true
+ sourceRef:
+ kind: GitRepository
+ name: cluster
diff --git a/clustertool/embed/generic/kubernetes/system/topolvm/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/system/topolvm/app/helm-release.yaml
new file mode 100644
index 0000000000000..d141fb8f053b4
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/topolvm/app/helm-release.yaml
@@ -0,0 +1,55 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: topolvm
+ namespace: topolvm-system
+spec:
+ interval: 5m
+ releaseName: topolvm
+ chart:
+ spec:
+
+ chart: topolvm
+ version: 15.4.0
+ sourceRef:
+ kind: HelmRepository
+ name: topolvm
+ namespace: flux-system
+ install:
+ createNamespace: true
+ crds: CreateReplace
+ remediation:
+ retries: 3
+ upgrade:
+ crds: CreateReplace
+ remediation:
+ retries: 3
+ values:
+ lvmd:
+ managed: false
+ env:
+ - name: LVM_SYSTEM_DIR
+ value: /tmp
+ deviceClasses:
+ - name: thin
+ volume-group: topolvm_vg # Volume Group name used in LVM_Disk_Watcher
+ default: true
+ spare-gb: 10
+ type: thin
+ thin-pool:
+ name: topolvm_thin # Logical Volume name used in LVM_Disk_Watcher
+ overprovision-ratio: 10.0 # Adjust to your convenience
+ storageClasses:
+ - name: topolvm-thin-provisioner
+ storageClass:
+ fsType: xfs
+ isDefaultClass: false
+ volumeBindingMode: WaitForFirstConsumer
+ allowVolumeExpansion: true
+ additionalParameters:
+ "topolvm.io/device-class": "thin"
+ node:
+ lvmdEmbedded: true
+
+ controller:
+ replicaCount: 1
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/system/topolvm/app/namespace.yaml b/clustertool/embed/generic/kubernetes/system/topolvm/app/namespace.yaml
new file mode 100644
index 0000000000000..b390b5d5c1b62
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/topolvm/app/namespace.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: topolvm-system
+ labels:
+ pod-security.kubernetes.io/enforce: privileged
+ topolvm.io/webhook: ignore
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/system/topolvm/app/volumeSnapshotClass.yaml b/clustertool/embed/generic/kubernetes/system/topolvm/app/volumeSnapshotClass.yaml
new file mode 100644
index 0000000000000..81120a84998ac
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/topolvm/app/volumeSnapshotClass.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: snapshot.storage.k8s.io/v1
+kind: VolumeSnapshotClass
+metadata:
+ name: topolvm-provisioner-thin
+ # annotations:
+ # snapshot.storage.kubernetes.io/is-default-class: "false"
+driver: topolvm.io
+deletionPolicy: Delete
\ No newline at end of file
diff --git a/clustertool/embed/generic/kubernetes/system/traefik-crds/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/system/traefik-crds/app/helm-release.yaml
new file mode 100644
index 0000000000000..94cbc148c35ed
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/traefik-crds/app/helm-release.yaml
@@ -0,0 +1,30 @@
+# yaml-language-server: $schema=https://kubernetes-schemas.zinn.ca/helm.toolkit.fluxcd.io/helmrelease_v2beta1.json
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: traefik-crds
+ namespace: system
+spec:
+ interval: 15m
+ chart:
+ spec:
+ chart: traefik-crds
+ version: 3.2.0
+ sourceRef:
+ kind: HelmRepository
+ name: truecharts
+ namespace: flux-system
+ interval: 15m
+ timeout: 20m
+ maxHistory: 3
+ install:
+ createNamespace: true
+ remediation:
+ retries: 3
+ upgrade:
+ cleanupOnFail: true
+ remediation:
+ retries: 3
+ uninstall:
+ keepHistory: false
+ values: {}
diff --git a/clustertool/embed/generic/kubernetes/system/volsync/app/helm-release.yaml b/clustertool/embed/generic/kubernetes/system/volsync/app/helm-release.yaml
new file mode 100644
index 0000000000000..49de7d0691c06
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/volsync/app/helm-release.yaml
@@ -0,0 +1,24 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: volsync
+ namespace: volsync
+spec:
+ interval: 5m
+ chart:
+ spec:
+
+ chart: volsync
+ version: 2.3.0
+ sourceRef:
+ kind: HelmRepository
+ name: truecharts
+ namespace: flux-system
+ interval: 5m
+ install:
+ createNamespace: true
+ remediation:
+ retries: 3
+ upgrade:
+ remediation:
+ retries: 3
diff --git a/clustertool/embed/generic/kubernetes/system/volsync/app/namespace.yaml b/clustertool/embed/generic/kubernetes/system/volsync/app/namespace.yaml
new file mode 100644
index 0000000000000..554f405e86eca
--- /dev/null
+++ b/clustertool/embed/generic/kubernetes/system/volsync/app/namespace.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: volsync
+ labels:
+ pod-security.kubernetes.io/enforce: privileged
+ topolvm.io/webhook: ignore
\ No newline at end of file
diff --git a/clustertool/embed/generic/patches/all.yaml b/clustertool/embed/generic/patches/all.yaml
new file mode 100644
index 0000000000000..56b683a106eba
--- /dev/null
+++ b/clustertool/embed/generic/patches/all.yaml
@@ -0,0 +1,100 @@
+- op: replace
+ path: /machine/time
+ value:
+ "disabled": false
+ "servers":
+ - "time.cloudflare.com"
+- op: add
+ path: /machine/kernel
+ value:
+ modules:
+ - "name": "dm_thin_pool"
+ - "name": "dm_mod"
+ - "name": nvme_tcp
+ - "name": vfio_pci
+ - "name": uio_pci_generic
+- op: replace
+ path: /cluster/proxy
+ value:
+ "disabled": true
+- op: add
+ path: /machine/kubelet/extraArgs
+ value:
+ "rotate-server-certificates": true
+- op: add
+ path: /machine/kubelet/extraConfig
+ value:
+ "maxPods": 250
+ "shutdownGracePeriod": "15s"
+ "shutdownGracePeriodCriticalPods": "10s"
+- op: add
+ path: /machine/kubelet/extraMounts
+ value:
+ - "destination": "/var/openebs/local"
+ "type": "bind"
+ "source": "/var/openebs/local"
+ "options":
+ - "bind"
+ - "rshared"
+ - "rw"
+ - destination: /var/lib/longhorn
+ type: bind
+ source: /var/lib/longhorn
+ options:
+ - bind
+ - rshared
+ - rw
+- op: replace
+ path: /machine/features/hostDNS
+ value:
+ enabled: true
+ resolveMemberNames: true
+ forwardKubeDNSToHost: false
+- op: add
+ path: /machine/sysctls
+ value:
+ fs.inotify.max_queued_events: "65536"
+ fs.inotify.max_user_instances: "8192"
+ fs.inotify.max_user_watches: "524288"
+ net.core.rmem_max: "2500000"
+ net.core.wmem_max: "2500000"
+ vm.nr_hugepages: "2048"
+
+## TODO: Check how we can have this pass checks
+# - op: add
+# path: /machine/udev
+# value:
+# # Thunderbolt
+# - ACTION=="add", SUBSYSTEM=="thunderbolt", ATTR{authorized}=="0", ATTR{authorized}="1"
+# # Intel GPU
+# - SUBSYSTEM=="drm", KERNEL=="renderD*", GROUP="44", MODE="0660"
+# # Google Coral USB Accelerator
+# - SUBSYSTEMS=="usb", ATTRS{idVendor}=="1a6e", ATTRS{idProduct}=="089a", GROUP="20", MODE="0660"
+# - SUBSYSTEMS=="usb", ATTRS{idVendor}=="18d1", ATTRS{idProduct}=="9302", GROUP="20", MODE="0660"
+
+- op: add
+ path: /machine/files
+ value:
+ - content: |
+ [plugins."io.containerd.grpc.v1.cri"]
+ enable_unprivileged_ports = true
+ enable_unprivileged_icmp = true
+ [plugins."io.containerd.grpc.v1.cri".containerd]
+ discard_unpacked_layers = false
+ [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
+ discard_unpacked_layers = false
+ permissions: 0
+ path: /etc/cri/conf.d/20-customization.part
+ op: create
+ - content: |
+ [ NFSMount_Global_Options ]
+ nfsvers=4.2
+ hard=True
+ noatime=True
+ nodiratime=True
+ rsize=131072
+ wsize=131072
+ nconnect=8
+ permissions: 420
+ path: /etc/nfsmount.conf
+ op: overwrite
diff --git a/clustertool/embed/generic/patches/controlplane.yaml b/clustertool/embed/generic/patches/controlplane.yaml
new file mode 100644
index 0000000000000..fc613a4a2fa42
--- /dev/null
+++ b/clustertool/embed/generic/patches/controlplane.yaml
@@ -0,0 +1,50 @@
+- op: add
+ path: /cluster/proxy/extraArgs
+ value:
+ "metrics-bind-address": "0.0.0.0:10249"
+- op: add
+ path: /cluster/controllerManager/extraArgs
+ value:
+ "bind-address": "0.0.0.0"
+
+- op: add
+ path: /cluster/scheduler/extraArgs
+ value:
+ "bind-address": "0.0.0.0"
+- op: replace
+ path: /cluster/apiServer/admissionControl
+ value:
+ - name: PodSecurity
+ configuration:
+ apiVersion: pod-security.admission.config.k8s.io/v1alpha1
+ defaults:
+ audit: restricted
+ audit-version: latest
+ enforce: baseline
+ enforce-version: latest
+ warn: restricted
+ warn-version: latest
+ exemptions:
+ namespaces:
+ - kube-system
+ - metallb
+ - metallb-config
+ - topolvm-system
+ - longhorn-system
+ - kyverno
+ - system-upgrade
+ - openebs
+ - snapshot-controller
+ - volsync
+ - flux-system
+ runtimeClasses: []
+ usernames: []
+ kind: PodSecurityConfiguration
+- op: add
+ path: /machine/features/kubernetesTalosAPIAccess
+ value:
+ enabled: true
+ allowedRoles:
+ - os:admin
+ allowedKubernetesNamespaces:
+ - system-upgrade
\ No newline at end of file
diff --git a/clustertool/embed/generic/patches/manifests.yaml b/clustertool/embed/generic/patches/manifests.yaml
new file mode 100644
index 0000000000000..c0c949378b262
--- /dev/null
+++ b/clustertool/embed/generic/patches/manifests.yaml
@@ -0,0 +1,6 @@
+- op: replace
+ path: /machine/time
+ value:
+ "disabled": false
+ "servers":
+ - "time.cloudflare.com"
\ No newline at end of file
diff --git a/clustertool/embed/generic/patches/sopssecret.yaml b/clustertool/embed/generic/patches/sopssecret.yaml
new file mode 100644
index 0000000000000..abe87d0d4083e
--- /dev/null
+++ b/clustertool/embed/generic/patches/sopssecret.yaml
@@ -0,0 +1,40 @@
+- op: add
+ path: /cluster/inlineManifests
+ value:
+ - name: flux-system
+ contents: |-
+ apiVersion: v1
+ kind: Namespace
+ metadata:
+ name: flux-system
+ - name: sops-age
+ contents: |-
+ apiVersion: v1
+ stringData:
+ age.agekey: REPLACEWITHSOPS
+ kind: Secret
+ metadata:
+ creationTimestamp: null
+ name: sops-age
+ namespace: flux-system
+ - name: cluster-config
+ contents: |-
+ apiVersion: v1
+ kind: ConfigMap
+ metadata:
+ creationTimestamp: null
+ name: cluster-config
+ namespace: flux-system
+ data:
+REPLACEWITHTALENV
+ - name: deploy-key
+ contents: |-
+ apiVersion: v1
+ kind: ConfigMap
+ metadata:
+ creationTimestamp: null
+ name: deploy-key
+ namespace: flux-system
+ stringData:
+REPLACEWITHDEPLOYKEY
+
\ No newline at end of file
diff --git a/clustertool/embed/generic/patches/worker.yaml b/clustertool/embed/generic/patches/worker.yaml
new file mode 100644
index 0000000000000..c0c949378b262
--- /dev/null
+++ b/clustertool/embed/generic/patches/worker.yaml
@@ -0,0 +1,6 @@
+- op: replace
+ path: /machine/time
+ value:
+ "disabled": false
+ "servers":
+ - "time.cloudflare.com"
\ No newline at end of file
diff --git a/clustertool/embed/generic/root/DOTREPLACEgithub/renovate.json5 b/clustertool/embed/generic/root/DOTREPLACEgithub/renovate.json5
new file mode 100644
index 0000000000000..65eec95090b0d
--- /dev/null
+++ b/clustertool/embed/generic/root/DOTREPLACEgithub/renovate.json5
@@ -0,0 +1,7 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "config:recommended",
+ "github>truecharts/public//.github/renovate/main.json5"
+ ]
+}
diff --git a/clustertool/embed/generic/root/DOTREPLACEgitignore b/clustertool/embed/generic/root/DOTREPLACEgitignore
new file mode 100644
index 0000000000000..37e077a83aeb9
--- /dev/null
+++ b/clustertool/embed/generic/root/DOTREPLACEgitignore
@@ -0,0 +1,10 @@
+age.agekey
+^clustertool.exe
+^clustertool*
+^clustertool
+*clustertool.exe
+clustertool.exe
+clustertool
+sopssecret.yaml
+sopssecret*
+*sopssecret*
\ No newline at end of file
diff --git a/clustertool/embed/generic/root/DOTREPLACEsops.yaml b/clustertool/embed/generic/root/DOTREPLACEsops.yaml
new file mode 100644
index 0000000000000..eaf2456a29bf4
--- /dev/null
+++ b/clustertool/embed/generic/root/DOTREPLACEsops.yaml
@@ -0,0 +1,15 @@
+## Do not edit between this and DO NOT REMOVE
+creation_rules:
+ - path_regex: ^clusters.*kubernetes.*values.ya?ml$
+ age: REPLACEME
+ encrypted_regex: "((?i)(displayname|email|pass|ca|id|bootstraptoken|secretboxencryptionsecret|secrets|secrets|password|cert|secret($|[^N])|key|token|^data$|^stringData))"
+ - path_regex: ^clusters.*kubernetes.*\.secret.ya?ml
+ age: REPLACEME
+ encrypted_regex: "((?i)(displayname|email|pass|ca|id|bootstraptoken|secretboxencryptionsecret|secrets|secrets|password|cert|secret($|[^N])|key|token|^data$|^stringData))"
+ - path_regex: talenv.yaml
+ age: REPLACEME
+ - path_regex: clusterenv.yaml
+ age: REPLACEME
+ - path_regex: talsecret.yaml
+ age: REPLACEME
+## DO NOT REMOVE: Personal setting go under this line
\ No newline at end of file
diff --git a/clustertool/embed/generic/root/README.md b/clustertool/embed/generic/root/README.md
new file mode 100644
index 0000000000000..e159bbe91c9a0
--- /dev/null
+++ b/clustertool/embed/generic/root/README.md
@@ -0,0 +1 @@
+This is a kubernetes cluster Powered by TrueCharts ClusterTool
\ No newline at end of file
diff --git a/clustertool/embed/generic/root/repositories/entries/kustomization.yaml b/clustertool/embed/generic/root/repositories/entries/kustomization.yaml
new file mode 100644
index 0000000000000..0b5a5054221da
--- /dev/null
+++ b/clustertool/embed/generic/root/repositories/entries/kustomization.yaml
@@ -0,0 +1,5 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources: []
\ No newline at end of file
diff --git a/clustertool/embed/generic/root/repositories/flux-entry.yaml b/clustertool/embed/generic/root/repositories/flux-entry.yaml
new file mode 100644
index 0000000000000..7888e23d899cb
--- /dev/null
+++ b/clustertool/embed/generic/root/repositories/flux-entry.yaml
@@ -0,0 +1,41 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+ name: flux-entry-repos
+ namespace: flux-system
+spec:
+ interval: 10m
+ path: ./repositories
+ prune: true
+ sourceRef:
+ kind: GitRepository
+ name: cluster
+ decryption:
+ provider: sops
+ secretRef:
+ name: sops-age
+ postBuild:
+ substituteFrom:
+ - kind: ConfigMap
+ name: cluster-config
+ patches:
+ - patch: |-
+ apiVersion: kustomize.toolkit.fluxcd.io/v1
+ kind: Kustomization
+ metadata:
+ name: not-used
+ spec:
+ decryption:
+ provider: sops
+ secretRef:
+ name: sops-age
+ postBuild:
+ substituteFrom:
+ - kind: ConfigMap
+ name: cluster-config
+ target:
+ group: kustomize.toolkit.fluxcd.io
+ kind: Kustomization
+ labelSelector: substitution.flux.home.arpa/disabled notin (true)
\ No newline at end of file
diff --git a/clustertool/embed/generic/root/repositories/git/kustomization.yaml b/clustertool/embed/generic/root/repositories/git/kustomization.yaml
new file mode 100644
index 0000000000000..f2c84de22a00c
--- /dev/null
+++ b/clustertool/embed/generic/root/repositories/git/kustomization.yaml
@@ -0,0 +1,7 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+ - this-repo.yaml
+ - truecharts.yaml
\ No newline at end of file
diff --git a/clustertool/embed/generic/root/repositories/git/this-repo.yaml b/clustertool/embed/generic/root/repositories/git/this-repo.yaml
new file mode 100644
index 0000000000000..285291fe220e4
--- /dev/null
+++ b/clustertool/embed/generic/root/repositories/git/this-repo.yaml
@@ -0,0 +1,20 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/gitrepository_v1.json
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: GitRepository
+metadata:
+ name: cluster
+ namespace: flux-system
+spec:
+ interval: 30m
+ url: ssh://REPLACEWITHGITREPO
+ ref:
+ branch: main
+ secretRef:
+ name: deploy-key
+ ignore: |
+ # exclude all
+ /*
+ # include flux directories
+ !/clusters
+ !/repositories
\ No newline at end of file
diff --git a/clustertool/embed/generic/root/repositories/git/truecharts.yaml b/clustertool/embed/generic/root/repositories/git/truecharts.yaml
new file mode 100644
index 0000000000000..5aca41d15d73d
--- /dev/null
+++ b/clustertool/embed/generic/root/repositories/git/truecharts.yaml
@@ -0,0 +1,19 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/gitrepository_v1.json
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: GitRepository
+metadata:
+ name: truecharts
+ namespace: flux-system
+spec:
+ interval: 30m
+ url: https://github.com/truecharts/public/
+ ref:
+ branch: master
+ secretRef:
+ name: deploy-key
+ ignore: |
+ # exclude all
+ /*
+ # include flux directories
+ !/repositories
\ No newline at end of file
diff --git a/clustertool/embed/generic/root/repositories/kustomization.yaml b/clustertool/embed/generic/root/repositories/kustomization.yaml
new file mode 100644
index 0000000000000..8df91a4f773e0
--- /dev/null
+++ b/clustertool/embed/generic/root/repositories/kustomization.yaml
@@ -0,0 +1,9 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+ - ./git
+ - ./helm
+ - ./oci
+ - ./entries
\ No newline at end of file
diff --git a/clustertool/embed/generic/root/repositories/oci/flux-manifests.yaml b/clustertool/embed/generic/root/repositories/oci/flux-manifests.yaml
new file mode 100644
index 0000000000000..007a57d6d1590
--- /dev/null
+++ b/clustertool/embed/generic/root/repositories/oci/flux-manifests.yaml
@@ -0,0 +1,12 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/ocirepository_v1beta2.json
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: OCIRepository
+metadata:
+ name: flux-manifests
+ namespace: flux-system
+spec:
+ interval: 10m
+ url: oci://ghcr.io/fluxcd/flux-manifests
+ ref:
+ tag: v2.4.0
\ No newline at end of file
diff --git a/clustertool/embed/generic/root/repositories/oci/kustomization.yaml b/clustertool/embed/generic/root/repositories/oci/kustomization.yaml
new file mode 100644
index 0000000000000..c2ddb7efeadbd
--- /dev/null
+++ b/clustertool/embed/generic/root/repositories/oci/kustomization.yaml
@@ -0,0 +1,6 @@
+---
+# yaml-language-server: $schema=https://json.schemastore.org/kustomization
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+ - flux-manifests.yaml
\ No newline at end of file
diff --git a/clustertool/embed/generic/root/truenas_exports/.gitkeep b/clustertool/embed/generic/root/truenas_exports/.gitkeep
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/clustertool/embed/linux_amd64/talosctl-linux-amd64 b/clustertool/embed/linux_amd64/talosctl-linux-amd64
new file mode 100644
index 0000000000000..dccd3ce04f32f
Binary files /dev/null and b/clustertool/embed/linux_amd64/talosctl-linux-amd64 differ
diff --git a/clustertool/embed/linux_arm64/talosctl-linux-arm64 b/clustertool/embed/linux_arm64/talosctl-linux-arm64
new file mode 100644
index 0000000000000..f1b8c8c46d3e6
Binary files /dev/null and b/clustertool/embed/linux_arm64/talosctl-linux-arm64 differ
diff --git a/clustertool/embed/windows_amd64/talosctl-windows-amd64.exe b/clustertool/embed/windows_amd64/talosctl-windows-amd64.exe
new file mode 100644
index 0000000000000..ca47aa14fb3b4
Binary files /dev/null and b/clustertool/embed/windows_amd64/talosctl-windows-amd64.exe differ
diff --git a/clustertool/fix.py b/clustertool/fix.py
new file mode 100644
index 0000000000000..ba337d7082cd7
--- /dev/null
+++ b/clustertool/fix.py
@@ -0,0 +1,63 @@
+import re
+import os
+
+# Path to the directory with Go files
+directory = "."
+
+# Updated regex pattern to find incorrect Msgf statements
+pattern = re.compile(r'log\.Info\(\)\.Msgf\((\"[^\"]*\")((?:,\s*(?:\"[^\"]*\"|\w+))*)\)')
+
+# Function to embed string literals and handle existing %s or %v placeholders
+def replacer(match):
+ message = match.group(1)
+ args = match.group(2).strip().split(',')[1:] # Extract arguments
+
+ formatted_message = message[:-1] # Remove the closing quote for now
+ new_args = []
+ placeholders = []
+
+ # Count the number of existing %s and %v in the message
+ existing_placeholders = re.findall(r'%[sv]', formatted_message)
+
+ for arg in args:
+ arg = arg.strip()
+ if re.match(r'\"[^\"]*\"', arg): # If the argument is a string literal
+ formatted_message += " " + arg[1:-1] # Embed the string literal without quotes
+ else: # If the argument is a variable
+ # Determine which placeholder to use
+ if "str" in arg.lower(): # Customize this check as needed
+ placeholders.append('%s')
+ else:
+ placeholders.append('%v')
+
+ new_args.append(arg)
+
+ # Insert additional placeholders, only if we haven't exceeded existing placeholders
+ for _ in range(len(placeholders)):
+ if len(existing_placeholders) > 0:
+ existing_placeholders.pop(0) # Consume an existing placeholder
+ else:
+ formatted_message += " " + placeholders.pop(0)
+
+ # Close the format string and reassemble the log statement
+ formatted_message += '",'
+
+ # Add any remaining arguments
+ return f'log.Info().Msgf({formatted_message} {", ".join(new_args)})'
+
+# Iterate over Go files and apply the regex replacement
+for subdir, _, files in os.walk(directory):
+ for file in files:
+ if file.endswith(".go"):
+ filepath = os.path.join(subdir, file)
+ with open(filepath, 'r') as f:
+ content = f.read()
+
+ # Apply the replacement
+ new_content = pattern.sub(replacer, content)
+
+ # Write the modified content back to the file
+ with open(filepath, 'w') as f:
+ f.write(new_content)
+
+print("Replacement completed!")
diff --git a/clustertool/go.mod b/clustertool/go.mod
new file mode 100644
index 0000000000000..bb36710a55bc5
--- /dev/null
+++ b/clustertool/go.mod
@@ -0,0 +1,294 @@
+module github.com/truecharts/private/clustertool
+
+go 1.23.0
+
+toolchain go1.23.2
+
+require (
+ filippo.io/age v1.2.0
+ github.com/Masterminds/semver/v3 v3.3.0
+ github.com/beevik/ntp v1.4.3
+ github.com/budimanjojo/talhelper/v3 v3.0.7
+ github.com/fatih/color v1.17.0
+ github.com/getsops/sops/v3 v3.9.1
+ github.com/go-git/go-git/v5 v5.12.0
+ github.com/go-logr/logr v1.4.2
+ github.com/go-logr/zapr v1.3.0
+ github.com/go-playground/validator/v10 v10.22.1
+ github.com/invopop/jsonschema v0.12.0
+ github.com/joho/godotenv v1.5.1
+ github.com/knadh/koanf/parsers/yaml v0.1.0
+ github.com/knadh/koanf/providers/file v1.1.2
+ github.com/knadh/koanf/v2 v2.1.1
+ github.com/leaanthony/debme v1.2.1
+ github.com/rs/zerolog v1.33.0
+ github.com/siderolabs/talos/pkg/machinery v1.8.1
+ github.com/spf13/cobra v1.8.1
+ go.uber.org/zap v1.27.0
+ golang.org/x/crypto v0.28.0
+ gopkg.in/yaml.v3 v3.0.1
+ helm.sh/helm/v3 v3.16.2
+ k8s.io/api v0.31.1
+ k8s.io/apimachinery v0.31.1
+ k8s.io/client-go v0.31.1
+ sigs.k8s.io/controller-runtime v0.19.0
+ sigs.k8s.io/kustomize/api v0.18.0
+ sigs.k8s.io/kustomize/kyaml v0.18.1
+ sigs.k8s.io/yaml v1.4.0
+)
+
+replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16
+
+replace go.mozilla.org/sops/v3 => github.com/getsops/sops/v3 v3.9.1
+
+require (
+ cloud.google.com/go v0.115.1 // indirect
+ cloud.google.com/go/auth v0.9.7 // indirect
+ cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
+ cloud.google.com/go/compute/metadata v0.5.2 // indirect
+ cloud.google.com/go/iam v1.2.1 // indirect
+ cloud.google.com/go/kms v1.20.0 // indirect
+ cloud.google.com/go/longrunning v0.6.1 // indirect
+ cloud.google.com/go/storage v1.43.0 // indirect
+ dario.cat/mergo v1.0.1 // indirect
+ github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.1 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
+ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
+ github.com/BurntSushi/toml v1.4.0 // indirect
+ github.com/MakeNowJust/heredoc v1.0.0 // indirect
+ github.com/Masterminds/goutils v1.1.1 // indirect
+ github.com/Masterminds/sprig/v3 v3.3.0 // indirect
+ github.com/Masterminds/squirrel v1.5.4 // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton // indirect
+ github.com/a8m/envsubst v1.4.2 // indirect
+ github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
+ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
+ github.com/aws/aws-sdk-go-v2 v1.31.0 // indirect
+ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5 // indirect
+ github.com/aws/aws-sdk-go-v2/config v1.27.39 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.17.37 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.25 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.18 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18 // indirect
+ github.com/aws/aws-sdk-go-v2/service/kms v1.36.3 // indirect
+ github.com/aws/aws-sdk-go-v2/service/s3 v1.63.3 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.23.3 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.31.3 // indirect
+ github.com/aws/smithy-go v1.21.0 // indirect
+ github.com/bahlo/generic-list-go v0.2.0 // indirect
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/blang/semver v3.5.1+incompatible // indirect
+ github.com/blang/semver/v4 v4.0.0 // indirect
+ github.com/buger/jsonparser v1.1.1 // indirect
+ github.com/cenkalti/backoff/v4 v4.3.0 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/chai2010/gettext-go v1.0.3 // indirect
+ github.com/cloudflare/circl v1.4.0 // indirect
+ github.com/containerd/containerd v1.7.20 // indirect
+ github.com/containerd/errdefs v0.1.0 // indirect
+ github.com/containerd/go-cni v1.1.10 // indirect
+ github.com/containerd/log v0.1.0 // indirect
+ github.com/containerd/platforms v0.2.1 // indirect
+ github.com/containernetworking/cni v1.2.3 // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
+ github.com/cyphar/filepath-securejoin v0.3.1 // indirect
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/distribution/distribution/v3 v3.0.0-alpha.1 // indirect
+ github.com/distribution/reference v0.6.0 // indirect
+ github.com/docker/cli v27.3.1+incompatible // indirect
+ github.com/docker/distribution v2.8.3+incompatible // indirect
+ github.com/docker/docker v27.3.1+incompatible // indirect
+ github.com/docker/docker-credential-helpers v0.8.2 // indirect
+ github.com/docker/go-connections v0.5.0 // indirect
+ github.com/docker/go-metrics v0.0.1 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/emicklei/go-restful/v3 v3.12.0 // indirect
+ github.com/emirpasic/gods v1.18.1 // indirect
+ github.com/evanphx/json-patch v5.9.0+incompatible // indirect
+ github.com/evanphx/json-patch/v5 v5.9.0 // indirect
+ github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/fsnotify/fsnotify v1.7.0 // indirect
+ github.com/fxamacker/cbor/v2 v2.7.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+ github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe // indirect
+ github.com/ghodss/yaml v1.0.0 // indirect
+ github.com/go-errors/errors v1.5.1 // indirect
+ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
+ github.com/go-git/go-billy/v5 v5.5.0 // indirect
+ github.com/go-gorp/gorp/v3 v3.1.0 // indirect
+ github.com/go-jose/go-jose/v4 v4.0.4 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-openapi/jsonpointer v0.21.0 // indirect
+ github.com/go-openapi/jsonreference v0.21.0 // indirect
+ github.com/go-openapi/swag v0.23.0 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
+ github.com/gobwas/glob v0.2.3 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/google/btree v1.1.2 // indirect
+ github.com/google/cel-go v0.21.0 // indirect
+ github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
+ github.com/google/go-cmp v0.6.0 // indirect
+ github.com/google/gofuzz v1.2.0 // indirect
+ github.com/google/s2a-go v0.1.8 // indirect
+ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
+ github.com/googleapis/gax-go/v2 v2.13.0 // indirect
+ github.com/gookit/filter v1.2.1 // indirect
+ github.com/gookit/goutil v0.6.15 // indirect
+ github.com/gookit/validate v1.5.2 // indirect
+ github.com/gorilla/mux v1.8.1 // indirect
+ github.com/gorilla/websocket v1.5.1 // indirect
+ github.com/gosuri/uitable v0.0.4 // indirect
+ github.com/goware/prefixer v0.0.0-20160118172347-395022866408 // indirect
+ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
+ github.com/hashicorp/errwrap v1.1.0 // indirect
+ github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
+ github.com/hashicorp/go-rootcerts v1.0.2 // indirect
+ github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect
+ github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
+ github.com/hashicorp/go-sockaddr v1.0.7 // indirect
+ github.com/hashicorp/hcl v1.0.1-vault-5 // indirect
+ github.com/hashicorp/vault/api v1.15.0 // indirect
+ github.com/hexops/gotextdiff v1.0.3 // indirect
+ github.com/huandu/xstrings v1.5.0 // indirect
+ github.com/imdario/mergo v1.0.0 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
+ github.com/jmoiron/sqlx v1.4.0 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/josharian/native v1.1.0 // indirect
+ github.com/jsimonetti/rtnetlink/v2 v2.0.2 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/kevinburke/ssh_config v1.2.0 // indirect
+ github.com/klauspost/compress v1.17.9 // indirect
+ github.com/knadh/koanf/maps v0.1.1 // indirect
+ github.com/kylelemons/godebug v1.1.0 // indirect
+ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
+ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/lib/pq v1.10.9 // indirect
+ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-runewidth v0.0.15 // indirect
+ github.com/mdlayher/ethtool v0.1.0 // indirect
+ github.com/mdlayher/genetlink v1.3.2 // indirect
+ github.com/mdlayher/netlink v1.7.2 // indirect
+ github.com/mdlayher/socket v0.5.1 // indirect
+ github.com/miekg/dns v1.1.59 // indirect
+ github.com/mitchellh/copystructure v1.2.0 // indirect
+ github.com/mitchellh/go-homedir v1.1.0 // indirect
+ github.com/mitchellh/go-wordwrap v1.0.1 // indirect
+ github.com/mitchellh/mapstructure v1.5.0 // indirect
+ github.com/mitchellh/reflectwalk v1.0.2 // indirect
+ github.com/moby/locker v1.0.1 // indirect
+ github.com/moby/spdystream v0.4.0 // indirect
+ github.com/moby/term v0.5.0 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.0 // indirect
+ github.com/opencontainers/runtime-spec v1.2.0 // indirect
+ github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
+ github.com/pjbgf/sha1cd v0.3.0 // indirect
+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
+ github.com/prometheus/client_golang v1.20.2 // indirect
+ github.com/prometheus/client_model v0.6.1 // indirect
+ github.com/prometheus/common v0.55.0 // indirect
+ github.com/prometheus/procfs v0.15.1 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/rubenv/sql-migrate v1.7.0 // indirect
+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/ryanuber/go-glob v1.0.0 // indirect
+ github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
+ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
+ github.com/shopspring/decimal v1.4.0 // indirect
+ github.com/siderolabs/crypto v0.4.4 // indirect
+ github.com/siderolabs/gen v0.5.0 // indirect
+ github.com/siderolabs/go-blockdevice v0.4.7 // indirect
+ github.com/siderolabs/go-blockdevice/v2 v2.0.2 // indirect
+ github.com/siderolabs/go-pointer v1.0.0 // indirect
+ github.com/siderolabs/image-factory v0.5.0 // indirect
+ github.com/siderolabs/net v0.4.0 // indirect
+ github.com/siderolabs/protoenc v0.2.1 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/skeema/knownhosts v1.2.2 // indirect
+ github.com/spf13/cast v1.7.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/stoewer/go-strcase v1.3.0 // indirect
+ github.com/urfave/cli v1.22.15 // indirect
+ github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
+ github.com/x448/float16 v0.8.4 // indirect
+ github.com/xanzy/ssh-agent v0.3.3 // indirect
+ github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
+ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
+ github.com/xeipuuv/gojsonschema v1.2.0 // indirect
+ github.com/xlab/treeprint v1.2.0 // indirect
+ go.opencensus.io v0.24.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
+ go.opentelemetry.io/otel v1.30.0 // indirect
+ go.opentelemetry.io/otel/metric v1.30.0 // indirect
+ go.opentelemetry.io/otel/trace v1.30.0 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
+ golang.org/x/mod v0.21.0 // indirect
+ golang.org/x/net v0.29.0 // indirect
+ golang.org/x/oauth2 v0.23.0 // indirect
+ golang.org/x/sync v0.8.0 // indirect
+ golang.org/x/sys v0.26.0 // indirect
+ golang.org/x/term v0.25.0 // indirect
+ golang.org/x/text v0.19.0 // indirect
+ golang.org/x/time v0.6.0 // indirect
+ google.golang.org/api v0.199.0 // indirect
+ google.golang.org/genproto v0.0.0-20240930140551-af27646dc61f // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f // indirect
+ google.golang.org/grpc v1.67.1 // indirect
+ google.golang.org/protobuf v1.34.2 // indirect
+ gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
+ gopkg.in/inf.v0 v0.9.1 // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
+ gopkg.in/warnings.v0 v0.1.2 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
+ k8s.io/apiextensions-apiserver v0.31.1 // indirect
+ k8s.io/apiserver v0.31.1 // indirect
+ k8s.io/cli-runtime v0.31.1 // indirect
+ k8s.io/component-base v0.31.1 // indirect
+ k8s.io/klog/v2 v2.130.1 // indirect
+ k8s.io/kube-openapi v0.0.0-20240709000822-3c01b740850f // indirect
+ k8s.io/kubectl v0.31.1 // indirect
+ k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
+ oras.land/oras-go v1.2.5 // indirect
+ sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
+ sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
+)
diff --git a/clustertool/go.sum b/clustertool/go.sum
new file mode 100644
index 0000000000000..bf3105a292539
--- /dev/null
+++ b/clustertool/go.sum
@@ -0,0 +1,911 @@
+c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0=
+c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w=
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
+cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
+cloud.google.com/go/auth v0.9.7 h1:ha65jNwOfI48YmUzNfMaUDfqt5ykuYIUnSartpU1+BA=
+cloud.google.com/go/auth v0.9.7/go.mod h1:Xo0n7n66eHyOWWCnitop6870Ilwo3PiZyodVkkH1xWM=
+cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
+cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
+cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
+cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
+cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU=
+cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g=
+cloud.google.com/go/kms v1.20.0 h1:uKUvjGqbBlI96xGE669hcVnEMw1Px/Mvfa62dhM5UrY=
+cloud.google.com/go/kms v1.20.0/go.mod h1:/dMbFF1tLLFnQV44AoI2GlotbjowyUfgVwezxW291fM=
+cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc=
+cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0=
+cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
+cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
+dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
+dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+filippo.io/age v1.2.0 h1:vRDp7pUMaAJzXNIWJVAZnEf/Dyi4Vu4wI8S1LBzufhE=
+filippo.io/age v1.2.0/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 h1:DRiANoJTiW6obBQe3SqZizkuV1PEgfiiGivmVocDy64=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0/go.mod h1:qLIye2hwb/ZouqhpSD9Zn3SJipvpEnz1Ywl3VUk9Y0s=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.1 h1:9fXQS/0TtQmKXp8SureKouF+idbQvp7cPUxykiohnBs=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.1/go.mod h1:f+OaoSg0VQYPMqB0Jp2D54j1VHzITYcJaCNwV+k00ts=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
+github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
+github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
+github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
+github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
+github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
+github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
+github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
+github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
+github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
+github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
+github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/Microsoft/hcsshim v0.12.6 h1:qEnZjoHXv+4/s0LmKZWE0/AiZmMWEIkFfWBSf1a0wlU=
+github.com/Microsoft/hcsshim v0.12.6/go.mod h1:ZABCLVcvLMjIkzr9rUGcQ1QA0p0P3Ps+d3N1g2DsFfk=
+github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
+github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
+github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton h1:ZGewsAoeSirbUS5cO8L0FMQA+iSop9xR1nmFYifDBPo=
+github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
+github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
+github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
+github.com/ProtonMail/gopenpgp/v2 v2.7.5 h1:STOY3vgES59gNgoOt2w0nyHBjKViB/qSg7NjbQWPJkA=
+github.com/ProtonMail/gopenpgp/v2 v2.7.5/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g=
+github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg=
+github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY=
+github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY=
+github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
+github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
+github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
+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/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
+github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
+github.com/aws/aws-sdk-go-v2 v1.31.0 h1:3V05LbxTSItI5kUqNwhJrrrY1BAXxXt0sN0l72QmG5U=
+github.com/aws/aws-sdk-go-v2 v1.31.0/go.mod h1:ztolYtaEUtdpf9Wftr31CJfLVjOnD/CVRkKOOYgF8hA=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5 h1:xDAuZTn4IMm8o1LnBZvmrL8JA1io4o3YWNXgohbf20g=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5/go.mod h1:wYSv6iDS621sEFLfKvpPE2ugjTuGlAG7iROg0hLOkfc=
+github.com/aws/aws-sdk-go-v2/config v1.27.39 h1:FCylu78eTGzW1ynHcongXK9YHtoXD5AiiUqq3YfJYjU=
+github.com/aws/aws-sdk-go-v2/config v1.27.39/go.mod h1:wczj2hbyskP4LjMKBEZwPRO1shXY+GsQleab+ZXT2ik=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.37 h1:G2aOH01yW8X373JK419THj5QVqu9vKEwxSEsGxihoW0=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.37/go.mod h1:0ecCjlb7htYCptRD45lXJ6aJDQac6D2NlKGpZqyTG6A=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 h1:C/d03NAmh8C4BZXhuRNboF/DqhBkBCeDiJDcaqIT5pA=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14/go.mod h1:7I0Ju7p9mCIdlrfS+JCgqcYD0VXz/N4yozsox+0o078=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.25 h1:HkpHeZMM39sGtMHVYG1buAg93vhj5d7F81y6G0OAbGc=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.25/go.mod h1:j3Vz04ZjaWA6kygOsZRpmWe4CyGqfqq2u3unDTU0QGA=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 h1:kYQ3H1u0ANr9KEKlGs/jTLrBFPo8P8NaH/w7A01NeeM=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18/go.mod h1:r506HmK5JDUh9+Mw4CfGJGSSoqIiLCndAuqXuhbv67Y=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 h1:Z7IdFUONvTcvS7YuhtVxN99v2cCoHRXOS4mTr0B/pUc=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18/go.mod h1:DkKMmksZVVyat+Y+r1dEOgJEfUeA7UngIHWeKsi0yNc=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.18 h1:OWYvKL53l1rbsUmW7bQyJVsYU/Ii3bbAAQIIFNbM0Tk=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.18/go.mod h1:CUx0G1v3wG6l01tUB+j7Y8kclA8NSqK4ef0YG79a4cg=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 h1:QFASJGfT8wMXtuP3D5CRmMjARHv9ZmzFUMJznHDOY3w=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5/go.mod h1:QdZ3OmoIjSX+8D1OPAzPxDfjXASbBMDsz9qvtyIhtik=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20 h1:rTWjG6AvWekO2B1LHeM3ktU7MqyX9rzWQ7hgzneZW7E=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20/go.mod h1:RGW2DDpVc8hu6Y6yG8G5CHVmVOAn1oV8rNKOHRJyswg=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 h1:Xbwbmk44URTiHNx6PNo0ujDE6ERlsCKJD3u1zfnzAPg=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20/go.mod h1:oAfOFzUB14ltPZj1rWwRc3d/6OgD76R8KlvU3EqM9Fg=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18 h1:eb+tFOIl9ZsUe2259/BKPeniKuz4/02zZFH/i4Nf8Rg=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18/go.mod h1:GVCC2IJNJTmdlyEsSmofEy7EfJncP7DNnXDzRjJ5Keg=
+github.com/aws/aws-sdk-go-v2/service/kms v1.36.3 h1:iHi6lC6LfW6SNvB2bixmlOW3WMyWFrHZCWX+P+CCxMk=
+github.com/aws/aws-sdk-go-v2/service/kms v1.36.3/go.mod h1:OHmlX4+o0XIlJAQGAHPIy0N9yZcYS/vNG+T7geSNcFw=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.63.3 h1:3zt8qqznMuAZWDTDpcwv9Xr11M/lVj2FsRR7oYBt0OA=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.63.3/go.mod h1:NLTqRLe3pUNu3nTEHI6XlHLKYmc8fbHUdMxAB6+s41Q=
+github.com/aws/aws-sdk-go-v2/service/sso v1.23.3 h1:rs4JCczF805+FDv2tRhZ1NU0RB2H6ryAvsWPanAr72Y=
+github.com/aws/aws-sdk-go-v2/service/sso v1.23.3/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3 h1:S7EPdMVZod8BGKQQPTBK+FcX9g7bKR7c4+HxWqHP7Vg=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3/go.mod h1:FnvDM4sfa+isJ3kDXIzAB9GAwVSzFzSy97uZ3IsHo4E=
+github.com/aws/aws-sdk-go-v2/service/sts v1.31.3 h1:VzudTFrDCIDakXtemR7l6Qzt2+JYsVqo2MxBPt5k8T8=
+github.com/aws/aws-sdk-go-v2/service/sts v1.31.3/go.mod h1:yMWe0F+XG0DkRZK5ODZhG7BEFYhLXi2dqGsv6tX0cgI=
+github.com/aws/smithy-go v1.21.0 h1:H7L8dtDRk0P1Qm6y0ji7MCYMQObJ5R9CRpyPhRUkLYA=
+github.com/aws/smithy-go v1.21.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
+github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
+github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
+github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
+github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
+github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
+github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
+github.com/brianvoe/gofakeit/v6 v6.24.0 h1:74yq7RRz/noddscZHRS2T84oHZisW9muwbb8sRnU52A=
+github.com/brianvoe/gofakeit/v6 v6.24.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8=
+github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
+github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
+github.com/budimanjojo/talhelper/v3 v3.0.7 h1:3I1YOsY4/KbvcH67UsfYdnf+KBPaghGauxySi+vflDc=
+github.com/budimanjojo/talhelper/v3 v3.0.7/go.mod h1:PZOabyESfd1CuOZ8S67rJg9X38+Ls7YYMLScaxU89d4=
+github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
+github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80=
+github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
+github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
+github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
+github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
+github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0=
+github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0=
+github.com/containerd/containerd v1.7.20 h1:Sl6jQYk3TRavaU83h66QMbI2Nqg9Jm6qzwX57Vsn1SQ=
+github.com/containerd/containerd v1.7.20/go.mod h1:52GsS5CwquuqPuLncsXwG0t2CiUce+KsNHJZQJvAgR0=
+github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
+github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
+github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM=
+github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0=
+github.com/containerd/go-cni v1.1.10 h1:c2U73nld7spSWfiJwSh/8W9DK+/qQwYM2rngIhCyhyg=
+github.com/containerd/go-cni v1.1.10/go.mod h1:/Y/sL8yqYQn1ZG1om1OncJB1W4zN3YmjfP/ShCzG/OY=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
+github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
+github.com/containernetworking/cni v1.2.3 h1:hhOcjNVUQTnzdRJ6alC5XF+wd9mfGIUaj8FuJbEslXM=
+github.com/containernetworking/cni v1.2.3/go.mod h1:DuLgF+aPd3DzcTQTtp/Nvl1Kim23oFKdm2okJzBQA5M=
+github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/cosi-project/runtime v0.5.5 h1:GFoHnngpg4QVZluAUDwUbCe/sYOYBXKULxL/6DD99pU=
+github.com/cosi-project/runtime v0.5.5/go.mod h1:m+bkfUzKYeUyoqYAQBxdce3bfgncG8BsqcbfKRbvJKs=
+github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
+github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
+github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
+github.com/cyphar/filepath-securejoin v0.3.1 h1:1V7cHiaW+C+39wEfpH6XlLBQo3j/PciWFrgfCLS8XrE=
+github.com/cyphar/filepath-securejoin v0.3.1/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/distribution/distribution/v3 v3.0.0-alpha.1 h1:jn7I1gvjOvmLztH1+1cLiUFud7aeJCIQcgzugtwjyJo=
+github.com/distribution/distribution/v3 v3.0.0-alpha.1/go.mod h1:LCp4JZp1ZalYg0W/TN05jarCQu+h4w7xc7ZfQF4Y/cY=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ=
+github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
+github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
+github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
+github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
+github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
+github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
+github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
+github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4=
+github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
+github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
+github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk=
+github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
+github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
+github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
+github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
+github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
+github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
+github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
+github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
+github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA=
+github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk=
+github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe h1:QKe/kmAYbndxwu91TcjHERsnMh5SgOB1x/qicvOdUJ8=
+github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe/go.mod h1:awFzISqLJoZLm+i9QQ4SgMNHDqljH6jWV0B36V5MrUM=
+github.com/getsops/sops/v3 v3.9.1 h1:wXsqzEsUPVQPcxsvjpwpqqD3DVRe9UZKJ7LSf5rzuLA=
+github.com/getsops/sops/v3 v3.9.1/go.mod h1:k3XzAfcvMI1Rfyw2tky0/RakHrdbVcUrtCGTYsmkMY8=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
+github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
+github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
+github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
+github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
+github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
+github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
+github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
+github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
+github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
+github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
+github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
+github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
+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.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
+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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
+github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+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/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
+github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
+github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
+github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc=
+github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU=
+github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
+github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
+github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM=
+github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
+github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
+github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+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/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
+github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
+github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
+github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
+github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
+github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
+github.com/gookit/filter v1.2.1 h1:37XivkBm2E5qe1KaGdJ5ZfF5l9NYdGWfLEeQadJD8O4=
+github.com/gookit/filter v1.2.1/go.mod h1:rxynQFr793x+XDwnRmJFEb53zDw0Zqx3OD7TXWoR9mQ=
+github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo=
+github.com/gookit/goutil v0.6.15/go.mod h1:qdKdYEHQdEtyH+4fNdQNZfJHhI0jUZzHxQVAV3DaMDY=
+github.com/gookit/validate v1.5.2 h1:i5I2OQ7WYHFRPRATGu9QarR9snnNHydvwSuHXaRWAV0=
+github.com/gookit/validate v1.5.2/go.mod h1:yuPy2WwDlwGRa06fFJ5XIO8QEwhRnTC2LmxmBa5SE14=
+github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
+github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
+github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
+github.com/goware/prefixer v0.0.0-20160118172347-395022866408 h1:Y9iQJfEqnN3/Nce9cOegemcy/9Ai5k3huT6E80F3zaw=
+github.com/goware/prefixer v0.0.0-20160118172347-395022866408/go.mod h1:PE1ycukgRPJ7bJ9a1fdfQ9j8i/cEcRAoLZzbxYpNB/s=
+github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
+github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
+github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
+github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
+github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
+github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
+github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 h1:iBt4Ew4XEGLfh6/bPk4rSYmuZJGizr6/x/AEizP0CQc=
+github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8/go.mod h1:aiJI+PIApBRQG7FZTEBx5GiiX+HbOHilUdNxUZi4eV0=
+github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
+github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
+github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
+github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw=
+github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU=
+github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=
+github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM=
+github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
+github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA=
+github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
+github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
+github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
+github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI=
+github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
+github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+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/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
+github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
+github.com/jsimonetti/rtnetlink/v2 v2.0.2 h1:ZKlbCujrIpp4/u3V2Ka0oxlf4BCkt6ojkvpy3nZoCBY=
+github.com/jsimonetti/rtnetlink/v2 v2.0.2/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
+github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
+github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
+github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w=
+github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY=
+github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w=
+github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI=
+github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM=
+github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
+github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
+github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
+github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
+github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
+github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
+github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY=
+github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
+github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+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-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mdlayher/ethtool v0.1.0 h1:XAWHsmKhyPOo42qq/yTPb0eFBGUKKTR1rE0dVrWVQ0Y=
+github.com/mdlayher/ethtool v0.1.0/go.mod h1:fBMLn2UhfRGtcH5ZFjr+6GUiHEjZsItFD7fSn7jbZVQ=
+github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
+github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
+github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
+github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
+github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
+github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
+github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
+github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
+github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
+github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
+github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
+github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8=
+github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
+github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
+github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
+github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
+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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
+github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
+github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
+github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
+github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
+github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w=
+github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA=
+github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=
+github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA=
+github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI=
+github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
+github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
+github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
+github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
+github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
+github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg=
+github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
+github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
+github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho=
+github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U=
+github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=
+github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ=
+github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY=
+github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
+github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
+github.com/rubenv/sql-migrate v1.7.0 h1:HtQq1xyTN2ISmQDggnh0c9U3JlP8apWh8YO2jzlXpTI=
+github.com/rubenv/sql-migrate v1.7.0/go.mod h1:S4wtDEG1CKn+0ShpTtzWhFpHHI5PvCUtiGI+C+Z2THE=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
+github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
+github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
+github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
+github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
+github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
+github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
+github.com/siderolabs/crypto v0.4.4 h1:Q6EDBMR2Ub2oAZW5Xl8lrKB27bM3Sn8Gkfw3rngco5U=
+github.com/siderolabs/crypto v0.4.4/go.mod h1:hsR3tJ3aaeuhCChsLF4dBd9vlJVPvmhg4vvx2ez4aD4=
+github.com/siderolabs/gen v0.5.0 h1:Afdjx+zuZDf53eH5DB+E+T2JeCwBXGinV66A6osLgQI=
+github.com/siderolabs/gen v0.5.0/go.mod h1:1GUMBNliW98Xeq8GPQeVMYqQE09LFItE8enR3wgMh3Q=
+github.com/siderolabs/go-api-signature v0.3.6 h1:wDIsXbpl7Oa/FXvxB6uz4VL9INA9fmr3EbmjEZYFJrU=
+github.com/siderolabs/go-api-signature v0.3.6/go.mod h1:hoH13AfunHflxbXfh+NoploqV13ZTDfQ1mQJWNVSW9U=
+github.com/siderolabs/go-blockdevice v0.4.7 h1:2bk4WpEEflGxjrNwp57ye24Pr+cYgAiAeNMWiQOuWbQ=
+github.com/siderolabs/go-blockdevice v0.4.7/go.mod h1:4PeOuk71pReJj1JQEXDE7kIIQJPVe8a+HZQa+qjxSEA=
+github.com/siderolabs/go-blockdevice/v2 v2.0.2 h1:GIdOBrCLQ7X9jbr0P/+7paw5SIfp/LL+dx9mTOzmw8w=
+github.com/siderolabs/go-blockdevice/v2 v2.0.2/go.mod h1:74htzCV913UzaLZ4H+NBXkwWlYnBJIq5m/379ZEcu8w=
+github.com/siderolabs/go-pointer v1.0.0 h1:6TshPKep2doDQJAAtHUuHWXbca8ZfyRySjSBT/4GsMU=
+github.com/siderolabs/go-pointer v1.0.0/go.mod h1:HTRFUNYa3R+k0FFKNv11zgkaCLzEkWVzoYZ433P3kHc=
+github.com/siderolabs/image-factory v0.5.0 h1:v1FXZLCcV6xu+6QpgvhDEICxVF7o2VxMjfU0MutkFbo=
+github.com/siderolabs/image-factory v0.5.0/go.mod h1:npJwHOBsI+h+gKdezCyrs7ZHDmkgRnrAK2Cjk1nzv8A=
+github.com/siderolabs/net v0.4.0 h1:1bOgVay/ijPkJz4qct98nHsiB/ysLQU0KLoBC4qLm7I=
+github.com/siderolabs/net v0.4.0/go.mod h1:/ibG+Hm9HU27agp5r9Q3eZicEfjquzNzQNux5uEk0kM=
+github.com/siderolabs/protoenc v0.2.1 h1:BqxEmeWQeMpNP3R6WrPqDatX8sM/r4t97OP8mFmg6GA=
+github.com/siderolabs/protoenc v0.2.1/go.mod h1:StTHxjet1g11GpNAWiATgc8K0HMKiFSEVVFOa/H0otc=
+github.com/siderolabs/talos/pkg/machinery v1.8.1 h1:oeJQmkLNjEG5jxrzPiC2XMQS5dcg1qZ17p5LKcaCbRM=
+github.com/siderolabs/talos/pkg/machinery v1.8.1/go.mod h1:mWTmuUk8G6CdkhUfDmsrIkgPo0G6J5hC/zGazgnyzBg=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
+github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
+github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
+github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
+github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
+github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM=
+github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0=
+github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
+github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
+github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
+github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
+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/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
+github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
+github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
+github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
+github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
+github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
+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=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/contrib/exporters/autoexport v0.46.1 h1:ysCfPZB9AjUlMa1UHYup3c9dAOCMQX/6sxSfPBUoxHw=
+go.opentelemetry.io/contrib/exporters/autoexport v0.46.1/go.mod h1:ha0aiYm+DOPsLHjh0zoQ8W8sLT+LJ58J3j47lGpSLrU=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 h1:hCq2hNMwsegUvPzI7sPOvtO9cqyy5GbWt/Ybp2xrx8Q=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0/go.mod h1:LqaApwGx/oUmzsbqxkzuBvyoPpkxk3JQWnqfVrJ3wCA=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI=
+go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
+go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0 h1:bflGWrfYyuulcdxf14V6n9+CoQcu5SAAdHmDPAJnlps=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0/go.mod h1:qcTO4xHAxZLaLxPd60TdE88rxtItPHgHWqOhOGRr0as=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I=
+go.opentelemetry.io/otel/exporters/prometheus v0.44.0 h1:08qeJgaPC0YEBu2PQMbqU3rogTlyzpjhCI2b58Yn00w=
+go.opentelemetry.io/otel/exporters/prometheus v0.44.0/go.mod h1:ERL2uIeBtg4TxZdojHUwzZfIFlUIjZtxubT5p4h1Gjg=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0 h1:dEZWPjVN22urgYCza3PXRUGEyCB++y1sAqm6guWFesk=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0/go.mod h1:sTt30Evb7hJB/gEk27qLb1+l9n4Tb8HvHkR0Wx3S6CU=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E=
+go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
+go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
+go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo=
+go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
+go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0=
+go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q=
+go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
+go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
+go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
+go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
+golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
+golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
+golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
+golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
+golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
+golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
+golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
+golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.199.0 h1:aWUXClp+VFJmqE0JPvpZOK3LDQMyFKYIow4etYd9qxs=
+google.golang.org/api v0.199.0/go.mod h1:ohG4qSztDJmZdjK/Ar6MhbAmb/Rpi4JHOqagsh90K28=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20240930140551-af27646dc61f h1:mCJ6SGikSxVlt9scCayUl2dMq0msUgmBArqRY6umieI=
+google.golang.org/genproto v0.0.0-20240930140551-af27646dc61f/go.mod h1:xtVODtPkMQRUZ4kqOTgp6JrXQrPevvfCSdk4mJtHUbM=
+google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f h1:jTm13A2itBi3La6yTGqn8bVSrc3ZZ1r8ENHlIXBfnRA=
+google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f/go.mod h1:CLGoBuH1VHxAUXVPP8FfPwPEVJB6lz3URE5mY2SuayE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f h1:cUMEy+8oS78BWIH9OWazBkzbr090Od9tWBNtZHkOhf0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
+google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
+gopkg.in/evanphx/json-patch.v4 v4.12.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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+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=
+gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
+gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
+helm.sh/helm/v3 v3.16.2 h1:Y9v7ry+ubQmi+cb5zw1Llx8OKHU9Hk9NQ/+P+LGBe2o=
+helm.sh/helm/v3 v3.16.2/go.mod h1:SyTXgKBjNqi2NPsHCW5dDAsHqvGIu0kdNYNH9gQaw70=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
+k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
+k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40=
+k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ=
+k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
+k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
+k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c=
+k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM=
+k8s.io/cli-runtime v0.31.1 h1:/ZmKhmZ6hNqDM+yf9s3Y4KEYakNXUn5sod2LWGGwCuk=
+k8s.io/cli-runtime v0.31.1/go.mod h1:pKv1cDIaq7ehWGuXQ+A//1OIF+7DI+xudXtExMCbe9U=
+k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0=
+k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg=
+k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8=
+k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w=
+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-20240709000822-3c01b740850f h1:2sXuKesAYbRHxL3aE2PN6zX/gcJr22cjrsej+W784Tc=
+k8s.io/kube-openapi v0.0.0-20240709000822-3c01b740850f/go.mod h1:UxDHUPsUwTOOxSU+oXURfFBcAS6JwiRXTYqYwfuGowc=
+k8s.io/kubectl v0.31.1 h1:ih4JQJHxsEggFqDJEHSOdJ69ZxZftgeZvYo7M/cpp24=
+k8s.io/kubectl v0.31.1/go.mod h1:aNuQoR43W6MLAtXQ/Bu4GDmoHlbhHKuyD49lmTC8eJM=
+k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
+k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo=
+oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo=
+sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q=
+sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
+sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo=
+sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U=
+sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E=
+sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/clustertool/main.go b/clustertool/main.go
new file mode 100644
index 0000000000000..94c9513e997f2
--- /dev/null
+++ b/clustertool/main.go
@@ -0,0 +1,81 @@
+package main
+
+import (
+ "os"
+ "time"
+
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
+
+ "github.com/truecharts/private/clustertool/cmd"
+ "github.com/truecharts/private/clustertool/embed"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+ ctrllog "sigs.k8s.io/controller-runtime/pkg/log"
+ ctrllogzap "sigs.k8s.io/controller-runtime/pkg/log/zap"
+)
+
+func main() {
+ // Configure zerolog
+ zerolog.DurationFieldUnit = time.Second
+
+ var zerologLevel zerolog.Level
+ var zapLevel zapcore.Level
+
+ // Switch-case for setting the global log level
+ switch os.Getenv("DEBUG") {
+ case "trace":
+ zerologLevel = zerolog.TraceLevel
+ zapLevel = zapcore.DebugLevel // zap does not have a Trace level, use Debug as equivalent
+ case "debug":
+ zerologLevel = zerolog.DebugLevel
+ zapLevel = zapcore.DebugLevel
+ case "warn":
+ zerologLevel = zerolog.WarnLevel
+ zapLevel = zapcore.WarnLevel
+ case "error":
+ zerologLevel = zerolog.ErrorLevel
+ zapLevel = zapcore.ErrorLevel
+ case "fatal":
+ zerologLevel = zerolog.FatalLevel
+ zapLevel = zapcore.FatalLevel
+ case "panic":
+ zerologLevel = zerolog.PanicLevel
+ zapLevel = zapcore.PanicLevel
+ default:
+ zerologLevel = zerolog.InfoLevel
+ zapLevel = zapcore.InfoLevel
+ }
+
+ // Set zerolog level
+ zerolog.SetGlobalLevel(zerologLevel)
+ log.Logger = log.Output(zerolog.ConsoleWriter{
+ Out: os.Stdout,
+ TimeFormat: time.RFC3339,
+ NoColor: true,
+ })
+
+ // Configure zap logger
+ zapConfig := zap.NewProductionConfig()
+ zapConfig.Level = zap.NewAtomicLevelAt(zapLevel)
+ zapLogger, err := zapConfig.Build()
+ if err != nil {
+ panic(err)
+ }
+ defer zapLogger.Sync()
+
+ // Set controller-runtime logger to use zap
+ ctrlLogger := ctrllogzap.New(ctrllogzap.UseDevMode(true), ctrllogzap.Level(zapLevel))
+ ctrllog.SetLogger(ctrlLogger)
+
+ embed.AllToCache()
+
+ helper.CheckSystemTime()
+ helper.CheckReqDomains()
+
+ err = cmd.Execute()
+ if err != nil {
+ log.Fatal().Err(err).Msg("Failed to execute command")
+ }
+}
diff --git a/clustertool/namespace.yaml b/clustertool/namespace.yaml
new file mode 100644
index 0000000000000..fd2a2fc71a1a6
--- /dev/null
+++ b/clustertool/namespace.yaml
@@ -0,0 +1,6 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: hass
+ labels:
+ pod-security.kubernetes.io/enforce: privileged
diff --git a/clustertool/partial_builds/partial_build.sh b/clustertool/partial_builds/partial_build.sh
new file mode 100755
index 0000000000000..0d2ccf390617e
--- /dev/null
+++ b/clustertool/partial_builds/partial_build.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+set -e
+
+# Define target OS and architectures
+declare -A os_targets
+os_targets[linux]=linux
+os_targets[windows]=windows
+os_targets[darwin]=darwin
+os_targets[freebsd]=freebsd
+
+# Define target architectures
+architectures=("amd64" "arm64")
+
+# Ensure embed directories exist
+for os in "${!os_targets[@]}"; do
+ for arch in "${architectures[@]}"; do
+ mkdir -p "./clustertool/embed/${os}_${arch}"
+ done
+done
+
+# Build the precommit binary for each OS and architecture
+for os in "${!os_targets[@]}"; do
+ for arch in "${architectures[@]}"; do
+ echo "Building precommit for $os/$arch"
+
+ # Determine output file name and extension
+ output="./embed/${os}_${arch}/precommit"
+ mkdir "./clustertool/embed/${os}_${arch}/" || echo "mkdir failed or not needed"
+ if [ "$os" == "windows" ]; then
+ output+=".exe"
+ fi
+
+ # Build the binary
+ cd clustertool
+ GOOS=$os GOARCH=$arch go build -o $output ./partial_builds/precommit/main.go
+ ls -l "./embed/${os}_${arch}/" || echo "ls failed"
+ cd -
+ done
+done
\ No newline at end of file
diff --git a/clustertool/partial_builds/precommit/main.go b/clustertool/partial_builds/precommit/main.go
new file mode 100644
index 0000000000000..38f276eb9daa6
--- /dev/null
+++ b/clustertool/partial_builds/precommit/main.go
@@ -0,0 +1,15 @@
+package main
+
+import (
+ "os"
+
+ "github.com/rs/zerolog/log"
+ "github.com/truecharts/private/clustertool/pkg/sops"
+)
+
+func main() {
+ if err := sops.CheckFilesAndReportEncryption(true, true); err != nil {
+ log.Info().Msgf("Error checking files: %v\n", err)
+ os.Exit(1)
+ }
+}
diff --git a/clustertool/pkg/charts/changelog/active_charts.go b/clustertool/pkg/charts/changelog/active_charts.go
new file mode 100644
index 0000000000000..16f3ddddceaee
--- /dev/null
+++ b/clustertool/pkg/charts/changelog/active_charts.go
@@ -0,0 +1,60 @@
+package changelog
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "github.com/rs/zerolog/log"
+)
+
+type ActiveChart struct {
+ Name string
+ Train string
+}
+
+type ActiveCharts struct {
+ items map[string]ActiveChart
+ mu *sync.RWMutex
+}
+
+func (a *ActiveCharts) isActiveChart(chartName string) bool {
+ a.mu.RLock()
+ defer a.mu.RUnlock()
+ _, ok := a.items[chartName]
+ return ok
+}
+
+func (a *ActiveCharts) getActiveChartsWalker(path string, entry os.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if entry.Name() != "Chart.yaml" {
+ return nil
+ }
+ // path = charts///Chart.yaml
+ segLen := len(strings.Split(path, "/"))
+ if segLen < 3 {
+ return fmt.Errorf("path (%s) is not valid. expected at least charts///Chart.yaml", path)
+ }
+ // chart = charts///
+ chart, _ := filepath.Split(path)
+ // chart = charts//
+ chart = strings.TrimSuffix(chart, "/")
+ // train = charts/
+ train := filepath.Dir(chart)
+ // train =
+ train = filepath.Base(train)
+ // chartName =
+ chartName := filepath.Base(chart)
+ a.mu.Lock()
+ if _, ok := a.items[chartName]; !ok {
+ a.items[chartName] = ActiveChart{Name: chartName, Train: train}
+ } else {
+ log.Error().Msgf("chart [%s] already exists in activeCharts", chartName)
+ }
+ a.mu.Unlock()
+ return nil
+}
diff --git a/clustertool/pkg/charts/changelog/changed_data.go b/clustertool/pkg/charts/changelog/changed_data.go
new file mode 100644
index 0000000000000..fac868e9f2829
--- /dev/null
+++ b/clustertool/pkg/charts/changelog/changed_data.go
@@ -0,0 +1,187 @@
+package changelog
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "sort"
+ "sync"
+ "time"
+
+ "github.com/Masterminds/semver/v3"
+ "github.com/go-git/go-git/v5/plumbing/object"
+ "github.com/rs/zerolog/log"
+)
+
+type ChangedData struct {
+ mu *sync.RWMutex `json:"-"`
+ LastCommit string `json:"last_commit"`
+ Charts map[string]*Chart `json:"charts"`
+}
+
+type Chart struct {
+ Versions map[string]*Version `json:"versions"`
+ SortedVersions []string `json:"-"` // Used only for rendering
+ Name string `json:"-"` // Used only for rendering
+ Train string `json:"-"` // Used only for rendering
+}
+
+func (c *Chart) SortVersions(reverse bool) ([]*semver.Version, error) {
+ chartVersions := []*semver.Version{}
+ for key := range c.Versions {
+ semVer, err := semver.NewVersion(key)
+ if err != nil {
+ return nil, err
+ }
+ chartVersions = append(chartVersions, semVer)
+ }
+ // Sort the versions from oldest to newest
+ sort.Slice(chartVersions, func(i, j int) bool {
+ if reverse {
+ return chartVersions[i].GreaterThan(chartVersions[j])
+ }
+ return chartVersions[i].LessThan(chartVersions[j])
+ })
+
+ for _, version := range chartVersions {
+ c.SortedVersions = append(c.SortedVersions, version.String())
+ }
+
+ return chartVersions, nil
+}
+
+func (c *ChangedData) AddOrUpdateChart(chart string, version string, train string, commit *object.Commit) {
+ if c.Charts == nil {
+ c.Charts = make(map[string]*Chart)
+ }
+ _, exists := c.Charts[chart]
+ if !exists {
+ c.Charts[chart] = &Chart{}
+ }
+
+ c.Charts[chart].AddVersion(version, train)
+ c.Charts[chart].Versions[version].AddCommit(commit)
+}
+
+func (c *Chart) AddVersion(version string, train string) {
+ if c.Versions == nil {
+ c.Versions = make(map[string]*Version)
+ }
+ _, exists := c.Versions[version]
+ if exists {
+ return
+ }
+ c.Versions[version] = &Version{
+ Version: version,
+ Train: train,
+ Commits: make(map[string]*Commit),
+ }
+}
+
+type Version struct {
+ Version string `json:"version"`
+ Train string `json:"train"`
+ Commits map[string]*Commit `json:"commits"`
+ SortedCommits []*Commit `json:"-"` // Used only for rendering
+}
+
+func (v *Version) AddCommit(commit *object.Commit) {
+ if v.Commits == nil {
+ v.Commits = make(map[string]*Commit)
+ }
+
+ _, exists := v.Commits[commit.Hash.String()]
+ if exists {
+ return
+ }
+ v.Commits[commit.Hash.String()] = &Commit{
+ CommitHash: commit.Hash.String(),
+ ParentHash: commit.ParentHashes[0].String(),
+ Author: Author{Name: commit.Author.Name, Date: commit.Author.When.Format(dateFormat)},
+ Message: getCommitMessage(commit),
+ Kind: getCommitKind(commit),
+ }
+}
+
+func (v *Version) SortCommits(reverse bool) ([]*Commit, error) {
+ commits := []*Commit{}
+ for _, commit := range v.Commits {
+ commits = append(commits, commit)
+ }
+
+ hasErr := false
+ sort.Slice(commits, func(i, j int) bool {
+ // While we could store the time.Time in the Author struct,
+ // it was giving mixed results as the timezones were different.
+ // The dateFormat we use does not contain timezone, so it sorts better.
+ iDate, err := time.Parse(dateFormat, commits[i].Author.Date)
+ if err != nil {
+ log.Fatal().Err(err).Msgf("Failed to parse date [%s]", commits[i].Author.Date)
+ return false
+ }
+ jDate, err := time.Parse(dateFormat, commits[j].Author.Date)
+ if err != nil {
+ log.Fatal().Err(err).Msgf("Failed to parse date [%s]", commits[j].Author.Date)
+ return false
+ }
+ if reverse {
+ return iDate.After(jDate)
+ }
+ return iDate.Before(jDate)
+ })
+
+ if hasErr {
+ return nil, errors.New("failed to sort commits")
+ }
+
+ v.SortedCommits = commits
+ return commits, nil
+}
+
+type Commit struct {
+ CommitHash string `json:"commit_hash"`
+ ParentHash string `json:"parent_hash"`
+ Author Author `json:"author"`
+ Kind string `json:"kind"`
+ Message string `json:"message"`
+}
+
+type Author struct {
+ Name string `json:"name"`
+ Date string `json:"date"`
+}
+
+func (c *ChangedData) LoadFromFile(path string) error {
+ fileInfo, err := os.Stat(path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil
+ }
+ return err
+ }
+ if fileInfo.IsDir() {
+ return fmt.Errorf("path is a directory")
+ }
+
+ bytes, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+
+ err = json.Unmarshal(bytes, &c)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *ChangedData) WriteToFile(path string) error {
+ data, err := json.MarshalIndent(c, "", " ")
+ if err != nil {
+ return err
+ }
+ log.Info().Msgf("Writing changed data to [%s]", path)
+ return os.WriteFile(path, data, 0644)
+}
diff --git a/clustertool/pkg/charts/changelog/changelog.go b/clustertool/pkg/charts/changelog/changelog.go
new file mode 100644
index 0000000000000..69ebf481fc1eb
--- /dev/null
+++ b/clustertool/pkg/charts/changelog/changelog.go
@@ -0,0 +1,257 @@
+package changelog
+
+import (
+ "fmt"
+ "os"
+ "sync"
+ "time"
+
+ "github.com/Masterminds/semver/v3"
+ "github.com/go-git/go-git/v5"
+ "github.com/rs/zerolog/log"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+type ChangelogOptions struct {
+ RepoPath string // Path to the repository (eg "./charts")
+ TemplatePath string // Path to the template file (eg "./changelog.tmpl")
+ ChangelogFileName string // Name of the changelog file eg "CHANGELOG.md"
+ JSONOutputPath string // Path to the JSON output file
+ PrettyJSON bool // If true, the JSON output will be pretty-printed
+ ChartsDir string // Dir where the charts are located (eg "./charts/")
+ StatusUpdateInterval int // Interval in seconds between status updates
+ SkipCommitsWithBadMessage bool // If true, commits with bad messages will be skipped
+}
+
+func checkPath(path string, createIfNotExist bool) error {
+ _, err := os.Stat(path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ if createIfNotExist {
+ _, err := os.Create(path)
+ if err != nil {
+ return fmt.Errorf("cannot create path %s: %w", path, err)
+ }
+ return nil
+ }
+ return nil
+ }
+ return fmt.Errorf("path %s cannot be used: %w", path, err)
+ }
+ return nil
+}
+
+func (o *ChangelogOptions) validate() error {
+ if o.RepoPath == "" {
+ return fmt.Errorf("repo path is empty")
+ }
+ if o.TemplatePath == "" {
+ return fmt.Errorf("template path is empty")
+ }
+ if o.ChangelogFileName == "" {
+ return fmt.Errorf("changelog file name is empty")
+ }
+ if o.ChartsDir == "" {
+ return fmt.Errorf("charts dir is empty")
+ }
+ if o.JSONOutputPath == "" {
+ return fmt.Errorf("json output path is empty")
+ }
+ if o.StatusUpdateInterval <= 0 {
+ return fmt.Errorf("status update interval is zero")
+ }
+
+ paths := map[string]bool{
+ o.TemplatePath: false,
+ o.RepoPath: false,
+ o.ChartsDir: false,
+ o.JSONOutputPath: false,
+ }
+
+ for path, create := range paths {
+ if err := checkPath(path, create); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+var changedData ChangedData = ChangedData{mu: &sync.RWMutex{}, Charts: make(map[string]*Chart)}
+var stagingData ChangedData = ChangedData{mu: &sync.RWMutex{}, Charts: make(map[string]*Chart)}
+var activeCharts ActiveCharts = ActiveCharts{items: make(map[string]ActiveChart), mu: &sync.RWMutex{}}
+var currentStatus status = status{processedCount: 0, totalCount: 0, skippedCount: 0, avgTime: 0, totalProcessingTime: 0, mu: &sync.RWMutex{}}
+var skipCommitsWithBadMessage bool
+var dateFormat = "2006-01-02"
+
+func (o *ChangelogOptions) Generate() error {
+ start := time.Now()
+ skipCommitsWithBadMessage = o.SkipCommitsWithBadMessage
+ log.Info().Msgf("Starting changelog generation at %s", start)
+ if err := o.validate(); err != nil {
+ return err
+ }
+ // Get active train and charts
+ if err := helper.WalkCharts2([]string{o.RepoPath}, activeCharts.getActiveChartsWalker, helper.AsyncMode); err != nil {
+ return err
+ }
+ log.Info().Msgf("Found [%d] active charts in [%s]", len(activeCharts.items), time.Since(start))
+
+ // Load existing json file
+ if err := changedData.LoadFromFile(o.JSONOutputPath); err != nil {
+ return fmt.Errorf("failed to load existing json file, maybe it is not matching the current structure: %w", err)
+ }
+ if changedData.LastCommit == "" {
+ log.Info().Msgf("No last commit found in [%s], starting from the beginning", o.JSONOutputPath)
+ } else {
+ log.Info().Msgf("Last commit found in [%s], will start from [%s]", o.JSONOutputPath, changedData.LastCommit)
+ }
+
+ // Open repo
+ repo, err := git.PlainOpen(o.RepoPath)
+ if err != nil {
+ return err
+ }
+
+ // Get list of commits. Order by committer time and only keep commits that are in the charts dir
+ // the iterator will yield the newer commits first, so we need to reverse the order
+ cIter, err := repo.Log(&git.LogOptions{Order: git.LogOrderCommitterTime})
+ if err != nil {
+ return err
+ }
+ commits, err := o.reverseCommits(cIter, changedData.LastCommit)
+ if err != nil {
+ return err
+ }
+ if len(commits) == 0 {
+ log.Info().Msgf("No commits to process in %s", o.RepoPath)
+ return nil
+ }
+ log.Info().Msgf("Found [%d] commits to process in %s", len(commits), o.RepoPath)
+
+ stop := make(chan struct{}) // Stop channel
+ defer close(stop)
+ go o.statusPrinter(stop)
+
+ // TODO: Once go-git is thread safe, we can parallelize this
+ // https://github.com/go-git/go-git/issues/773
+ for _, c := range commits {
+ changedData.mu.Lock()
+ changedData.LastCommit = c.Hash.String()
+ changedData.mu.Unlock()
+ commitStart := time.Now()
+
+ if err := processCommit(c); err != nil {
+ log.Error().Err(err).Msgf("Error processing commit: %s", c.Hash.String())
+ return err
+ }
+
+ currentStatus.mu.Lock()
+ currentStatus.processedCount++
+ currentStatus.totalProcessingTime += time.Since(commitStart)
+ currentStatus.avgTime = currentStatus.totalProcessingTime / time.Duration(currentStatus.processedCount+currentStatus.skippedCount)
+ currentStatus.mu.Unlock()
+ }
+
+ stop <- struct{}{}
+
+ if err := mergeStagingToCurrent(); err != nil {
+ return err
+ }
+ if err := changedData.WriteToFile(o.JSONOutputPath); err != nil {
+ return fmt.Errorf("error writing json new file: %s", err)
+ }
+ log.Info().Msgf("Finished in %s", time.Since(start))
+ o.printStatus(start, false)
+ return nil
+}
+
+// We have to go over the stagingData, for each chart,
+// we sort the versions from the changelogData
+// and we add the commits from stagingData to the nearest next version in changelogData
+func mergeStagingToCurrent() error {
+ start := time.Now()
+ log.Info().Msgf("Merging staging to current", )
+ changedData.mu.Lock()
+ defer changedData.mu.Unlock()
+
+ stagingData.mu.Lock()
+ defer stagingData.mu.Unlock()
+ for chart, stagingChartItem := range stagingData.Charts {
+ // If the staging chart doesn't exist in the changelogData, we add it and go to the next chart
+ chartItem, ok := changedData.Charts[chart]
+ if !ok {
+ changedData.Charts[chart] = stagingChartItem
+ continue
+ }
+
+ // If the chart exists in the changelogData but does not have any versions
+ // we add the versions from stagingData and go to the next chart (probably a new chart)
+ if chartItem.Versions == nil || len(chartItem.Versions) == 0 {
+ chartItem.Versions = stagingChartItem.Versions
+ continue
+ }
+
+ // Get all the versions from the changedData chart
+ chartVersions, err := chartItem.SortVersions(false)
+ if err != nil {
+ return err
+ }
+
+ // Go over the versions in stagingData chart,
+ // for each version, we find the immediately next version in changedData chart
+ for versionKey := range stagingData.Charts[chart].Versions {
+ stagingVer, err := semver.NewVersion(versionKey)
+ if err != nil { // This should never happen
+ return err
+ }
+
+ foundGreater := false
+ // Go over the versions in the changedData chart versions
+ for _, chartVer := range chartVersions {
+ // If the changedData version is greater than the staging version,
+ // we add the commits to this version and break
+ if !chartVer.GreaterThan(stagingVer) {
+ continue
+ }
+ foundGreater = true
+ chartVerItem, ok := chartItem.Versions[versionKey]
+ if !ok {
+ chartItem.AddVersion(versionKey, stagingChartItem.Versions[versionKey].Train)
+ chartVerItem = chartItem.Versions[versionKey]
+ }
+
+ // Add the commits from stagingData to the given version in the changedData chart
+ for commitKey, commit := range stagingChartItem.Versions[versionKey].Commits {
+ if chartVerItem.Commits == nil {
+ log.Warn().Msgf("Commits were nil for version [%s] in chart [%s]", versionKey, chart)
+ chartVerItem.Commits = make(map[string]*Commit)
+ }
+
+ if _, ok := chartVerItem.Commits[commitKey]; ok {
+ // This should never happen, but we log it just in case
+ log.Warn().Msgf("Commit [%s] already exists in version [%s]", commitKey, versionKey)
+ continue
+ }
+ chartVerItem.Commits[commitKey] = commit
+ }
+ break
+ }
+ if !foundGreater {
+ // Add the version to the changedData chart
+ for commitKey, commit := range stagingChartItem.Versions[versionKey].Commits {
+ if _, ok := chartItem.Versions[versionKey].Commits[commitKey]; ok {
+ // This should never happen, but we log it just in case
+ log.Warn().Msgf("Commit [%s] already exists in version [%s]", commitKey, versionKey)
+ continue
+ }
+ chartItem.Versions[versionKey].Commits[commitKey] = commit
+ }
+ }
+ }
+
+ }
+
+ log.Info().Msgf("Finished merging in %s", time.Since(start))
+ return nil
+}
diff --git a/clustertool/pkg/charts/changelog/commit.go b/clustertool/pkg/charts/changelog/commit.go
new file mode 100644
index 0000000000000..16b877d9a5631
--- /dev/null
+++ b/clustertool/pkg/charts/changelog/commit.go
@@ -0,0 +1,89 @@
+package changelog
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/go-git/go-git/v5/plumbing/format/diff"
+ "github.com/go-git/go-git/v5/plumbing/object"
+ "github.com/rs/zerolog/log"
+)
+
+// getCommitMessage returns the first line of the commit message
+func getCommitMessage(c *object.Commit) string {
+ return strings.TrimSpace(strings.Split(c.Message, "\n")[0])
+}
+
+// TODO: update regex
+var commitMessageRegex = regexp.MustCompile(`^(chore|feat|fix|docs)\((.+)\)?: (.+)`)
+
+func getCommitKind(c *object.Commit) string {
+ match := commitMessageRegex.FindStringSubmatch(getCommitMessage(c))
+ if match == nil {
+ return ""
+ }
+ return match[1]
+}
+
+func isValidCommit(c *object.Commit) bool {
+ if c.Message == "" {
+ currentStatus.incSkippedCount()
+ log.Debug().Msgf("Skipping commit [%s]. Reason: the commit message is empty", c.Hash.String())
+ return false
+ }
+ if c.ParentHashes == nil || len(c.ParentHashes) == 0 {
+ currentStatus.incSkippedCount()
+ log.Debug().Msgf("Skipping commit [%s]. Reason: the commit is Batman (has no parent)", c.Hash.String())
+ return false
+ }
+
+ if skipCommitsWithBadMessage && !commitMessageRegex.MatchString(getCommitMessage(c)) {
+ currentStatus.incSkippedCount()
+ log.Debug().Msgf("Skipping commit [%s]. Reason: the commit message does not match the pattern", c.Hash.String())
+ return false
+ }
+
+ return true
+}
+
+type oldNewPaths struct {
+ old diff.File
+ new diff.File
+}
+type chartsWithChangedFiles map[string][]oldNewPaths
+type chartsWithChangedFile map[string]oldNewPaths
+
+func processCommit(c *object.Commit) error {
+ var err error
+ if !isValidCommit(c) {
+ return nil
+ }
+
+ parCommit, err := c.Parent(0)
+ if err != nil {
+ return fmt.Errorf("failed to get parent commit: %w", err)
+ }
+ patch, err := parCommit.Patch(c)
+ if err != nil {
+ return fmt.Errorf("failed to get patch: %w", err)
+ }
+
+ // Go over the filePatches (old/new pairs) and get create a
+ // map of charts with an slice of all the old/new fileDiffs
+ chartsWithMultipleFiles, err := getChartsWithMultipleChangedFiles(patch)
+ if err != nil {
+ return fmt.Errorf("failed to get changed files: %w", err)
+ }
+
+ // For each chart, keep a single old/new pair, preferably the chart.yaml
+ // otherwise the first file in the list, doesn't matter
+ chartsWithSingleFile := getChartsWithSingleChangedFile(chartsWithMultipleFiles)
+
+ // Populate the changedData and stagingData
+ if err := processChartsWithSingleChangedFile(c, parCommit, chartsWithSingleFile); err != nil {
+ return fmt.Errorf("failed to process changed file: %w", err)
+ }
+
+ return nil
+}
diff --git a/clustertool/pkg/charts/changelog/patch.go b/clustertool/pkg/charts/changelog/patch.go
new file mode 100644
index 0000000000000..6dafad57a3ed6
--- /dev/null
+++ b/clustertool/pkg/charts/changelog/patch.go
@@ -0,0 +1,155 @@
+package changelog
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/Masterminds/semver/v3"
+ "github.com/go-git/go-git/v5/plumbing/format/diff"
+ "github.com/go-git/go-git/v5/plumbing/object"
+ "github.com/rs/zerolog/log"
+)
+
+var errSkipPatch = errors.New("skip patch")
+
+func getChangedFilePair(p diff.FilePatch) (string, oldNewPaths, error) {
+ old, new := p.Files()
+ if new == nil { // No new file, nothing to do
+ log.Debug().Msgf("Skipping file patch. Reason: New file is empty")
+ return "", oldNewPaths{}, errSkipPatch
+ }
+
+ // Get chart name and check if its an active chart
+ // if the new.Path() is a path outside of the charts folder,
+ // it will not be an active chart anyway and so we skip the diff
+ chartName := getChartName(new.Path())
+ if chartName == invalidName || !activeCharts.isActiveChart(chartName) {
+ log.Debug().Msgf("Skipping file patch. Reason: [%s] is not an active chart", new.Path())
+ return "", oldNewPaths{}, errSkipPatch
+ }
+ if _, err := getChartPath(new.Path()); err != nil {
+ log.Debug().Msgf("Skipping file patch. Reason: [%s] is not a valid chart path", new.Path())
+ return "", oldNewPaths{}, errSkipPatch
+ }
+ if old != nil { // If an old file exists in the patch
+ if _, err := getChartPath(old.Path()); err != nil {
+ log.Debug().Msgf("Skipping file patch. Reason: [%s] is not a valid chart path", old.Path())
+ return "", oldNewPaths{}, errSkipPatch
+ }
+ }
+
+ return chartName, oldNewPaths{new: new, old: old}, nil
+}
+
+func getOldAndNewVersion(c *object.Commit, par *object.Commit, paths oldNewPaths) (string, string, error) {
+ newChartPath, err := getChartPath(paths.new.Path())
+ if err != nil {
+ return "", "", fmt.Errorf("failed to get chart path from file path [%s]: %w", paths.new.Path(), err)
+ }
+ newChartVer, err := getChartVersion(c, newChartPath)
+ if err != nil {
+ return "", "", fmt.Errorf("failed to get chart data from path [%s]: %w", newChartPath, err)
+ }
+
+ oldChartVer := ""
+ if paths.old != nil { // If an old file exists in the patch
+ oldChartPath, err := getChartPath(paths.old.Path())
+ if err != nil {
+ return "", "", fmt.Errorf("failed to get chart path from file path [%s]: %w", paths.old.Path(), err)
+ }
+ // Note here we pass the parent commit, not the current commit
+ oldChartVer, err = getChartVersion(par, oldChartPath)
+ if err != nil {
+ return "", "", fmt.Errorf("failed to get chart data from path [%s]: %w", oldChartPath, err)
+ }
+ }
+
+ return oldChartVer, newChartVer, nil
+}
+
+func getChartsWithMultipleChangedFiles(p *object.Patch) (chartsWithChangedFiles, error) {
+ chartsWithMultipleFiles := make(chartsWithChangedFiles)
+ for _, p := range p.FilePatches() {
+ // Get chart name and the "new" file path
+ chartName, paths, err := getChangedFilePair(p)
+ if err != nil {
+ if errors.Is(err, errSkipPatch) {
+ continue
+ }
+ return chartsWithChangedFiles{}, fmt.Errorf("failed to get changed files: %w", err)
+ }
+ // if there is no new file, skip the filePatch
+ if paths.new.Path() == "" {
+ continue
+ }
+
+ // Add the file to the charts changed files
+ chartsWithMultipleFiles[chartName] = append(chartsWithMultipleFiles[chartName], paths)
+ }
+
+ return chartsWithMultipleFiles, nil
+}
+
+func getChartsWithSingleChangedFile(c chartsWithChangedFiles) chartsWithChangedFile {
+ chartsWithSingleFile := make(chartsWithChangedFile)
+ for chartName, filePaths := range c {
+ for _, paths := range filePaths {
+ _, ok := chartsWithSingleFile[chartName]
+ // If the chart hasn't been seen before,
+ // or the filePath is a Chart.yaml file
+ // we add the pair to the map
+ if !ok || strings.HasSuffix(paths.new.Path(), "Chart.yaml") {
+ chartsWithSingleFile[chartName] = paths
+ continue
+ }
+ }
+ }
+ return chartsWithSingleFile
+}
+
+func processChartsWithSingleChangedFile(c *object.Commit, par *object.Commit, chartsWithSingleFile chartsWithChangedFile) error {
+ // For each chart, get the old and new versions
+ for chartName, paths := range chartsWithSingleFile {
+ oldVer, newVer, err := getOldAndNewVersion(c, par, paths)
+ if err != nil {
+ return fmt.Errorf("failed to get old and new versions: %w", err)
+ }
+ // If the old version is empty, (chart addition)
+ // we add the new version to the changedData
+ if oldVer == "" {
+ changedData.mu.Lock()
+ changedData.AddOrUpdateChart(chartName, newVer, getChartTrain(paths.new.Path()), c)
+ changedData.mu.Unlock()
+ continue
+ }
+
+ oldSemVer, err := semver.NewVersion(oldVer)
+ if err != nil {
+ return fmt.Errorf("failed to parse old version ([%s]) for file [%s] in commit [%s]: %w", oldVer, paths.new.Path(), c.Hash.String(), err)
+ }
+ newSemVer, err := semver.NewVersion(newVer)
+ if err != nil {
+ return fmt.Errorf("failed to parse new version ([%s]) for file [%s] in commit [%s]: %w", newVer, paths.new.Path(), c.Hash.String(), err)
+ }
+
+ // if new version is greater than the old version, we add the new version to the changedData
+ if newSemVer.GreaterThan(oldSemVer) {
+ changedData.mu.Lock()
+ changedData.AddOrUpdateChart(chartName, newVer, getChartTrain(paths.new.Path()), c)
+ changedData.mu.Unlock()
+ continue
+ }
+
+ // Otherwise, we add the new version to the stagingData
+ // It is probably less or equal to the old version,
+ // in either case the chart changes is unreleased.
+ // so it should go to the "next" version, we do that at the end
+ // although if its less, it will be hard to actually get which is the "next" version
+ // but we can't really do anything about it, so just put it on the immediate next version
+ stagingData.mu.Lock()
+ stagingData.AddOrUpdateChart(chartName, newVer, getChartTrain(paths.new.Path()), c)
+ stagingData.mu.Unlock()
+ }
+ return nil
+}
diff --git a/clustertool/pkg/charts/changelog/render.go b/clustertool/pkg/charts/changelog/render.go
new file mode 100644
index 0000000000000..3df9180d13711
--- /dev/null
+++ b/clustertool/pkg/charts/changelog/render.go
@@ -0,0 +1,75 @@
+package changelog
+
+import (
+ "bytes"
+ "html/template"
+ "os"
+ "path/filepath"
+ "sync"
+ "time"
+
+ "github.com/rs/zerolog/log"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+func (o *ChangelogOptions) Render() error {
+ start := time.Now()
+ log.Info().Msgf("Starting changelog render at %s", start)
+
+ changelogData := ChangedData{mu: &sync.RWMutex{}, Charts: make(map[string]*Chart)}
+ activeCharts := ActiveCharts{items: make(map[string]ActiveChart), mu: &sync.RWMutex{}}
+ if err := changelogData.LoadFromFile(o.JSONOutputPath); err != nil {
+ log.Fatal().Err(err).Msgf("failed to load %s", o.JSONOutputPath)
+ }
+ if err := helper.WalkCharts2([]string{o.RepoPath}, activeCharts.getActiveChartsWalker, helper.AsyncMode); err != nil {
+ log.Fatal().Err(err).Msg("failed to walk charts")
+ }
+
+ for _, chart := range activeCharts.items {
+ if changelogData.Charts[chart.Name] == nil {
+ log.Error().Msgf("chart [%s] not found in %s", chart.Name, o.JSONOutputPath)
+ continue
+
+ }
+ if changelogData.Charts[chart.Name].Versions == nil {
+ log.Error().Msgf("chart [%s] has no versions in %s", chart.Name, o.JSONOutputPath)
+ continue
+ }
+ // load template
+ tmpl, err := template.ParseFiles(o.TemplatePath)
+ if err != nil {
+ log.Fatal().Err(err).Msgf("failed to parse %s", o.TemplatePath)
+ }
+
+ if _, err := changelogData.Charts[chart.Name].SortVersions(true); err != nil {
+ log.Fatal().Err(err).Msgf("failed to sort versions for %s", chart.Name)
+ }
+ for _, version := range changelogData.Charts[chart.Name].Versions {
+ version.SortedCommits, err = version.SortCommits(true)
+ if err != nil {
+ log.Fatal().Err(err).Msgf("failed to sort commits for version [%s] in chart [%s]", version.Version, chart.Name)
+ }
+ }
+
+ changelogData.Charts[chart.Name].Name = chart.Name
+ changelogData.Charts[chart.Name].Train = chart.Train
+ // render template
+ var buf bytes.Buffer
+ err = tmpl.Execute(&buf, changelogData.Charts[chart.Name])
+ if err != nil {
+ log.Fatal().Err(err).Msgf("failed to render %s", o.TemplatePath)
+ }
+
+ output := filepath.Join(o.ChartsDir, chart.Train, chart.Name)
+ if err := os.MkdirAll(output, os.ModePerm); err != nil {
+ log.Fatal().Err(err).Msgf("failed to create %s directory", output)
+ }
+ // write rendered template to file
+ if err := os.WriteFile(filepath.Join(output, o.ChangelogFileName), buf.Bytes(), 0644); err != nil {
+ log.Fatal().Err(err).Msgf("failed to write %s", o.ChangelogFileName)
+ }
+ }
+
+ log.Info().Msgf("Finished in %s", time.Since(start))
+ return nil
+}
diff --git a/clustertool/pkg/charts/changelog/utils.go b/clustertool/pkg/charts/changelog/utils.go
new file mode 100644
index 0000000000000..4374a1f9461b6
--- /dev/null
+++ b/clustertool/pkg/charts/changelog/utils.go
@@ -0,0 +1,154 @@
+package changelog
+
+import (
+ "errors"
+ "fmt"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/go-git/go-git/v5/plumbing/object"
+ "github.com/rs/zerolog/log"
+)
+
+type status struct {
+ processedCount int
+ totalCount int
+ skippedCount int
+ avgTime time.Duration
+ totalProcessingTime time.Duration
+ mu *sync.RWMutex
+}
+
+func (s *status) incSkippedCount() {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.skippedCount++
+ s.processedCount--
+}
+
+func (o *ChangelogOptions) printStatus(start time.Time, eta bool) {
+ currentStatus.mu.RLock()
+ defer currentStatus.mu.RUnlock()
+ if eta {
+ log.Info().Msgf("Processed [%d + (%d skipped) / %d] commits in %s, ETA: %s (avg commit processing time: %s)", currentStatus.processedCount, currentStatus.skippedCount, currentStatus.totalCount, time.Since(start), time.Duration(currentStatus.totalCount-currentStatus.processedCount-currentStatus.skippedCount)*currentStatus.avgTime, currentStatus.avgTime)
+ } else {
+ log.Info().Msgf("Processed [%d + (%d skipped) / %d] commits in %s", currentStatus.processedCount, currentStatus.skippedCount, currentStatus.totalCount, time.Since(start))
+ }
+}
+
+func (o *ChangelogOptions) statusPrinter(stop <-chan struct{}) {
+ log.Info().Msgf("Printing status every [%d] seconds", o.StatusUpdateInterval)
+ start := time.Now()
+ ticker := time.NewTicker(time.Second * time.Duration(o.StatusUpdateInterval))
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ticker.C:
+ o.printStatus(start, true)
+ case <-stop:
+ return
+ }
+ }
+}
+
+func (o *ChangelogOptions) reverseCommits(cIter object.CommitIter, lastCommit string) ([]*object.Commit, error) {
+ start := time.Now()
+ var commits []*object.Commit
+ var errDoneReversing = errors.New("done reversing commits")
+ log.Info().Msgf("Reversing commits order", )
+ defer cIter.Close()
+ if err := cIter.ForEach(func(c *object.Commit) error {
+ // We go from newer to oldest, if we hit the last commit, we stop
+ if c.Hash.String() == lastCommit {
+ return errDoneReversing
+ }
+
+ currentStatus.totalCount++
+ // Reverse the order of the commits to get the oldest first
+ commits = append([]*object.Commit{c}, commits...)
+ return nil
+ }); err != nil {
+ if !errors.Is(err, errDoneReversing) {
+ return nil, err
+ }
+ }
+
+ log.Info().Msgf("Finished reversing commits in %s", time.Since(start))
+ return commits, nil
+}
+
+// Just some random text to avoid any chart name conflicts
+var invalidName = "5fdad45c8f5b954e5643c314"
+
+func getChartName(path string) string {
+ // path = charts///...
+ parts := strings.Split(path, "/")
+ if len(parts) < 3 {
+ log.Debug().Msgf("failed to get chart name from path [%s]", path)
+ return invalidName
+ }
+ return parts[2]
+}
+
+var chartFilePathRegex = regexp.MustCompile(`^charts/([\w-_]+)/([\w-_]+)/Chart.yaml$`)
+
+func getChartPath(path string) (string, error) {
+ original := path
+ for {
+ if path == "." {
+ return "", fmt.Errorf("path too short [%s], or could not construct chart path", original)
+ }
+ if chartFilePathRegex.MatchString(filepath.Join(path, "Chart.yaml")) {
+ return filepath.Join(path, "Chart.yaml"), nil
+ }
+ // Remove the last part of the path and try again
+ path = filepath.Dir(path)
+ }
+}
+
+func getChartVersion(c *object.Commit, path string) (string, error) {
+ tree, err := c.Tree()
+ if err != nil {
+ return "", fmt.Errorf("failed to get tree: %w", err)
+ }
+ file, err := tree.File(path)
+ if err != nil {
+ return "", fmt.Errorf("failed to get file: %w", err)
+ }
+ strData, err := file.Contents()
+ if err != nil {
+ return "", fmt.Errorf("failed to get file contents: %w", err)
+ }
+ return getVersion(strData)
+}
+
+var charsToRemove = []string{"-"}
+
+// We use this instead of NewHelmChart.Load(), because
+// this will work even if the Chart.yaml is malformed
+func getVersion(strData string) (string, error) {
+ lines := strings.Split(strData, "\n")
+ for _, line := range lines {
+ if strings.HasPrefix(line, "version:") {
+ ver := strings.TrimSpace(strings.Split(line, ":")[1])
+ // In some cases there was a type in the version (eg "1.0.-2")
+ for _, c := range charsToRemove {
+ ver = strings.ReplaceAll(ver, c, "")
+ }
+ return ver, nil
+ }
+ }
+ return "", fmt.Errorf("could not find version in file")
+}
+
+func getChartTrain(path string) string {
+ parts := strings.Split(path, "/")
+ if len(parts) < 2 {
+ log.Error().Msgf("Could not get chart train from path [%s]", path)
+ return ""
+ }
+ return parts[1]
+}
diff --git a/clustertool/pkg/charts/chartFile/chart_file.go b/clustertool/pkg/charts/chartFile/chart_file.go
new file mode 100644
index 0000000000000..6c117e27fa725
--- /dev/null
+++ b/clustertool/pkg/charts/chartFile/chart_file.go
@@ -0,0 +1,227 @@
+package chartFile
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+
+ "github.com/go-playground/validator/v10"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+
+ "github.com/knadh/koanf/parsers/yaml"
+ "github.com/knadh/koanf/providers/file"
+ "github.com/knadh/koanf/v2"
+)
+
+const (
+ minHelmVersion = "3.11"
+ maxHelmVersion = "3.15"
+ kubeVersion = ">=1.24.0-0"
+ apiVersion = "v2"
+ chartType = "application"
+ maintainerName = "TrueCharts"
+ maintainerEmail = "info@truecharts.org"
+ maintainerURL = "https://truecharts.org"
+ defaultCategory = "unsorted"
+ defaultAppVersion = "unknown"
+ defaultDescription = "No description provided."
+ defaultHome = "https://truecharts.org"
+ defaultIcon = "https://github.com/truecharts/website/blob/main/static/svg/logo.svg"
+)
+
+var validate *validator.Validate
+
+// Maintainer represents a maintainer of the Helm chart.
+type Maintainer struct {
+ Name string `yaml:"name" validate:"required"`
+ Email string `yaml:"email"`
+ URL string `yaml:"url" validate:"required"`
+}
+
+// Dependency represents a dependency of the Helm chart.
+type Dependency struct {
+ Name string `yaml:"name" validate:"required"`
+ Version string `yaml:"version" validate:"required"`
+ Repository string `yaml:"repository" validate:"required"`
+ Condition string `yaml:"condition"`
+ Alias string `yaml:"alias"`
+ Tags []string `yaml:"tags"`
+ ImportValues []string `yaml:"import-values"`
+}
+
+// ChartMetadata represents the metadata structure in Chart.yaml.
+type ChartMetadata struct {
+ Annotations map[string]string `yaml:"annotations"`
+ APIVersion string `yaml:"apiVersion" validate:"required"`
+ AppVersion string `yaml:"appVersion" validate:"required"`
+ Dependencies []Dependency `yaml:"dependencies"`
+ Deprecated bool `yaml:"deprecated"`
+ Description string `yaml:"description" validate:"required"`
+ Home string `yaml:"home" validate:"required"`
+ Icon string `yaml:"icon" validate:"required"`
+ Keywords []string `yaml:"keywords"`
+ KubeVersion string `yaml:"kubeVersion" validate:"required"`
+ Maintainers []Maintainer `yaml:"maintainers" validate:"required,dive"`
+ Name string `yaml:"name" validate:"required"`
+ Sources []string `yaml:"sources"`
+ Type string `yaml:"type"`
+ Version string `yaml:"version" validate:"required"`
+ // Add other fields as needed
+}
+
+// HelmChart represents the entire Chart.yaml structure.
+type HelmChart struct {
+ K *koanf.Koanf
+ Metadata ChartMetadata `yaml:"metadata" validate:"required,dive"`
+ // Add other fields as needed
+}
+
+func NewHelmChart() *HelmChart {
+ return &HelmChart{
+ K: koanf.New("."),
+ }
+}
+
+// LoadFromFile loads values from a YAML file into the HelmChart struct.
+func (h *HelmChart) LoadFromFile(filename string) error {
+ // Load YAML file using koanf
+ if err := h.K.Load(file.Provider(filename), yaml.Parser()); err != nil {
+ return fmt.Errorf("error loading from file %s: %v", filename, err)
+ }
+
+ // Unmarshal the data into the HelmChart struct
+ if err := h.K.Unmarshal("", &h.Metadata); err != nil {
+ return fmt.Errorf("error unmarshalling data: %v", err)
+ }
+
+ // Set default values for fields if they are not set or empty
+ h.setDefaultValues()
+
+ // Initialize validator
+ validate = validator.New(validator.WithRequiredStructEnabled())
+
+ if err := validate.Struct(h.Metadata); err != nil {
+ return fmt.Errorf("chart.yaml validation error: %v", err)
+ }
+
+ return nil
+}
+
+// setDefaultValues sets default values for fields in ChartMetadata if they are not set or empty.
+func (h *HelmChart) setDefaultValues() {
+ h.setDeprecation()
+ h.setApiVersion(apiVersion)
+ h.setKubeVersion(kubeVersion)
+ h.setType(chartType)
+ h.setAppVersion(defaultAppVersion)
+ h.setDescription(defaultDescription)
+ h.setIcon(defaultIcon)
+ h.setHome(defaultHome)
+
+ h.setMaintainers(Maintainer{
+ Name: maintainerName,
+ Email: maintainerEmail,
+ URL: maintainerURL,
+ })
+
+ // Make sure annotations is not nil
+ if h.Metadata.Annotations == nil {
+ h.Metadata.Annotations = make(map[string]string)
+ }
+
+ h.setAnnotation("truecharts.org/category", defaultCategory, false)
+ h.setAnnotation("truecharts.org/min_helm_version", minHelmVersion, true)
+ h.setAnnotation("truecharts.org/max_helm_version", maxHelmVersion, true)
+
+ // Set default values for other fields as needed
+}
+
+// SaveToFile saves the Helm chart metadata back to the Chart.yaml file.
+func (h *HelmChart) SaveToFile(filename string) error {
+
+ // Initialize validator
+ validate = validator.New(validator.WithRequiredStructEnabled())
+
+ if err := validate.Struct(h.Metadata); err != nil {
+ return fmt.Errorf("chart.yaml validation error: %v", err)
+ }
+
+ var configBytes bytes.Buffer
+ err := helper.MarshalYaml(&configBytes, h.Metadata)
+ if err != nil {
+ return fmt.Errorf("error encoding data: %v", err)
+ }
+
+ // Write the configuration to the file using os.WriteFile
+ err = os.WriteFile(filename, configBytes.Bytes(), 0644)
+ if err != nil {
+ return fmt.Errorf("error writing to file %s: %v", filename, err)
+ }
+
+ return nil
+}
+
+// setAnnotation sets the annotation key to value if it is not set or empty or force is true.
+func (h *HelmChart) setAnnotation(key, value string, force bool) {
+ if a, ok := h.Metadata.Annotations[key]; !ok || a == "" || force {
+ h.Metadata.Annotations[key] = value
+ }
+}
+
+// setDeprecation sets the deprecation field to false if it is not set.
+func (h *HelmChart) setDeprecation() {
+ if !h.Metadata.Deprecated {
+ h.Metadata.Deprecated = false
+ }
+}
+
+// setIcon sets the icon field to icon if it is not set or empty.
+func (h *HelmChart) setIcon(icon string) {
+ if h.Metadata.Icon == "" {
+ h.Metadata.Icon = icon
+ }
+}
+
+// setHome sets the home field to home if it is not set or empty.
+func (h *HelmChart) setHome(home string) {
+ if h.Metadata.Home == "" {
+ h.Metadata.Home = home
+ }
+}
+
+// setDescription sets the description field to description if it is not set or empty.
+func (h *HelmChart) setDescription(description string) {
+ if h.Metadata.Description == "" {
+ h.Metadata.Description = description
+ }
+}
+
+// setAppVersion sets the appVersion field to appVersion if it is not set or empty.
+func (h *HelmChart) setAppVersion(appVersion string) {
+ if h.Metadata.AppVersion == "" {
+ h.Metadata.AppVersion = appVersion
+ }
+}
+
+// setType sets the type field to cType if it is not set or empty.
+func (h *HelmChart) setType(cType string) {
+ if h.Metadata.Type == "" {
+ h.Metadata.Type = cType
+ }
+}
+
+// setApiVersion sets the apiVersion field to apiVersion
+func (h *HelmChart) setApiVersion(apiVersion string) {
+ h.Metadata.APIVersion = apiVersion
+}
+
+// setKubeVersion sets the kubeVersion field to kubeVersion
+func (h *HelmChart) setKubeVersion(kubeVersion string) {
+ h.Metadata.KubeVersion = kubeVersion
+}
+
+// setMaintainers sets the maintainers field to maintainers
+func (h *HelmChart) setMaintainers(maintainers Maintainer) {
+ h.Metadata.Maintainers = make([]Maintainer, 1)
+ h.Metadata.Maintainers[0] = maintainers
+}
diff --git a/clustertool/pkg/charts/chartFile/chart_file_test.go b/clustertool/pkg/charts/chartFile/chart_file_test.go
new file mode 100644
index 0000000000000..d82f072a152d2
--- /dev/null
+++ b/clustertool/pkg/charts/chartFile/chart_file_test.go
@@ -0,0 +1,621 @@
+package chartFile
+
+import (
+ "fmt"
+ "reflect"
+ "testing"
+)
+
+func TestSetAnnotation(t *testing.T) {
+ type args struct {
+ key string
+ value string
+ force bool
+ }
+
+ type testData struct {
+ name string
+ data args
+ initial map[string]string
+ want map[string]string
+ }
+
+ tests := []testData{
+ {
+ name: "Should set annotation when not present",
+ initial: map[string]string{},
+ want: map[string]string{
+ "test": "test",
+ },
+ data: args{
+ key: "test",
+ value: "test",
+ force: false,
+ },
+ },
+ {
+ name: "Should not set annotation when present and force is false",
+ initial: map[string]string{
+ "test": "value",
+ },
+ want: map[string]string{
+ "test": "value",
+ },
+ data: args{
+ key: "test",
+ value: "test",
+ force: false,
+ },
+ },
+ {
+ name: "Should set annotation when present and force is true",
+ initial: map[string]string{
+ "test": "value",
+ },
+ want: map[string]string{
+ "test": "test",
+ },
+ data: args{
+ key: "test",
+ value: "test",
+ force: true,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ h := NewHelmChart()
+ h.Metadata.Annotations = tt.initial
+
+ h.setAnnotation(tt.data.key, tt.data.value, tt.data.force)
+
+ if !reflect.DeepEqual(h.Metadata.Annotations, tt.want) {
+ t.Errorf("%s - Annotations, got %v, want %v", tt.name, h.Metadata.Annotations, tt.want)
+ }
+ })
+ }
+}
+
+func TestSetDeprecation(t *testing.T) {
+ type testData struct {
+ name string
+ input ChartMetadata
+ want ChartMetadata
+ }
+
+ tests := []testData{
+ {
+ name: "Should set deprecation to false when not present",
+ input: ChartMetadata{
+ Deprecated: false,
+ },
+ want: ChartMetadata{
+ Deprecated: false,
+ },
+ },
+ {
+ name: "Should not set deprecation when present",
+ input: ChartMetadata{
+ Deprecated: true,
+ },
+ want: ChartMetadata{
+ Deprecated: true,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ h := NewHelmChart()
+ h.Metadata = tt.input
+
+ h.setDeprecation()
+
+ if !reflect.DeepEqual(h.Metadata, tt.want) {
+ t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
+ }
+ })
+ }
+
+}
+
+type TestData struct {
+ name string
+ value string
+ initial ChartMetadata
+ want ChartMetadata
+}
+
+func TestSetIcon(t *testing.T) {
+ tests := []TestData{
+ {
+ name: "Should set icon when not present",
+ value: "test",
+ initial: ChartMetadata{
+ Icon: "",
+ },
+ want: ChartMetadata{
+ Icon: "test",
+ },
+ },
+ {
+ name: "Should not set icon when present",
+ value: "test",
+ initial: ChartMetadata{
+ Icon: "some-icon",
+ },
+ want: ChartMetadata{
+ Icon: "some-icon",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ h := NewHelmChart()
+ h.Metadata = tt.initial
+
+ h.setIcon(tt.value)
+
+ if !reflect.DeepEqual(h.Metadata, tt.want) {
+ t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
+ }
+ })
+ }
+}
+
+func TestSetHome(t *testing.T) {
+ tests := []TestData{
+ {
+ name: "Should set home when not present",
+ value: "test",
+ initial: ChartMetadata{
+ Home: "",
+ },
+ want: ChartMetadata{
+ Home: "test",
+ },
+ },
+ {
+ name: "Should not set home when present",
+ value: "test",
+ initial: ChartMetadata{
+ Home: "some-home",
+ },
+ want: ChartMetadata{
+ Home: "some-home",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ h := NewHelmChart()
+ h.Metadata = tt.initial
+
+ h.setHome(tt.value)
+
+ if !reflect.DeepEqual(h.Metadata, tt.want) {
+ t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
+ }
+ })
+ }
+}
+
+func TestSetDescription(t *testing.T) {
+ tests := []TestData{
+ {
+ name: "Should set description when not present",
+ value: "test",
+ initial: ChartMetadata{
+ Description: "",
+ },
+ want: ChartMetadata{
+ Description: "test",
+ },
+ },
+ {
+ name: "Should not set description when present",
+ value: "test",
+ initial: ChartMetadata{
+ Description: "some-description",
+ },
+ want: ChartMetadata{
+ Description: "some-description",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ h := NewHelmChart()
+ h.Metadata = tt.initial
+
+ h.setDescription(tt.value)
+
+ if !reflect.DeepEqual(h.Metadata, tt.want) {
+ t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
+ }
+ })
+ }
+}
+
+func TestSetAppVersion(t *testing.T) {
+ tests := []TestData{
+ {
+ name: "Should set appVersion when not present",
+ value: "test",
+ initial: ChartMetadata{
+ AppVersion: "",
+ },
+ want: ChartMetadata{
+ AppVersion: "test",
+ },
+ },
+ {
+ name: "Should not set appVersion when present",
+ value: "test",
+ initial: ChartMetadata{
+ AppVersion: "some-appVersion",
+ },
+ want: ChartMetadata{
+ AppVersion: "some-appVersion",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ h := NewHelmChart()
+ h.Metadata = tt.initial
+
+ h.setAppVersion(tt.value)
+
+ if !reflect.DeepEqual(h.Metadata, tt.want) {
+ t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
+ }
+ })
+ }
+}
+
+func TestSetType(t *testing.T) {
+ tests := []TestData{
+ {
+ name: "Should set type when not present",
+ value: "test",
+ initial: ChartMetadata{
+ Type: "",
+ },
+ want: ChartMetadata{
+ Type: "test",
+ },
+ },
+ {
+ name: "Should not set type when present",
+ value: "test",
+ initial: ChartMetadata{
+ Type: "some-type",
+ },
+ want: ChartMetadata{
+ Type: "some-type",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ h := NewHelmChart()
+ h.Metadata = tt.initial
+
+ h.setType(tt.value)
+
+ if !reflect.DeepEqual(h.Metadata, tt.want) {
+ t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
+ }
+ })
+ }
+}
+
+func TestSetApiVersion(t *testing.T) {
+ tests := []TestData{
+ {
+ name: "Should set apiVersion when not present",
+ value: "test",
+ initial: ChartMetadata{
+ APIVersion: "",
+ },
+ want: ChartMetadata{
+ APIVersion: "test",
+ },
+ },
+ {
+ name: "Should always set apiVersion when present",
+ value: "test",
+ initial: ChartMetadata{
+ APIVersion: "some-apiVersion",
+ },
+ want: ChartMetadata{
+ APIVersion: "test",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ h := NewHelmChart()
+ h.Metadata = tt.initial
+
+ h.setApiVersion(tt.value)
+
+ if !reflect.DeepEqual(h.Metadata, tt.want) {
+ t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
+ }
+ })
+ }
+}
+
+func TestSetKubeVersion(t *testing.T) {
+ tests := []TestData{
+ {
+ name: "Should set kubeVersion when not present",
+ value: "test",
+ initial: ChartMetadata{
+ KubeVersion: "",
+ },
+ want: ChartMetadata{
+ KubeVersion: "test",
+ },
+ },
+ {
+ name: "Should always set kubeVersion when present",
+ value: "test",
+ initial: ChartMetadata{
+ KubeVersion: "some-kubeVersion",
+ },
+ want: ChartMetadata{
+ KubeVersion: "test",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ h := NewHelmChart()
+ h.Metadata = tt.initial
+
+ h.setKubeVersion(tt.value)
+
+ if !reflect.DeepEqual(h.Metadata, tt.want) {
+ t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
+ }
+ })
+ }
+}
+
+func TestSetMaintainers(t *testing.T) {
+ type testData struct {
+ name string
+ value Maintainer
+ initial ChartMetadata
+ want ChartMetadata
+ }
+ tests := []testData{
+ {
+ name: "Should set maintainers when not present",
+ value: Maintainer{
+ Name: "test-name",
+ Email: "test-mail",
+ URL: "test-url",
+ },
+ initial: ChartMetadata{
+ Maintainers: []Maintainer{},
+ },
+ want: ChartMetadata{
+ Maintainers: []Maintainer{
+ {
+ Name: "test-name",
+ Email: "test-mail",
+ URL: "test-url",
+ },
+ },
+ },
+ },
+ {
+ name: "Should always set maintainers when present",
+ value: Maintainer{
+ Name: "test-name",
+ Email: "test-mail",
+ URL: "test-url",
+ },
+ initial: ChartMetadata{
+ Maintainers: []Maintainer{
+ {
+ Name: "some-maintainer",
+ Email: "some-mail",
+ URL: "some-url",
+ },
+ },
+ },
+ want: ChartMetadata{
+ Maintainers: []Maintainer{
+ {
+ Name: "test-name",
+ Email: "test-mail",
+ URL: "test-url",
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+
+ h := NewHelmChart()
+ h.Metadata = tt.initial
+
+ h.setMaintainers(tt.value)
+
+ if !reflect.DeepEqual(h.Metadata.Maintainers, tt.want.Maintainers) {
+ t.Errorf("%s - Maintainers, got %v, want %v", tt.name, h.Metadata.Maintainers, tt.want.Maintainers)
+ }
+ })
+ }
+}
+
+func TestSetDefaults(t *testing.T) {
+ type testData struct {
+ name string
+ initial ChartMetadata
+ want ChartMetadata
+ }
+ tests := []testData{
+ {
+ name: "Should set defaults",
+ initial: ChartMetadata{},
+ want: ChartMetadata{
+ KubeVersion: kubeVersion,
+ APIVersion: apiVersion,
+ Type: chartType,
+ Deprecated: false,
+ AppVersion: defaultAppVersion,
+ Description: defaultDescription,
+ Home: defaultHome,
+ Icon: defaultIcon,
+ Maintainers: []Maintainer{
+ {
+ Name: maintainerName,
+ Email: maintainerEmail,
+ URL: maintainerURL,
+ },
+ },
+ Annotations: map[string]string{
+ "truecharts.org/category": defaultCategory,
+ "truecharts.org/min_helm_version": minHelmVersion,
+ "truecharts.org/max_helm_version": maxHelmVersion,
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+
+ h := NewHelmChart()
+ h.Metadata = tt.initial
+
+ h.setDefaultValues()
+
+ if !reflect.DeepEqual(h.Metadata, tt.want) {
+ t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
+ }
+ })
+ }
+
+}
+
+func TestLoadFromFile(t *testing.T) {
+ type testData struct {
+ name string
+ file string
+ wantErr bool
+ }
+
+ testDataPath := "../../testdata/chart_yaml"
+ tests := []testData{
+ {
+ name: "Should load from file",
+ file: "validChart.yaml",
+ wantErr: false,
+ },
+ {
+ name: "Should fail to load from malformed file",
+ file: "malformedChart.yaml",
+ wantErr: true,
+ },
+ {
+ name: "Should fail to load from missing file",
+ file: "missingChart.yaml",
+ wantErr: true,
+ },
+ {
+ name: "Should fail to load from invalid file",
+ file: "invalidChart.yaml",
+ wantErr: true,
+ },
+ {
+ name: "Should fail to load from unmashalable file",
+ file: "unmarshalableChart.yaml",
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ h := NewHelmChart()
+
+ err := h.LoadFromFile(fmt.Sprintf("%s/%s", testDataPath, tt.file))
+ if (err != nil) != tt.wantErr {
+ t.Errorf("%s - LoadFromFile() error = %v, wantErr %v", tt.name, err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestSaveToFile(t *testing.T) {
+ type testData struct {
+ name string
+ inFile string
+ outFile string
+ mutatedData ChartMetadata
+ shouldMutate bool
+ wantErr bool
+ }
+
+ testDataPath := "../../testdata/chart_yaml"
+ tests := []testData{
+ {
+ name: "Should fail to save to file",
+ inFile: "validChart.yaml",
+ outFile: "/tmp/test.yaml",
+ mutatedData: ChartMetadata{},
+ shouldMutate: true,
+ wantErr: true,
+ },
+ {
+ name: "Should save to file",
+ inFile: "validChart.yaml",
+ outFile: "/tmp/test.yaml",
+ mutatedData: ChartMetadata{},
+ shouldMutate: false,
+ wantErr: false,
+ },
+ {
+ name: "Should fail to write to file",
+ inFile: "validChart.yaml",
+ outFile: "/non-existent-dir/test.yaml",
+ mutatedData: ChartMetadata{},
+ shouldMutate: false,
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ h := NewHelmChart()
+ if err := h.LoadFromFile(fmt.Sprintf("%s/%s", testDataPath, tt.inFile)); err != nil {
+ t.Errorf("%s - LoadFromFile() error = %v", tt.name, err)
+ }
+
+ if tt.shouldMutate {
+ h.Metadata = tt.mutatedData
+ }
+
+ err := h.SaveToFile(tt.outFile)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("%s - SaveToFile() error = %v, wantErr %v", tt.name, err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/clustertool/pkg/charts/chartFile/updater.go b/clustertool/pkg/charts/chartFile/updater.go
new file mode 100644
index 0000000000000..b0a1ca151a944
--- /dev/null
+++ b/clustertool/pkg/charts/chartFile/updater.go
@@ -0,0 +1,208 @@
+package chartFile
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "slices"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+ "github.com/truecharts/private/clustertool/pkg/charts/helmignore"
+ "github.com/truecharts/private/clustertool/pkg/charts/image"
+ "github.com/truecharts/private/clustertool/pkg/charts/readme"
+ "github.com/truecharts/private/clustertool/pkg/charts/version"
+)
+
+// UpdateChartFile updates the specified Chart.yaml file with an optional bump parameter.
+func UpdateChartFile(chartPathOrFolder, bump string) error {
+ fileInfo, err := os.Stat(chartPathOrFolder)
+ if err != nil {
+ return err
+ }
+
+ chartPath := chartPathOrFolder
+ if fileInfo.IsDir() {
+ chartPath = filepath.Join(chartPathOrFolder, "Chart.yaml")
+ }
+
+ log.Info().Msgf("🏃 Processing chart [%s]", chartPath)
+ chart := NewHelmChart()
+ if err := chart.LoadFromFile(chartPath); err != nil {
+ return err
+ }
+
+ if chart.Metadata.Annotations == nil {
+ chart.Metadata.Annotations = make(map[string]string)
+ }
+
+ train := GetTrain(chartPath, chart)
+ setMetadata(chart, train)
+
+ var values image.Images
+ // Fetch image details from values.yaml
+ if err := values.LoadValuesFile(filepath.Join(filepath.Dir(chartPath), "values.yaml")); err != nil {
+ return err
+ }
+ setAppVersionFromImage(chart, &values, "image")
+
+ var imageLinks []string
+ for _, details := range values.ImagesMap {
+ imageLinks = append(imageLinks, details.Link)
+ }
+
+ // Attempt to update sources
+ if err := updateSources(chart, train, imageLinks); err != nil {
+ return err
+ }
+
+ // Update appVersion, icon, and home URLs
+ if bump == version.Major || bump == version.Minor || bump == version.Patch {
+ newVersion, err := version.IncrementVersion(chart.Metadata.Version, bump)
+ log.Info().Msgf("🆚 Bumping [%s], from [%s] to [%s]", chart.Metadata.Name, chart.Metadata.Version, newVersion)
+ if err != nil {
+ log.Error().Err(err).Msg("Error bumping version")
+ }
+ chart.Metadata.Version = newVersion
+ }
+
+ // Save the modified metadata back to the file
+ if err := chart.SaveToFile(chartPath); err != nil {
+ return fmt.Errorf("error saving Chart.yaml: %s", err)
+ }
+
+ log.Info().Msgf("Chart file updated and saved to [%s]", chartPath)
+
+ templateDir := chartPath
+ for i := 0; i < 4; i++ {
+ templateDir = filepath.Dir(templateDir)
+ }
+
+ // Generate README.md for the specified train and chart
+ readmeErr := readme.GenerateReadme(templateDir, chartPath, chart.Metadata.Name, train)
+ if readmeErr != nil {
+ log.Info().Msgf("Error Generating readme for %v: %v\n", chart.Metadata.Name, readmeErr)
+ os.Exit(1)
+ }
+
+ // Generate .helmignore for the specified train and chart
+ helmignoreErr := helmignore.GenerateHelmIgnore(templateDir, chartPath)
+ if helmignoreErr != nil {
+ log.Info().Msgf("Error Generating helmignore for %v: %v\n", chart.Metadata.Name, helmignoreErr)
+ os.Exit(1)
+ }
+ return nil
+}
+
+func setAppVersionFromImage(chart *HelmChart, imageMap *image.Images, key string) {
+ imageDetails, exists := imageMap.ImagesMap[key]
+ if !exists {
+ log.Warn().Msgf("Details for image key [%s] not found in values.yaml, skipping setting appVersion", key)
+ return
+ }
+
+ log.Info().Msgf("Detected - Tag [%s], Image Version [%s]", imageDetails.Tag, imageDetails.Version)
+ chart.Metadata.AppVersion = imageDetails.Version
+}
+
+// detectTrainFromFile detects the train name based on the path of Chart.yaml.
+func detectTrainFromFile(chartFilename string) string {
+ parts := strings.Split(
+ // Remove the filename from the path
+ filepath.Dir(chartFilename),
+ string(os.PathSeparator),
+ )
+
+ if len(parts) >= 2 {
+ return parts[len(parts)-2]
+ }
+
+ // One case to reach here is when the tool is run from inside the train directory
+ // But we can't safely assume the current directory is the train directory
+ log.Error().Msgf("Unable to detect train from path [%s]", chartFilename)
+ return ""
+}
+
+func GetTrain(chartPath string, chart *HelmChart) string {
+ // Detect the train from the path of Chart.yaml
+ // Do not rely on the annotations in the chart, as they may be outdated
+ train := detectTrainFromFile(chartPath)
+ if train == "" {
+ // If the train cannot be detected from the path, fallback to detect it from the annotations
+ if val, exists := chart.Metadata.Annotations["truecharts.org/train"]; exists {
+ train = val
+ } else {
+ log.Error().Msgf("Unable to detect train for chart [%s]. Setting as [unknown]", chart.Metadata.Name)
+ train = "unknown"
+ }
+ }
+
+ return train
+}
+
+func setMetadata(chart *HelmChart, train string) {
+ chart.Metadata.Annotations["truecharts.org/train"] = train
+ chart.Metadata.Icon = fmt.Sprintf("https://truecharts.org/img/hotlink-ok/chart-icons/%s.webp", chart.Metadata.Name)
+ chart.Metadata.Home = fmt.Sprintf("https://truecharts.org/charts/%s/%s", train, chart.Metadata.Name)
+}
+
+// UpdateSources updates the sources in Chart.yaml using Go.
+func updateSources(chart *HelmChart, train string, imageLinks []string) error {
+ var updatedSources []string
+
+ // Those sources are automatically generated by this tool,
+ // So we only need to keep sources that are not in this list
+ for _, source := range chart.Metadata.Sources {
+ if !strings.HasPrefix(source, "https://ghcr") &&
+ !strings.HasPrefix(source, "https://docker.io") &&
+ !strings.HasPrefix(source, "https://hub.docker") &&
+ !strings.HasPrefix(source, "https://fleet.linuxserver") &&
+ !strings.HasPrefix(source, "https://mcr.microsoft") &&
+ !strings.HasPrefix(source, "https://cr.hotio.dev") &&
+ !strings.HasPrefix(source, "https://github.com/truecharts") &&
+ !strings.HasPrefix(source, "https://gallery.ecr.aws") &&
+ !strings.HasPrefix(source, "https://gcr") &&
+ !strings.HasPrefix(source, "https://quay") &&
+ !strings.HasPrefix(source, "http://") &&
+ !strings.Contains(source, ".azurecr.io") &&
+ !strings.Contains(source, ".ocir.io") {
+ if source != "" {
+ log.Info().Msgf("🔗 Keeping source [%s]", source)
+ updatedSources = append(updatedSources, source)
+ }
+ }
+ }
+
+ // Add the GitHub source for the chart
+ ghSource := fmt.Sprintf("https://github.com/truecharts/charts/tree/master/charts/%s/%s", train, chart.Metadata.Name)
+ updatedSources = append(updatedSources, ghSource)
+
+ // Add new sources for each image
+ updatedSources = append(updatedSources, imageLinks...)
+
+ // Deduplicate sources
+ deduplicatedSources := make(map[string]bool)
+ var finalSources []string
+ for _, source := range updatedSources {
+ // Skip empty sources
+ if source == "" {
+ continue
+ }
+ // Skip sources that have already been added
+ if _, exists := deduplicatedSources[source]; exists {
+ continue
+ }
+
+ // Add the source to the list of sources and mark it as added
+ deduplicatedSources[source] = true
+ finalSources = append(finalSources, source)
+ }
+
+ // Sort the sources, so subsequent commits will only include actual changes
+ slices.Sort(finalSources)
+
+ // Update the chart's sources
+ chart.Metadata.Sources = finalSources
+
+ return nil
+}
diff --git a/clustertool/pkg/charts/chartFile/updater_test.go b/clustertool/pkg/charts/chartFile/updater_test.go
new file mode 100644
index 0000000000000..72d696960f6eb
--- /dev/null
+++ b/clustertool/pkg/charts/chartFile/updater_test.go
@@ -0,0 +1,230 @@
+package chartFile
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/truecharts/private/clustertool/pkg/charts/image"
+)
+
+func TestSetAppVersionFromImage(t *testing.T) {
+ type TestData struct {
+ chart *HelmChart
+ image *image.Images
+ key string
+ result string
+ }
+
+ tests := []TestData{
+ {
+ chart: &HelmChart{
+ Metadata: ChartMetadata{
+ AppVersion: "1.0.0",
+ },
+ },
+ image: &image.Images{
+ ImagesMap: map[string]image.ImageDetails{
+ "image": {
+ Repository: "nginx",
+ Tag: "1.15.8",
+ Link: "https://hub.docker.com/_/nginx",
+ Version: "1.15.8",
+ },
+ },
+ },
+ key: "image",
+ result: "1.15.8",
+ },
+ {
+ chart: &HelmChart{
+ Metadata: ChartMetadata{
+ AppVersion: "1.0.0",
+ },
+ },
+ image: &image.Images{
+ ImagesMap: map[string]image.ImageDetails{
+ "image": {
+ Repository: "nginx",
+ Tag: "1.15.8",
+ Link: "https://hub.docker.com/_/nginx",
+ Version: "1.15.8",
+ },
+ },
+ },
+ key: "nonexistent",
+ result: "1.0.0",
+ },
+ }
+
+ for _, tt := range tests {
+ setAppVersionFromImage(tt.chart, tt.image, tt.key)
+ if tt.chart.Metadata.AppVersion != tt.result {
+ t.Errorf("Expected %s, got %s", tt.result, tt.chart.Metadata.AppVersion)
+ }
+ }
+}
+func TestGetTrain(t *testing.T) {
+ type TestData struct {
+ name string
+ chart *HelmChart
+ chartPath string
+ result string
+ }
+
+ tests := []TestData{
+ {
+ name: "Test get train from path",
+ chart: &HelmChart{
+ Metadata: ChartMetadata{
+ Name: "test-chart",
+ Annotations: map[string]string{
+ "truecharts.org/train": "express",
+ },
+ },
+ },
+ chartPath: "../../testdata/updater/stable/my-app/Chart.yaml",
+ result: "stable",
+ },
+ {
+ name: "Test get train from annotations as fallback",
+ chart: &HelmChart{
+ Metadata: ChartMetadata{
+ Name: "test-chart",
+ Annotations: map[string]string{
+ "truecharts.org/train": "dev",
+ },
+ },
+ },
+ // Too short path, cant detect train from path
+ // so we should fallback to annotations
+ chartPath: "my-app/Chart.yaml",
+ result: "dev",
+ },
+ {
+ name: "Test failing to get train from path or annotations",
+ chart: &HelmChart{
+ Metadata: ChartMetadata{
+ Name: "test-chart",
+ Annotations: map[string]string{},
+ },
+ },
+ // Too short path, cant detect train from path
+ // so we should fallback to annotations
+ chartPath: "my-app/Chart.yaml",
+ result: "unknown",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ train := GetTrain(tt.chartPath, tt.chart)
+
+ if train != tt.result {
+ t.Errorf("Expected train to be %s, but got %s", tt.result, train)
+ }
+ })
+ }
+}
+
+func TestSetMetadata(t *testing.T) {
+ type TestData struct {
+ chart *HelmChart
+ train string
+ expected *HelmChart
+ }
+
+ tests := []TestData{
+ {
+ chart: &HelmChart{
+ Metadata: ChartMetadata{
+ Name: "test-chart",
+ Annotations: map[string]string{},
+ },
+ },
+ train: "stable",
+ expected: &HelmChart{
+ Metadata: ChartMetadata{
+ Name: "test-chart",
+ Annotations: map[string]string{
+ "truecharts.org/train": "stable",
+ },
+ Icon: "https://truecharts.org/img/hotlink-ok/chart-icons/test-chart.webp",
+ Home: "https://truecharts.org/charts/stable/test-chart",
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.chart.Metadata.Name, func(t *testing.T) {
+ setMetadata(tt.chart, tt.train)
+
+ if !reflect.DeepEqual(tt.chart, tt.expected) {
+ t.Errorf("Expected chart to be %v, but got %v", tt.expected, tt.chart)
+ }
+ })
+ }
+}
+
+func TestUpdateSources(t *testing.T) {
+ type TestData struct {
+ name string
+ chart *HelmChart
+ train string
+ imageLinks []string
+ expected []string
+ }
+
+ tests := []TestData{
+ {
+ name: "Test update sources",
+ chart: &HelmChart{
+ Metadata: ChartMetadata{
+ Name: "test-chart",
+ Sources: []string{
+ "",
+ "https://ghcr/truecharts/some-chart",
+ "https://docker.io/truecharts/some-chart",
+ "https://hub.docker/truecharts/some-chart",
+ "https://fleet.linuxserver/truecharts/some-chart",
+ "https://mcr.microsoft/truecharts/some-chart",
+ "https://github.com/truecharts/some-chart",
+ "https://gallery.ecr.aws/truecharts/some-chart",
+ "https://gcr/truecharts/some-chart",
+ "https://quay/truecharts/some-chart",
+ "http://truecharts/some-chart",
+ "https://truecharts.azurecr.io/some-chart",
+ "https://truecharts.ocir.io/some-chart",
+ "https://unrelated.com/some-chart",
+ "https://unrelated.com/some-chart",
+ "https://cr.hotio.dev/truecharts/some-chart",
+ },
+ },
+ },
+ train: "stable",
+ imageLinks: []string{
+ "",
+ "https://hub.docker.com/_/nginx",
+ "https://quay.io/truecharts/test-chart",
+ },
+ expected: []string{
+ "https://github.com/truecharts/charts/tree/master/charts/stable/test-chart",
+ "https://hub.docker.com/_/nginx",
+ "https://quay.io/truecharts/test-chart",
+ "https://unrelated.com/some-chart",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.chart.Metadata.Name, func(t *testing.T) {
+ if err := updateSources(tt.chart, tt.train, tt.imageLinks); err != nil {
+ t.Errorf("Expected no error, but got %v", err)
+ }
+
+ if !reflect.DeepEqual(tt.chart.Metadata.Sources, tt.expected) {
+ t.Errorf("Expected chart to be %v, but got %v", tt.expected, tt.chart.Metadata.Sources)
+ }
+ })
+ }
+}
diff --git a/clustertool/pkg/charts/deps/deps.go b/clustertool/pkg/charts/deps/deps.go
new file mode 100644
index 0000000000000..19c18db225f8a
--- /dev/null
+++ b/clustertool/pkg/charts/deps/deps.go
@@ -0,0 +1,197 @@
+package deps
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+
+ "github.com/truecharts/private/clustertool/pkg/charts/chartFile"
+ "github.com/truecharts/private/clustertool/pkg/fluxhandler"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+func LoadGPGKey() error {
+ log.Info().Msg("🔑 Fetching and Loading TrueCharts PGP Public Key 🔑")
+ if err := os.MkdirAll(helper.GpgDir, os.ModePerm); err != nil {
+ log.Fatal().Err(err).Msg("❌ Failed to create GPG directory")
+ }
+
+ keybaseURL := "https://truecharts.org/pub_key.gpg"
+ pubringPath := path.Join(helper.GpgDir, "pubring.gpg")
+ if err := downloadFile(keybaseURL, pubringPath); err != nil {
+ log.Fatal().Err(err).Msg("❌ Failed to download keybase public key")
+ }
+
+ certmanURL := "https://cert-manager.io/public-keys/cert-manager-keyring-2021-09-20-1020CF3C033D4F35BAE1C19E1226061C665DF13E.gpg"
+ certmanPath := path.Join(helper.GpgDir, "certman.gpg")
+ if err := downloadFile(certmanURL, certmanPath); err != nil {
+ log.Fatal().Err(err).Msg("❌ Failed to download certman public key")
+ }
+
+ log.Info().Msg("✅ Public Key loaded successfully")
+ return nil
+}
+
+func downloadFile(url, destination string) error {
+ response, err := http.Get(url)
+ if err != nil {
+ log.Error().Err(err).Msgf("❌ Failed to download [%s]", url)
+ return err
+ }
+ defer response.Body.Close()
+
+ body, err := io.ReadAll(response.Body)
+ if err != nil {
+ log.Error().Err(err).Msg("❌ Failed to read response body")
+ return err
+ }
+
+ err = os.WriteFile(destination, body, os.ModePerm)
+ if err != nil {
+ log.Error().Err(err).Msgf("❌ Failed to write file at [%s]", destination)
+ return err
+ }
+
+ return nil
+}
+
+// fetchIndexFile downloads an index file from a repo if not already cached
+func fetchIndexFile(repo string, repoDir string, repoURL string) error {
+ destPath := path.Join(helper.IndexCache, repoDir, "index.yaml")
+ if strings.HasPrefix(repoURL, "oci") {
+ log.Info().Msgf("⏩ URL [%s] is OCI, skipping index download", repoURL)
+ return nil
+ }
+
+ if _, err := os.Stat(destPath); err == nil {
+ log.Info().Msgf("✅ Index file for [%s] already cached", repo)
+ return nil
+ }
+
+ log.Info().Msgf("🙅 Index file for [%s] not cached", repo)
+
+ // Create index directory
+ err := os.MkdirAll(path.Join(helper.IndexCache, repoDir), os.ModePerm)
+ if err != nil {
+ log.Fatal().Err(err).Msg("❌ Failed to create index directory")
+ }
+
+ // Download index file
+ log.Info().Msgf("⏬ Downloading index [%s]...", repoURL)
+ err = downloadFile(repoURL, destPath)
+ if err != nil {
+ log.Fatal().Err(err).Msgf("❌ Failed to download index for [%s] from [%s]", repo, repoURL)
+ }
+
+ log.Info().Msg("✅ Index File downloaded")
+
+ return nil
+}
+
+// fetchDependency downloads a dependency from a repo if not already cached
+func fetchDependency(repo string, repoDir string, name string, version string, repoURL string) error {
+ destPath := path.Join(helper.HelmCache, repoDir, fmt.Sprintf("%s-%s.tgz", name, version))
+ if _, err := os.Stat(destPath); err == nil {
+ log.Info().Msgf("✅ Dependency [%s-%s] already cached", name, version)
+ return nil
+ }
+
+ log.Info().Msgf("🙅 Dependency [%s-%s] not cached", name, version)
+
+ repoCacheDir := path.Join(helper.HelmCache, repoDir)
+ // Create cache directory
+ if err := os.MkdirAll(repoCacheDir, os.ModePerm); err != nil {
+ return fmt.Errorf("❌ Failed to create cache directory: %s", err)
+ }
+
+ // Download dependency
+ log.Info().Msgf("⏬ Downloading dependency [%s-%s] from [%s]", name, version, repo)
+ if err := fluxhandler.HelmPull(repo, name, version, repoCacheDir, false); err != nil {
+ return fmt.Errorf("❌ Failed to download or verify dependency: %s", err)
+ }
+
+ log.Info().Msg("✅ Dependency downloaded")
+
+ return nil
+}
+
+// copyDependency copies a dependency from the cache to the chart folder
+func copyDependency(chartFolder string, repo string, repoDir string, name string, version string) error {
+ log.Info().Msg("📝 Copying dependency")
+
+ targetChartsFolder := path.Join(chartFolder, "charts")
+ if err := os.MkdirAll(targetChartsFolder, os.ModePerm); err != nil {
+ return fmt.Errorf("❌ Failed to create charts directory: %s", err)
+ }
+
+ srcPath := path.Join(helper.HelmCache, repoDir, fmt.Sprintf("%s-%s.tgz", name, version))
+ destPath := path.Join(targetChartsFolder, fmt.Sprintf("%s-%s.tgz", name, version))
+ if err := helper.CopyFile(srcPath, destPath, false); err != nil {
+ return fmt.Errorf("❌ Failed to copy dependency: %s", err)
+ }
+
+ log.Info().Msg("✅ Dependency copied!")
+ return nil
+}
+
+func DownloadDeps(chartPath string, placeholder string) error {
+ chartFolder := filepath.Dir(chartPath)
+
+ helmChart := chartFile.NewHelmChart()
+ err := helmChart.LoadFromFile(chartPath)
+ if err != nil {
+ log.Fatal().Err(err).Msgf("❌ Failed to load Helm chart from file in [%s]", chartFolder)
+ }
+
+ fmt.Print("\n\n")
+ log.Info().Msgf("🏃 Processing Chart [%s] with [%d] dependencies", chartFolder, len(helmChart.Metadata.Dependencies))
+
+ // Make sure the directory "charts" exists in the chart folder
+ targetChartsFolder := path.Join(chartFolder, "charts")
+ if err := os.MkdirAll(targetChartsFolder, os.ModePerm); err != nil {
+ return fmt.Errorf("❌ Failed to create charts directory: %s", err)
+ }
+
+ // Process dependencies as needed
+ for _, dep := range helmChart.Metadata.Dependencies {
+ name := dep.Name
+ version := dep.Version
+ repo := dep.Repository
+ repoURL := fmt.Sprintf("%s/index.yaml", strings.TrimRight(repo, "/"))
+
+ fmt.Print("\n")
+ log.Info().Msgf("📦 Dependency [%s]", name)
+ log.Info().Msgf("🆚 Version [%s]", version)
+ log.Info().Msgf("📥 Repo [%s]", repo)
+ log.Info().Msgf("🔗 URL [%s]", repoURL)
+
+ repoDir := repo
+ // Remove protocol(s) from repoDir
+ for _, prefix := range []string{"http://", "https://", "oci://"} {
+ repoDir = strings.TrimPrefix(repoDir, prefix)
+ }
+
+ if err := fetchIndexFile(repo, repoDir, repoURL); err != nil {
+ return fmt.Errorf("❌ Failed to fetch index file: %s", err)
+ }
+
+ if err := fetchDependency(repo, repoDir, name, version, repoURL); err != nil {
+ log.Fatal().Err(err).Msg("❌ Failed to fetch dependency")
+ }
+
+ if err := copyDependency(chartFolder, repo, repoDir, name, version); err != nil {
+ log.Fatal().Err(err).Msg("❌ Failed to copy dependency")
+ }
+
+ log.Info().Msg("✅ Dependency processed!")
+ }
+
+ log.Info().Msg("✅ Processing complete!")
+ return nil
+}
diff --git a/clustertool/pkg/charts/helmignore/helmignore.go b/clustertool/pkg/charts/helmignore/helmignore.go
new file mode 100644
index 0000000000000..8d4f2707874d2
--- /dev/null
+++ b/clustertool/pkg/charts/helmignore/helmignore.go
@@ -0,0 +1,30 @@
+package helmignore
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/rs/zerolog/log"
+)
+
+func GenerateHelmIgnore(templatePath string, chartPath string) error {
+ // Define file paths
+ template := filepath.Join(templatePath, "templates/helmignore.tpl")
+ target := filepath.Join(filepath.Dir(chartPath), ".helmignore")
+
+ // Read template file
+ templateContent, err := os.ReadFile(template)
+ if err != nil {
+ return fmt.Errorf("failed to read template file: %v", err)
+ }
+
+ // Write the modified content to the .helmignore file in the chart directory
+ err = os.WriteFile(target, []byte(templateContent), 0644)
+ if err != nil {
+ return fmt.Errorf("failed to write .helmignore file: %v", err)
+ }
+
+ log.Info().Msgf("Generated .helmignore for [%s]", chartPath)
+ return nil
+}
diff --git a/clustertool/pkg/charts/image/cleanup.go b/clustertool/pkg/charts/image/cleanup.go
new file mode 100644
index 0000000000000..fa72b958858a1
--- /dev/null
+++ b/clustertool/pkg/charts/image/cleanup.go
@@ -0,0 +1,187 @@
+package image
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+
+ "k8s.io/apimachinery/pkg/util/validation"
+)
+
+var (
+ // Valid SemVer format (Major.Minor.Patch)
+ semVerPattern = regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)$`)
+ // Matches tags like "RELEASE.2023-11-20T22-40-07Z"
+ releasePattern = regexp.MustCompile(`^RELEASE\.[0-9]{4}-[0-9]{2}-[0-9]{2}T`)
+ // Matches tags like "x64-1.2.3" and "arm64-1.2.3" followed by a numeric version
+ archPattern = regexp.MustCompile(`^[a-zA-Z0-9]+-[0-9]+\.[0-9]+`)
+ // Matches tags like "latest-2023-12-18"
+ prefixYearMonthDayPattern = regexp.MustCompile(`^[a-zA-Z0-9]+-[0-9]{4}-[0-9]{2}-[0-9]{2}$`)
+ // Matches dates like "2023-11-15" and "2022-04"
+ yearMonthDayPattern = regexp.MustCompile(`^[0-9]{4}-[0-9]{2}(-[0-9]{2})?$`)
+ // Matches tags like "1.2.3.4" "1.2" and "1"
+ incompleteSemVerPattern = regexp.MustCompile(`^[0-9]+(\.[0-9]+)*$`)
+ // Matches tags like "something-abcdefg" (only chars before dash and exactly 7 characters after the dash)
+ shortCommitHashSuffixPattern = regexp.MustCompile(`^[a-zA-Z]+-[a-zA-Z0-9]{7}$`)
+ // Matches tags like "v1.2.3", "V1.2.3, #1.2.3, $1.2.3, etc"
+ leadingSymbolPattern = regexp.MustCompile(`^version|Version|[vV]|^[^a-zA-Z0-9]+`)
+)
+
+func CleanTag(tag string) (string, error) {
+ tag = strings.TrimSpace(tag)
+
+ if tag == "" {
+ return "", fmt.Errorf("tag is empty")
+ }
+
+ // Do basic cleaning
+ tag = cleanSha(tag)
+ tag = cleanLeadingSymbol(tag)
+
+ // Return early if the tag is already in SemVer format
+ if semVerPattern.MatchString(tag) {
+ return tag, nil
+ }
+
+ switch {
+ case releasePattern.MatchString(tag):
+ tag = cleanRelease(tag)
+ case archPattern.MatchString(tag):
+ tag = cleanArch(tag)
+ case prefixYearMonthDayPattern.MatchString(tag):
+ tag = cleanPrefixYearMonthDay(tag)
+ case yearMonthDayPattern.MatchString(tag):
+ tag = cleanYearMonthDay(tag)
+ case incompleteSemVerPattern.MatchString(tag):
+ tag = cleanIncompleteSemVer(tag)
+ case shortCommitHashSuffixPattern.MatchString(tag):
+ tag = keepShortCommitHashSuffix(tag)
+ case leadingSymbolPattern.MatchString(tag):
+ tag = cleanLeadingSymbol(tag)
+ }
+
+ // If string contains `-` the second part is usually
+ // either a commit hash or things like "debian" or "alpine"
+ // Make sure the first part is some kind of versioning and strip the rest
+ if strings.Contains(tag, "-") {
+ split := strings.Split(tag, "-")
+ switch {
+ case semVerPattern.MatchString(split[0]):
+ tag = split[0]
+ case incompleteSemVerPattern.MatchString(split[0]):
+ tag = split[0]
+ }
+ }
+
+ // Re-check for incomplete SemVer after cleaning
+ if incompleteSemVerPattern.MatchString(tag) {
+ tag = cleanIncompleteSemVer(tag)
+ }
+
+ if err := checkValidLabelValue(tag); err != nil {
+ return "", err
+ }
+
+ if !semVerPattern.MatchString(tag) {
+ log.Warn().Msgf("Could not produce a valid SemVer tag for tag [%s]", tag)
+ }
+
+ // Build and return the updated SemVer string
+ return tag, nil
+}
+
+func Clean(tag string) error {
+
+ newTag, err := CleanTag(tag)
+ if err != nil {
+ log.Fatal().Err(err).Msgf("Failed to clean tag [%s]", tag)
+ }
+
+ log.Info().Msgf("Tag [%s] cleaned to [%s]", tag, newTag)
+ return nil
+}
+
+func checkValidLabelValue(tag string) error {
+ if errs := validation.IsValidLabelValue(tag); len(errs) > 0 {
+ return fmt.Errorf("tag [%s] is not valid for label use. error: %s", tag, (strings.Join(errs, ", ")))
+ }
+ return nil
+}
+
+// keepShortCommitHashSuffix keeps the last 7 characters of a tag
+// eg "something-abcdefg" -> "abcdefg"
+func keepShortCommitHashSuffix(tag string) string {
+ return strings.Split(tag, "-")[1]
+}
+
+// cleanRelease Transforms release pattern format
+// eg "RELEASE.2023-11-20T22-40-07Z" -> "2023.11.20"
+func cleanRelease(tag string) string {
+ tag = strings.Split(tag, ".")[1]
+ tag = strings.Split(tag, "T")[0]
+ tag = strings.ReplaceAll(tag, "-", ".")
+
+ return tag
+}
+
+// cleanArch removes arch prefixes
+// eg "x64-1.2.3" -> "1.2.3"
+func cleanArch(tag string) string {
+ tag = strings.Split(tag, "-")[1]
+
+ return tag
+}
+
+// cleanYearMonthDay Transforms date versions
+// eg "2023-11-15" -> "2023.11.15" and "2022-04" -> "2022.4"
+func cleanYearMonthDay(tag string) string {
+ tag = strings.ReplaceAll(tag, "-", ".")
+ parts := strings.Split(tag, ".")
+ for idx := range parts {
+ parts[idx] = strings.TrimPrefix(parts[idx], "0")
+ }
+ for len(parts) < 3 {
+ parts = append(parts, "0")
+ }
+ tag = strings.Join(parts, ".")
+
+ return tag
+}
+
+// cleanIncompleteSemVer Transforms incomplete SemVer strings
+// eg "1.2" -> "1.2.0" and "1" -> "1.0.0"
+// versions with more parts are left as-is
+func cleanIncompleteSemVer(tag string) string {
+ parts := strings.Split(tag, ".")
+ switch {
+ case len(parts) == 2:
+ tag = tag + ".0"
+ case len(parts) == 1:
+ tag = tag + ".0.0"
+ }
+
+ return tag
+}
+
+// cleanLeadingSymbol Trims leading 'v' or non-alphanumeric characters
+// e.g "v1.2.3" -> "1.2.3"
+func cleanLeadingSymbol(tag string) string {
+ return leadingSymbolPattern.ReplaceAllString(tag, "")
+}
+
+// cleanSha Strips everything after '@'
+// e.g "v1.2.3@sha256:abc123" -> "v1.2.3"
+func cleanSha(tag string) string {
+ return strings.Split(tag, "@")[0]
+}
+
+// cleanPrefixYearMonthDay Transforms date versions with prefix
+// eg "latest-2023-12-18" -> "2023.12.18"
+func cleanPrefixYearMonthDay(tag string) string {
+ calVer := strings.Split(tag, "-")[1:]
+ tag = strings.Join(calVer, ".")
+
+ return tag
+}
diff --git a/clustertool/pkg/charts/image/cleanup_test.go b/clustertool/pkg/charts/image/cleanup_test.go
new file mode 100644
index 0000000000000..df49c9683ba04
--- /dev/null
+++ b/clustertool/pkg/charts/image/cleanup_test.go
@@ -0,0 +1,277 @@
+package image
+
+import (
+ "strings"
+ "testing"
+)
+
+type args struct {
+ tag string
+}
+type testdata struct {
+ name string
+ args args
+ want string
+ wantErr bool
+}
+
+func TestCleanTag(t *testing.T) {
+ tests := []testdata{
+ // No match with any pattern tests
+ {
+ name: "Test valid SemVer format",
+ args: args{
+ tag: "1.2.3",
+ },
+ want: "1.2.3",
+ wantErr: false,
+ },
+ {
+ name: "Test pattern that cannot be converted to SemVer",
+ args: args{
+ tag: "latest",
+ },
+ want: "latest",
+ wantErr: false,
+ },
+ {
+ name: "Test empty tag",
+ args: args{
+ tag: "",
+ },
+ want: "",
+ wantErr: true,
+ },
+ {
+ name: "Test tag with only whitespace",
+ args: args{
+ tag: " ",
+ },
+ want: "",
+ wantErr: true,
+ },
+ {
+ name: "Test full tag with digest",
+ args: args{
+ tag: "1.2.3@sha256:abc123",
+ },
+ want: "1.2.3",
+ },
+ {
+ name: "Test tag with longer version and `-suffix`",
+ args: args{
+ tag: "1.2.3.4-suffix",
+ },
+ want: "1.2.3.4",
+ },
+ {
+ name: "Test tag with semver and `-suffix`",
+ args: args{
+ tag: "1.2.3-abc12367",
+ },
+ want: "1.2.3",
+ },
+ {
+ name: "Test tag with calver and `-suffix`",
+ args: args{
+ tag: "2023.11.2-abc12367",
+ },
+ want: "2023.11.2",
+ },
+ {
+ name: "Test with invalid label format",
+ args: args{
+ tag: strings.Repeat("a", 300),
+ },
+ want: "",
+ wantErr: true,
+ },
+ // cleanSha tests
+ {
+ name: "Test cleanSha",
+ args: args{
+ tag: "1.2.3@sha256:abc123",
+ },
+ want: "1.2.3",
+ },
+ // cleanLeadingSymbol tests
+ {
+ name: "Test cleanLeadingSymbol ($)",
+ args: args{
+ tag: "$1.2.3",
+ },
+ want: "1.2.3",
+ },
+ {
+ name: "Test cleanLeadingSymbol (v)",
+ args: args{
+ tag: "v1.2.3",
+ },
+ want: "1.2.3",
+ },
+ {
+ name: "Test cleanLeadingSymbol (version)",
+ args: args{
+ tag: "version-a78f38c1",
+ },
+ want: "a78f38c1",
+ },
+ // keepShortCommitHashSuffix tests
+ {
+ name: "Test keepShortCommitHashSuffix (something-hash)",
+ args: args{
+ tag: "something-abcd123",
+ },
+ want: "abcd123",
+ },
+ {
+ name: "Test keepShortCommitHashSuffix (version-hash)",
+ args: args{
+ tag: "version-abcd123",
+ },
+ want: "abcd123",
+ },
+ // cleanIncompleteSemVer tests
+ {
+ name: "Test cleanIncompleteSemVer (2 parts)",
+ args: args{
+ tag: "1.2",
+ },
+ want: "1.2.0",
+ },
+ {
+ name: "Test cleanIncompleteSemVer (1 part)",
+ args: args{
+ tag: "1",
+ },
+ want: "1.0.0",
+ },
+ {
+ name: "Test cleanIncompleteSemVer (more than 3 parts)",
+ args: args{
+ tag: "1.2.3.4.5",
+ },
+ want: "1.2.3.4.5",
+ },
+ {
+ name: "Test cleanIncompleteSemVer (with suffix)",
+ args: args{
+ tag: "2.440-jdk17",
+ },
+ want: "2.440.0",
+ },
+ // cleanYearMonthDay tests
+ {
+ name: "Test cleanYearMonthDay (year-month-day)",
+ args: args{
+ tag: "2023-11-15",
+ },
+ want: "2023.11.15",
+ },
+ {
+ name: "Test cleanYearMonthDay (year-month)",
+ args: args{
+ tag: "2022-04",
+ },
+ want: "2022.4.0",
+ },
+ {
+ name: "Test cleanYearMonthDay (with prefix)",
+ args: args{
+ tag: "latest-2023-12-18",
+ },
+ want: "2023.12.18",
+ },
+ // cleanPrefix tests
+ {
+ name: "Test cleanPrefix (random prefix)",
+ args: args{
+ tag: "abc123-v1.2.3",
+ },
+ want: "1.2.3",
+ },
+ {
+ name: "Test cleanPrefix (version prefix)",
+ args: args{
+ tag: "version-1.2.3",
+ },
+ want: "1.2.3",
+ },
+ // cleanArch tests
+ {
+ name: "Test cleanArch",
+ args: args{
+ tag: "x64-1.2.3",
+ },
+ want: "1.2.3",
+ },
+ // cleanRelease tests
+ {
+ name: "Test cleanRelease",
+ args: args{
+ tag: "RELEASE.2023-11-20T22-40-07Z",
+ },
+ want: "2023.11.20",
+ },
+ // cleanStupidSemVerLike tests
+ {
+ name: "Test cleanStupidSemVerLike",
+ args: args{
+ tag: "v.1.2.3",
+ },
+ want: "1.2.3",
+ },
+ {
+ name: "Test cleanStupidSemVerLike (2)",
+ args: args{
+ tag: "V.1.2.3",
+ },
+ want: "1.2.3",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+
+ got, err := CleanTag(tt.args.tag)
+ // If we expected an error, but didn't get one, fail the test
+ if (err != nil) != tt.wantErr {
+ t.Errorf("CleanTag() error = %v, wantErr %t", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("CleanTag() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+func TestCheckValidLabelValue(t *testing.T) {
+ tests := []testdata{
+ {
+ name: "Test invalid label format",
+ args: args{
+ tag: "1.2.3@sha256:abc123",
+ },
+ wantErr: true,
+ },
+ {
+ name: "Test valid label format",
+ args: args{
+ tag: "1.2.3",
+ },
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+
+ err := checkValidLabelValue(tt.args.tag)
+ // If we expected an error, but didn't get one, fail the test
+ if (err != nil) != tt.wantErr {
+ t.Errorf("checkValidLabelValue() error = %v, wantErr %t", err, tt.wantErr)
+ return
+ }
+ })
+ }
+}
diff --git a/clustertool/pkg/charts/image/image.go b/clustertool/pkg/charts/image/image.go
new file mode 100644
index 0000000000000..28cbfe1c7c70f
--- /dev/null
+++ b/clustertool/pkg/charts/image/image.go
@@ -0,0 +1,146 @@
+package image
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/knadh/koanf/parsers/yaml"
+ "github.com/knadh/koanf/providers/file"
+ "github.com/knadh/koanf/v2"
+ "github.com/rs/zerolog/log"
+)
+
+// Images represents the structure of values.yaml.
+type Images struct {
+ ImagesMap map[string]ImageDetails
+ K *koanf.Koanf
+}
+
+// ImageDetails represents details for each image.
+type ImageDetails struct {
+ Repository string `yaml:"repository"`
+ Tag string `yaml:"tag"`
+ Version string
+ Link string
+ // Add other fields as needed
+}
+
+var imageRegex = regexp.MustCompile(`^image|[a-zA-Z0-9]+Image$`)
+
+func (i *Images) LoadValuesFile(filename string) error {
+ // Initialize koanf instance
+ i.K = koanf.New(".")
+
+ // Load YAML file using koanf
+ if err := i.K.Load(file.Provider(filename), yaml.Parser()); err != nil {
+ return err
+ }
+
+ // List only root-level keys that match the criteria
+ keys := getFilteredRootLevelKeys(i.K)
+ i.ImagesMap = make(map[string]ImageDetails)
+ for _, key := range keys {
+ // Extract relevant fields from the loaded configuration
+ var img ImageDetails
+ if err := i.K.Unmarshal(key, &img); err != nil {
+ return err
+ }
+
+ // Set the Link field based on the repository
+ img.Link = constructLink(img.Repository)
+
+ // Set the Version field based on the tag
+ version, err := CleanTag(img.Tag)
+ if err != nil {
+ log.Error().Err(err).Msg("❌ Failed to clean tag")
+ }
+
+ img.Version = version
+
+ // Save the extracted values to the struct
+ i.ImagesMap[key] = img
+ }
+
+ return nil
+}
+
+func getFilteredRootLevelKeys(k *koanf.Koanf) []string {
+ filteredKeys := []string{}
+
+ // k.Raw() returns a map[string]interface{} with all the keys and their values
+ // This means the keys will only be the root-level keys, we can drill into the
+ // values later if we want the nested keys.
+ for key := range k.Raw() {
+ if key == "imageSelector" {
+ log.Error().Msg("❌ Found [imageSelector] in top level keys, this is not supported.")
+ continue
+ }
+ // Filter keys that match the regex
+ if imageRegex.MatchString(key) {
+ filteredKeys = append(filteredKeys, key)
+ }
+ }
+
+ return filteredKeys
+}
+
+// constructLink constructs a link based on the repository using the logic from the main function.
+func constructLink(repository string) string {
+ prefix := ""
+
+ switch {
+ case strings.HasPrefix(repository, "lscr.io/linuxserver/"):
+ prefix = "https://fleet.linuxserver.io/image?name="
+ repository = strings.TrimPrefix(repository, "lscr.io/")
+ case strings.HasPrefix(repository, "tccr.io/tccr/"):
+ prefix = "https://github.com/truecharts/containers/tree/master/apps/"
+ repository = strings.TrimPrefix(repository, "tccr.io/tccr/")
+ case strings.HasPrefix(repository, "mcr.microsoft.com/"):
+ prefix = "https://mcr.microsoft.com/en-us/product/"
+ repository = strings.TrimPrefix(repository, "mcr.microsoft.com/")
+ case strings.HasPrefix(repository, "public.ecr.aws/"):
+ prefix = "https://gallery.ecr.aws/"
+ repository = strings.TrimPrefix(repository, "public.ecr.aws/")
+ case strings.HasPrefix(repository, "ghcr.io/"):
+ prefix = "https://"
+ case strings.HasPrefix(repository, "quay.io/"):
+ prefix = "https://"
+ case strings.HasPrefix(repository, "gcr.io/"):
+ prefix = "https://"
+ case strings.Contains(repository, ".azurecr.io/"):
+ reg := fmt.Sprintf(`%s.azurecr.io/`, strings.Split(repository, ".")[0])
+ prefix = fmt.Sprintf("https://%s", reg)
+ repository = strings.TrimPrefix(repository, reg)
+ case strings.Contains(repository, ".ocir.io/"):
+ prefix = ""
+ default:
+ // Docker Hub or unknown registry
+ prefix = "https://hub.docker.com/r/"
+ repository = strings.TrimPrefix(repository, "docker.io/")
+ repository = strings.TrimPrefix(repository, "index.docker.io/")
+ repository = strings.TrimPrefix(repository, "registry-1.docker.io/")
+ repository = strings.TrimPrefix(repository, "registry.hub.docker.com/")
+
+ // Check for Docker Official Image
+ if strings.Count(repository, "/") == 0 || strings.HasPrefix(repository, "library/") {
+ prefix = "https://hub.docker.com/_/"
+ repository = strings.TrimPrefix(repository, "library/")
+ }
+
+ // Avoid creating a bad link if the image name has more than 1 slash
+ slashes := strings.Count(repository, "/")
+ if slashes > 1 {
+ prefix = ""
+ log.Warn().Msgf("WARNING: Could not determine source repository url for [%s]", repository)
+ }
+ }
+
+ if prefix == "" {
+ log.Warn().Msgf("WARNING: Could not determine source repository url for [%s]", repository)
+ return ""
+ }
+
+ containerURL := fmt.Sprintf("%s%s", prefix, repository)
+ return containerURL
+}
diff --git a/clustertool/pkg/charts/image/image_test.go b/clustertool/pkg/charts/image/image_test.go
new file mode 100644
index 0000000000000..e3bc38b4f6212
--- /dev/null
+++ b/clustertool/pkg/charts/image/image_test.go
@@ -0,0 +1,171 @@
+package image
+
+import (
+ "fmt"
+ "reflect"
+ "testing"
+)
+
+func TestLoadValuesFile(t *testing.T) {
+ type TestData struct {
+ name string
+ valuesFile string
+ expected map[string]ImageDetails
+ wantErr bool
+ }
+ testDataPath := "../../testdata/values_yaml"
+ tests := []TestData{
+ {
+ name: "Test malformed file",
+ valuesFile: "malformedValues.yaml",
+ expected: nil,
+ wantErr: true,
+ },
+ {
+ name: "Test empty file",
+ valuesFile: "emptyValues.yaml",
+ expected: nil,
+ wantErr: false,
+ },
+ {
+ name: "Test single image file",
+ valuesFile: "singleImageValues.yaml",
+ expected: map[string]ImageDetails{
+ "image": {
+ Repository: "nginx",
+ Tag: "1.15.8",
+ Link: "https://hub.docker.com/_/nginx",
+ Version: "1.15.8",
+ },
+ },
+ },
+ {
+ name: "Test multiple image file",
+ valuesFile: "multiImageValues.yaml",
+ expected: map[string]ImageDetails{
+ "image": {
+ Repository: "author/image",
+ Tag: "1.0.0",
+ Link: "https://hub.docker.com/r/author/image",
+ Version: "1.0.0",
+ },
+ "dockerHub1Image": {
+ Repository: "docker.io/author/image",
+ Tag: "1.0.0",
+ Link: "https://hub.docker.com/r/author/image",
+ Version: "1.0.0",
+ },
+ "dockerHub2Image": {
+ Repository: "index.docker.io/author/image",
+ Tag: "1.0.0",
+ Link: "https://hub.docker.com/r/author/image",
+ Version: "1.0.0",
+ },
+ "dockerHub3Image": {
+ Repository: "registry-1.docker.io/author/image",
+ Tag: "1.0.0",
+ Link: "https://hub.docker.com/r/author/image",
+ Version: "1.0.0",
+ },
+ "dockerHub4Image": {
+ Repository: "registry.hub.docker.com/author/image",
+ Tag: "1.0.0",
+ Link: "https://hub.docker.com/r/author/image",
+ Version: "1.0.0",
+ },
+ "dockerHub5Image": {
+ Repository: "image",
+ Tag: "1.0.0",
+ Link: "https://hub.docker.com/_/image",
+ Version: "1.0.0",
+ },
+ "dockerHub6Image": {
+ Repository: "library/image",
+ Tag: "1.0.0",
+ Link: "https://hub.docker.com/_/image",
+ Version: "1.0.0",
+ },
+ "lscrImage": {
+ Repository: "lscr.io/linuxserver/image",
+ Tag: "1.0.0",
+ Link: "https://fleet.linuxserver.io/image?name=linuxserver/image",
+ Version: "1.0.0",
+ },
+ "tccrImage": {
+ Repository: "tccr.io/tccr/image",
+ Tag: "1.0.0",
+ Link: "https://github.com/truecharts/containers/tree/master/apps/image",
+ Version: "1.0.0",
+ },
+ "mcrImage": {
+ Repository: "mcr.microsoft.com/author/image",
+ Tag: "1.0.0",
+ Link: "https://mcr.microsoft.com/en-us/product/author/image",
+ Version: "1.0.0",
+ },
+ "ecrImage": {
+ Repository: "public.ecr.aws/author/image",
+ Tag: "1.0.0",
+ Link: "https://gallery.ecr.aws/author/image",
+ Version: "1.0.0",
+ },
+ "ghcrImage": {
+ Repository: "ghcr.io/author/image",
+ Tag: "1.0.0",
+ Link: "https://ghcr.io/author/image",
+ Version: "1.0.0",
+ },
+ "quayImage": {
+ Repository: "quay.io/author/image",
+ Tag: "1.0.0",
+ Link: "https://quay.io/author/image",
+ Version: "1.0.0",
+ },
+ "gcrImage": {
+ Repository: "gcr.io/author/image",
+ Tag: "1.0.0",
+ Link: "https://gcr.io/author/image",
+ Version: "1.0.0",
+ },
+ "azurecrImage": {
+ Repository: "author.azurecr.io/image",
+ Tag: "1.0.0",
+ Link: "https://author.azurecr.io/image",
+ Version: "1.0.0",
+ },
+ "ocirImage": {
+ Repository: "author.ocir.io/image",
+ Tag: "1.0.0",
+ Link: "",
+ Version: "1.0.0",
+ },
+ "unknownImage": {
+ Repository: "unknown.io/author/image",
+ Tag: "1.0.0",
+ Link: "",
+ Version: "1.0.0",
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var images Images
+ err := images.LoadValuesFile(fmt.Sprintf("%s/%s", testDataPath, tt.valuesFile))
+ if (err != nil) != tt.wantErr {
+ t.Errorf("LoadValuesFile() error = %v, wantErr %v", err, tt.wantErr)
+ }
+
+ if tt.expected == nil && len(images.ImagesMap) > 0 {
+ t.Errorf("LoadValuesFile() expected = %+v, got %+v", tt.expected, images.ImagesMap)
+ }
+
+ if tt.expected != nil {
+ if !reflect.DeepEqual(images.ImagesMap, tt.expected) {
+ t.Errorf("LoadValuesFile() expected = %+v, got %+v", tt.expected, images.ImagesMap)
+ }
+ }
+ })
+ }
+}
diff --git a/clustertool/pkg/charts/info/info.go b/clustertool/pkg/charts/info/info.go
new file mode 100644
index 0000000000000..90073ab370505
--- /dev/null
+++ b/clustertool/pkg/charts/info/info.go
@@ -0,0 +1,61 @@
+package info
+
+import (
+ "runtime/debug"
+ "time"
+
+ "github.com/rs/zerolog/log"
+)
+
+type Data struct {
+ GoVersion string
+ GoArch string
+ GoOS string
+ GoC bool
+ GitCommit string
+ GitDate time.Time
+ GitDirty bool
+}
+
+func NewInfo() *Data {
+ info, _ := debug.ReadBuildInfo()
+ data := &Data{
+ GoVersion: info.GoVersion,
+ }
+
+ // Available info: https://github.com/golang/go/blob/master/src/runtime/debug/mod.go#L73
+ for _, kv := range info.Settings {
+ switch kv.Key {
+ case "GOARCH":
+ data.GoArch = kv.Value
+ case "GOOS":
+ data.GoOS = kv.Value
+ case "CGO_ENABLED":
+ data.GoC = kv.Value == "1"
+ case "vcs.revision":
+ data.GitCommit = kv.Value
+ case "vcs.time":
+ data.GitDate, _ = time.Parse(time.RFC3339, kv.Value)
+ case "vcs.modified":
+ data.GitDirty = kv.Value == "true"
+ }
+ }
+
+ return data
+}
+
+func (d *Data) Print() {
+ log.Info().Msgf(`
+Charttool is a tool for managing TrueCharts charts.
+
+Go
+ Version: %s
+ OS: %s
+ Arch: %s
+ CGO: %t
+Git
+ Commit: %s
+ Date: %s
+ Dirty: %t
+`, d.GoVersion, d.GoOS, d.GoArch, d.GoC, d.GitCommit, d.GitDate, d.GitDirty)
+}
diff --git a/clustertool/pkg/charts/readme/readme.go b/clustertool/pkg/charts/readme/readme.go
new file mode 100644
index 0000000000000..99479a4c106a2
--- /dev/null
+++ b/clustertool/pkg/charts/readme/readme.go
@@ -0,0 +1,35 @@
+package readme
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+)
+
+func GenerateReadme(templatePath string, chartPath string, chartName string, train string) error {
+ // Define file paths
+ template := filepath.Join(templatePath, "templates/README.md.tpl")
+ target := filepath.Join(filepath.Dir(chartPath), "README.md")
+
+ // Read template file
+ templateContent, err := os.ReadFile(template)
+ if err != nil {
+ return fmt.Errorf("failed to read template file: %v", err)
+ }
+
+ // Replace placeholders in the template
+ readmeContent := strings.ReplaceAll(string(templateContent), "TRAINPLACEHOLDER", train)
+ readmeContent = strings.ReplaceAll(readmeContent, "CHARTPLACEHOLDER", chartName)
+
+ // Write the modified content to the README.md file in the chart directory
+ err = os.WriteFile(target, []byte(readmeContent), 0644)
+ if err != nil {
+ return fmt.Errorf("failed to write README.md file: %v", err)
+ }
+
+ log.Info().Msgf("Generated README.md for [%s] in [%s] train", chartName, train)
+ return nil
+}
diff --git a/clustertool/pkg/charts/valuesYaml/addonValues.go b/clustertool/pkg/charts/valuesYaml/addonValues.go
new file mode 100644
index 0000000000000..53a8a6afe4aa6
--- /dev/null
+++ b/clustertool/pkg/charts/valuesYaml/addonValues.go
@@ -0,0 +1,60 @@
+package valuesYaml
+
+// Addons represents the schema for the 'addons' section.
+type Addons struct {
+ Codeserver Codeserver `yaml:"codeserver,omitempty" schema:"additional_attrs:true,type:dict"`
+ Netshoot Netshoot `yaml:"netshoot,omitempty" schema:"additional_attrs:true,type:dict"`
+ VPN VPN `yaml:"vpn,omitempty" schema:"additional_attrs:true,type:dict"`
+}
+
+// Codeserver represents the schema for the 'codeserver' addon.
+type Codeserver struct {
+ Enabled bool `yaml:"enabled" schema:"type:boolean,default:false,show_subquestions_if:true"`
+ Service ServiceConfiguration `yaml:"service,omitempty" schema:"additional_attrs:true,type:dict"`
+ Ingress IngressConfiguration `yaml:"ingress,omitempty" schema:"additional_attrs:true,type:dict"`
+ EnvList []EnvList `yaml:"envList,omitempty" schema:"type:list,default:[],items:type:dict,show_if:[[type,!=,disabled]]"`
+}
+
+// Netshoot represents the schema for the 'netshoot' addon.
+type Netshoot struct {
+ Enabled bool `yaml:"enabled" schema:"type:boolean,default:false,show_subquestions_if:true"`
+ EnvList []EnvList `yaml:"envList,omitempty" schema:"type:list,default:[],items:type:dict,show_if:[['type','!=','disabled']]"`
+}
+
+// VPN represents the schema for the 'vpn' addon.
+type VPN struct {
+ Type string `yaml:"type,omitempty" schema:"type:string,default:disabled,enum:,disabled,gluetun,tailscale,openvpn,wireguard"`
+ OpenVPN OpenVPNSettings `yaml:"openvpn,omitempty" schema:"additional_attrs:true,type:dict,show_if:[[type,=,openvpn]]"`
+ Tailscale TailscaleSettings `yaml:"tailscale,omitempty" schema:"additional_attrs:true,type:dict,show_if:[[type,=,tailscale]]"`
+ KillSwitch bool `yaml:"killSwitch" schema:"type:boolean,show_if:[[type,!=,disabled]],default:true"`
+ ExcludedNetworksIPv4 []ExcludedNetwork `yaml:"excludedNetworks_IPv4,omitempty" schema:"type:list,default:[],items:type:dict,show_if:[[type,!=,disabled]]"`
+ ExcludedNetworksIPv6 []ExcludedNetwork `yaml:"excludedNetworks_IPv6,omitempty" schema:"type:list,default:[],items:type:dict,show_if:[[type,!=,disabled]]"`
+ ConfigFile string `yaml:"configFile,omitempty" schema:"type:string,show_if:[[type,!=,disabled]]"`
+ EnvList []EnvList `yaml:"envList,omitempty" schema:"type:list,default:[],items:type:dict,show_if:[['type','!=','disabled']]"`
+}
+
+// OpenVPNSettings represents the schema for OpenVPN settings.
+type OpenVPNSettings struct {
+ Username string `yaml:"username,omitempty" schema:"type:string,default:''"`
+ Password string `yaml:"password,omitempty" schema:"type:string,show_if:[[username,!=,]],default:''"`
+}
+
+// TailscaleSettings represents the schema for Tailscale settings.
+type TailscaleSettings struct {
+ AuthKey string `yaml:"authkey,omitempty" schema:"type:string,private:true,default:''"`
+ AuthOnce bool `yaml:"auth_once,omitempty" schema:"type:boolean,default:true"`
+ AcceptDNS bool `yaml:"accept_dns,omitempty" schema:"type:boolean,default:false"`
+ Userspace bool `yaml:"userspace,omitempty" schema:"type:boolean,default:false"`
+ Routes string `yaml:"routes,omitempty" schema:"type:string,default:''"`
+ DestIP string `yaml:"dest_ip,omitempty" schema:"type:string,default:''"`
+ Sock5Server string `yaml:"sock5_server,omitempty" schema:"type:string,default:''"`
+ OutboundHTTPProxyListen string `yaml:"outbound_http_proxy_listen,omitempty" schema:"type:string,default:''"`
+ ExtraArgs string `yaml:"extra_args,omitempty" schema:"type:string,default:''"`
+ DaemonExtraArgs string `yaml:"daemon_extra_args,omitempty" schema:"type:string,default:''"`
+}
+
+// ExcludedNetwork represents the schema for an excluded network in the killswitch.
+type ExcludedNetwork struct {
+ NetworkV4 string `yaml:"networkv4,omitempty" schema:"type:string,required:true"`
+ NetworkV6 string `yaml:"networkv6,omitempty" schema:"type:string,required:true"`
+}
diff --git a/clustertool/pkg/charts/valuesYaml/externalInterfaceValues.go b/clustertool/pkg/charts/valuesYaml/externalInterfaceValues.go
new file mode 100644
index 0000000000000..685e632dfc41d
--- /dev/null
+++ b/clustertool/pkg/charts/valuesYaml/externalInterfaceValues.go
@@ -0,0 +1,36 @@
+package valuesYaml
+
+// InterfaceConfiguration represents the configuration for an interface.
+type InterfaceConfiguration struct {
+ HostInterface string `yaml:"hostInterface,omitempty" schema:"type:string" required:"true" description:"Host Interface"`
+}
+
+// IPAMConfiguration represents the configuration for IP Address Management.
+type IPAMConfiguration struct {
+ Type string `yaml:"type,omitempty" schema:"type:string" required:"true" enum:"[dhcp, static]" description:"IPAM Type"`
+ StaticIPConfigurations []string `yaml:"staticIPConfigurations,omitempty" schema:"type:list" show_if:"[['type', '=', 'static']]" items:"type:ipaddr,cidr:true" description:"Static IP Addresses"`
+ StaticRoutes []StaticRouteConfiguration `yaml:"staticRoutes,omitempty" schema:"type:list" show_if:"[['type', '=', 'static']]" description:"Static Routes"`
+}
+
+// StaticRouteConfiguration represents the configuration for a static route.
+type StaticRouteConfiguration struct {
+ Destination string `yaml:"destination,omitempty" schema:"type:ipaddr,cidr:true" required:"true" description:"Destination"`
+ Gateway string `yaml:"gateway,omitempty" schema:"type:ipaddr,cidr:false" required:"true" description:"Gateway"`
+}
+
+// NetworkingExpertConfiguration represents the expert configuration for networking.
+type NetworkingExpertConfiguration struct {
+ ScaleExternalInterface bool `yaml:"scaleExternalInterface,omitempty" schema:"type:boolean" default:"false" show_subquestions_if:"true" description:"Add External Interfaces"`
+ InterfaceConfiguration InterfaceConfiguration `yaml:"interfaceConfiguration,omitempty" schema:"type:dict" $ref:"normalize/interfaceConfiguration" description:"Interface Configuration"`
+ IPAM IPAMConfiguration `yaml:"ipam,omitempty" schema:"type:dict" required:"true" description:"IP Address Management"`
+}
+
+// ServiceExpertConfiguration represents the expert configuration for services.
+type ServiceExpertConfiguration struct {
+ ScaleExternalInterface bool `yaml:"scaleExternalInterface,omitempty" schema:"type:boolean" default:"false" show_subquestions_if:"true" description:"Add External Interfaces"`
+}
+
+// NetworkingConfiguration represents the configuration for networking.
+type NetworkingConfiguration struct {
+ ExpertConfiguration NetworkingExpertConfiguration `yaml:"expertConfiguration,omitempty" schema:"type:boolean" default:"false" show_subquestions_if:"true" description:"Show Expert Config"`
+}
diff --git a/clustertool/pkg/charts/valuesYaml/ingressValues.go b/clustertool/pkg/charts/valuesYaml/ingressValues.go
new file mode 100644
index 0000000000000..b8aeb4ca37c6b
--- /dev/null
+++ b/clustertool/pkg/charts/valuesYaml/ingressValues.go
@@ -0,0 +1,59 @@
+package valuesYaml
+
+// MiddlewareEntry represents the schema for a middleware entry.
+type MiddlewareEntry struct {
+ Name string `yaml:"name" schema:"type:string,default:'',required:true"`
+}
+
+// Integration represents the schema for integrations.
+type Integration struct {
+ Homepage IntegrationHomepage `yaml:"homepage" schema:"additional_attrs:true,type:dict"`
+}
+
+// IntegrationHomepage represents the schema for the Homepage integration.
+type IntegrationHomepage struct {
+ Enabled bool `yaml:"enabled" schema:"type:boolean,default:false"`
+ Name string `yaml:"name" schema:"type:string,default:'',show_if:[[enabled,=,true]]"`
+ Description string `yaml:"description" schema:"type:string,default:'',show_if:[[enabled,=,true]]"`
+ Group string `yaml:"group" schema:"type:string,default:default,show_if:[[enabled,=,true]]"`
+}
+
+// TLSEntry represents the schema for a TLS entry.
+type TLSEntry struct {
+ Host []string `yaml:"hosts" schema:"type:list,default:[],items:type:string,required:true"`
+ CertificateIssuer string `yaml:"certificateIssuer" schema:"type:string,default:''"`
+ ClusterCertificate string `yaml:"clusterCertificate" schema:"type:string,show_if:[[certificateIssuer,=,]]"`
+ SecretName string `yaml:"secretName" schema:"type:string,show_if:[[certificateIssuer,=,]]"`
+ ScaleCert int `yaml:"scaleCert" schema:"type:int,show_if:[[certificateIssuer,=,]]"`
+}
+
+// IngressConfiguration represents the schema for Ingress settings with variable name.
+type IngressConfiguration struct {
+ Enabled bool `yaml:"enabled" schema:"type:boolean,default:true,hidden:true"`
+ Name string `yaml:"name" schema:"type:string,default:''"`
+ IngressClassName string `yaml:"ingressClassName" schema:"type:string,default:''"`
+ AllowCors bool `yaml:"allowCors" schema:"type:boolean,show_if:[[advanced,=,true]],default:false"`
+ Hosts []HostEntry `yaml:"hosts" schema:"type:list,default:[],items:type:dict"`
+ CertificateIssuer string `yaml:"certificateIssuer" schema:"type:string,default:''"`
+ TLS []TLSEntry `yaml:"tls" schema:"type:list,default:[],items:type:dict,show_if:[[certificateIssuer,=,]]"`
+ Integration Integration `yaml:"integration" schema:"additional_attrs:true,type:dict"`
+ Entrypoint string `yaml:"entrypoint" schema:"type:string,default:websecure,required:true"`
+ Middlewares []MiddlewareEntry `yaml:"middlewares" schema:"type:list,default:[],items:type:dict"`
+}
+
+// HostEntry represents the schema for a host entry.
+type HostEntry struct {
+ Host string `yaml:"host" schema:"type:string,default:'',required:true"`
+ Paths []PathEntry `yaml:"paths" schema:"type:list,default:[{path:/,pathType:Prefix}],items:type:dict"`
+}
+
+// PathEntry represents the schema for a path entry.
+type PathEntry struct {
+ Path string `yaml:"path" schema:"type:string,required:true,default:/"`
+ PathType string `yaml:"pathType" schema:"type:string,required:true,default:Prefix"`
+}
+
+// RootReference represents the root-level reference for Ingress settings.
+type RootReference struct {
+ Ingress map[string]IngressConfiguration `yaml:"ingress" schema:"additional_attrs:true,type:dict" description:"Ingress Settings"`
+}
diff --git a/clustertool/pkg/charts/valuesYaml/metricsValues.go b/clustertool/pkg/charts/valuesYaml/metricsValues.go
new file mode 100644
index 0000000000000..ad0b7a005873c
--- /dev/null
+++ b/clustertool/pkg/charts/valuesYaml/metricsValues.go
@@ -0,0 +1,16 @@
+package valuesYaml
+
+// PrometheusRule represents the schema for Prometheus rule settings.
+type PrometheusRule struct {
+ Enabled bool `yaml:"enabled" schema:"type:boolean,default:false"`
+}
+
+// MetricsConfiguration represents the metrics configuration.
+type MetricsConfiguration struct {
+ Enabled bool `yaml:"enabled"`
+ PrometheusRule struct {
+ Enabled bool `yaml:"enabled"`
+ // ... other prometheusRule configuration
+ } `yaml:"prometheusRule"`
+ // ... other metrics configuration
+}
diff --git a/clustertool/pkg/charts/valuesYaml/networkPolicyValues.go b/clustertool/pkg/charts/valuesYaml/networkPolicyValues.go
new file mode 100644
index 0000000000000..27dfa63453d8e
--- /dev/null
+++ b/clustertool/pkg/charts/valuesYaml/networkPolicyValues.go
@@ -0,0 +1,80 @@
+package valuesYaml
+
+// NetworkPolicyEntry represents the schema for a network policy entry.
+type NetworkPolicyEntry struct {
+ Name string `yaml:"name" schema:"type:string,required:true,default:''"`
+ Enabled bool `yaml:"enabled" schema:"type:boolean,default:false,show_subquestions_if:true"`
+ Policy string `yaml:"policyType" schema:"type:string,default:'',enum:,ingress,egress,ingress-egress"`
+ Ingress []IngressEntry `yaml:"ingress" schema:"type:list,default:[],items:type:dict"`
+ Egress []EgressEntry `yaml:"egress" schema:"type:list,default:[],items:type:dict"`
+}
+
+// IngressEntry represents the schema for an ingress entry.
+type IngressEntry struct {
+ From []FromEntry `yaml:"from" schema:"type:list,default:[],items:type:dict"`
+ NamespaceSel NamespaceSel `yaml:"namespaceSelector" schema:"additional_attrs:true,type:dict"`
+ PodSel PodSel `yaml:"podSelector" schema:"additional_attrs:true,type:dict"`
+ Ports []PortsEntry `yaml:"ports" schema:"type:list,default:[],items:type:dict"`
+}
+
+// EgressEntry represents the schema for an egress entry.
+type EgressEntry struct {
+ To []ToEntry `yaml:"to" schema:"type:list,default:[],items:type:dict"`
+ NamespaceSel NamespaceSel `yaml:"namespaceSelector" schema:"additional_attrs:true,type:dict"`
+ PodSel PodSel `yaml:"podSelector" schema:"additional_attrs:true,type:dict"`
+ Ports []PortsEntry `yaml:"ports" schema:"type:list,default:[],items:type:dict"`
+}
+
+// FromEntry represents the schema for a 'from' entry.
+type FromEntry struct {
+ IPBlock IPBlock `yaml:"ipBlock" schema:"additional_attrs:true,type:dict"`
+ NamespaceSel NamespaceSel `yaml:"namespaceSelector" schema:"additional_attrs:true,type:dict"`
+ PodSel PodSel `yaml:"podSelector" schema:"additional_attrs:true,type:dict"`
+}
+
+// ToEntry represents the schema for a 'to' entry.
+type ToEntry struct {
+ IPBlock IPBlock `yaml:"ipBlock" schema:"additional_attrs:true,type:dict"`
+ NamespaceSel NamespaceSel `yaml:"namespaceSelector" schema:"additional_attrs:true,type:dict"`
+ PodSel PodSel `yaml:"podSelector" schema:"additional_attrs:true,type:dict"`
+}
+
+// IPBlock represents the schema for an IP block.
+type IPBlock struct {
+ CIDR string `yaml:"cidr" schema:"type:string,default:''"`
+ Except []Except `yaml:"except" schema:"type:list,default:[],items:type:dict"`
+}
+
+// Except represents the schema for the 'except' field.
+type Except struct {
+ ExceptInt string `yaml:"exceptint" schema:"type:string"`
+}
+
+// NamespaceSel represents the schema for namespace selector.
+type NamespaceSel struct {
+ MatchExpressions []ExpressionEntry `yaml:"matchExpressions" schema:"type:list,default:[],items:type:dict"`
+}
+
+// PodSel represents the schema for pod selector.
+type PodSel struct {
+ MatchExpressions []ExpressionEntry `yaml:"matchExpressions" schema:"type:list,default:[],items:type:dict"`
+}
+
+// ExpressionEntry represents the schema for an expression entry.
+type ExpressionEntry struct {
+ Key string `yaml:"key" schema:"type:string"`
+ Operator string `yaml:"operator" schema:"type:string,default:TCP,enum:TCP,UDP,SCTP,In,NotIn,Exists,DoesNotExist"`
+ Values []Value `yaml:"values" schema:"type:list,default:[],items:type:dict"`
+}
+
+// Value represents the schema for a value entry.
+type Value struct {
+ Value string `yaml:"value" schema:"type:string"`
+}
+
+// PortsEntry represents the schema for a ports entry.
+type PortsEntry struct {
+ Port int `yaml:"port" schema:"type:int"`
+ EndPort int `yaml:"endPort" schema:"type:int"`
+ Protocol string `yaml:"protocol" schema:"type:string,default:TCP,enum:TCP,UDP,SCTP"`
+}
diff --git a/clustertool/pkg/charts/valuesYaml/parse.go b/clustertool/pkg/charts/valuesYaml/parse.go
new file mode 100644
index 0000000000000..ca60560c89ad5
--- /dev/null
+++ b/clustertool/pkg/charts/valuesYaml/parse.go
@@ -0,0 +1,113 @@
+package valuesYaml
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/knadh/koanf/parsers/yaml"
+ "github.com/knadh/koanf/providers/file"
+ "github.com/knadh/koanf/v2"
+)
+
+// TODO: enable validation
+// var validate *validator.Validate
+
+// GlobalSettings represents the "Global Settings" section of the schema.
+type GlobalSettings struct {
+ StopAll bool `yaml:"stopAll" description:"Stops All Running pods and hibernates cnpg" default:"false"`
+}
+
+// ImagePullSecretEntry represents an entry in the Image Pull Secrets configuration.
+type ImagePullSecretEntry struct {
+ Registry string `yaml:"registry" validate:"required" description:"Registry"`
+ Username string `yaml:"username" validate:"required" description:"Username"`
+ Password string `yaml:"password" validate:"required" description:"Password"`
+ Email string `yaml:"email" validate:"required" description:"Email"`
+}
+
+// Values represents the entire configuration.
+type Values struct {
+ Global GlobalSettings `yaml:"global"`
+ Workload map[string]WorkloadRootReference `yaml:"workload,omitempty" schema:"additional_attrs:true,type:dict"`
+ ImagePullSecretList []ImagePullSecretEntry `yaml:"imagePullSecretList,omitempty" schema:"type:list" items:"type:dict" additional_attrs:"true" description:"Image Pull Secrets"`
+ PodOptions PodOptions `yaml:"podOptions,omitempty" description:"Global Pod Options (Advanced)"`
+ Service map[string]ServiceConfiguration `yaml:"service,omitempty" schema:"additional_attrs:true,type:dict" description:"Service Settings"`
+ ServiceExpert ServiceExpertConfiguration `yaml:"serviceexpert,omitempty" schema:"type:boolean" default:"false" show_subquestions_if:"true" description:"Show Expert Config"`
+ ServiceList []ServiceConfiguration `yaml:"serviceList,omitempty" schema:"type:list,default:[]" items:"type:dict" description:"Add Manual Custom Services"`
+ Persistence map[string]Persistence `yaml:"persistence,omitempty" schema:"type:dict" description:"Integrated Persistent Storage"`
+ PersistenceList []Persistence `yaml:"persistenceList,omitempty" schema:"type:list,default:[]" description:"Additional App Storage"`
+ Ingress map[string]IngressConfiguration `yaml:"ingress,omitempty" schema:"additional_attrs:true,type:dict" description:"Ingress Settings"`
+ IngressList []IngressConfiguration `yaml:"ingressList,omitempty" schema:"type:list,default:[]"`
+ SecurityContext SecurityContext `yaml:"securityContext,omitempty" schema:"additional_attrs:true,type:dict"`
+ Resources Resources `yaml:"resources,omitempty" schema:"additional_attrs:true,type:dict"`
+ DeviceList DeviceList `yaml:"deviceList,omitempty" schema:"type:list,default:[]"`
+ ScaleGPU ScaleGPU `yaml:"scaleGPU,omitempty" schema:"type:list,default:[]"`
+ Metrics map[string]MetricsConfiguration `yaml:"metrics,omitempty" schema:"additional_attrs:true,type:dict"`
+ NetworkPolicy []NetworkPolicyEntry `yaml:"networkPolicy,omitempty" schema:"type:list,default:[]"`
+ Addons Addons `yaml:"addons,omitempty" schema:"additional_attrs:true,type:dict"`
+}
+
+// ValuesFile represents the entire values.yaml structure.
+type ValuesFile struct {
+ K *koanf.Koanf
+ Values Values `yaml:"metadata" validate:"required,dive"`
+}
+
+func NewValuesFile() *ValuesFile {
+ return &ValuesFile{
+ K: koanf.New("."),
+ }
+}
+
+// LoadFromFile loads values from a YAML file into the Helmvalues struct.
+func (v *ValuesFile) LoadFromFile(filename string) error {
+ if v.K == nil {
+ v.K = koanf.New(".")
+ }
+
+ if err := v.K.Load(file.Provider(filename), yaml.Parser()); err != nil {
+ return fmt.Errorf("error loading from file %s: %v", filename, err)
+ }
+
+ // Unmarshal the data into the values struct
+ if err := v.K.Unmarshal("", &v.Values); err != nil {
+ return fmt.Errorf("error unmarshalling data: %v", err)
+ }
+
+ // NOTE: Can be uncommented for debugging
+ // loadedData, _ := v.K.Marshal(yaml.Parser())
+ // log.Info().Msgf("Loaded struct data:\n%s\n", loadedData)
+
+ // Set default values for fields if they are not set or empty
+ v.setDefaultValues()
+
+ // TODO: enable validation
+ // Validate the loaded data
+ // if err := validate.Struct(v.Values); err != nil {
+ // return fmt.Errorf("values.yaml validation error: %v", err)
+ // }
+
+ return nil
+}
+
+// setDefaultValues sets default values for fields in valuesMetadata if they are not set or empty.
+func (v *ValuesFile) setDefaultValues() {
+ // Set default values for other fields as needed
+}
+
+// SaveToFile saves the Helm values metadata back to the values.yaml file.
+func (v *ValuesFile) SaveToFile(filename string) error {
+ // Marshal the existing metadata to YAML
+ loadedData, err := v.K.Marshal(yaml.Parser())
+ if err != nil {
+ return fmt.Errorf("error marshalling data: %v", err)
+ }
+
+ // Write the configuration to the file using os.WriteFile
+ err = os.WriteFile(filename, loadedData, 0644)
+ if err != nil {
+ return fmt.Errorf("error writing to file %s: %v", filename, err)
+ }
+
+ return nil
+}
diff --git a/clustertool/pkg/charts/valuesYaml/persistenceValues.go b/clustertool/pkg/charts/valuesYaml/persistenceValues.go
new file mode 100644
index 0000000000000..1ff7ca27e50ba
--- /dev/null
+++ b/clustertool/pkg/charts/valuesYaml/persistenceValues.go
@@ -0,0 +1,74 @@
+package valuesYaml
+
+// ISCSIOptions represents the schema for iSCSI Options.
+type ISCSIOptions struct {
+ TargetPortal string `yaml:"targetPortal" schema:"type:string,required:true" description:"targetPortal"`
+ IQN string `yaml:"iqn" schema:"type:string,required:true" description:"iqn"`
+ LUN int `yaml:"lun" schema:"type:int,default:0" description:"lun"`
+ AuthSession AuthSession `yaml:"authSession" schema:"type:dict,additional_attrs:true" description:"authSession"`
+ AuthDiscovery AuthDiscovery `yaml:"authDiscovery" schema:"type:dict,additional_attrs:true" description:"authDiscovery"`
+}
+
+// AuthSession represents the schema for authentication session in iSCSI Options.
+type AuthSession struct {
+ Username string `yaml:"username" schema:"type:string" description:"username"`
+ Password string `yaml:"password" schema:"type:string" description:"password"`
+ UsernameInitiator string `yaml:"usernameInitiator" schema:"type:string" description:"usernameInitiator"`
+ PasswordInitiator string `yaml:"passwordInitiator" schema:"type:string" description:"passwordInitiator"`
+}
+
+// AuthDiscovery represents the schema for authentication discovery in iSCSI Options.
+type AuthDiscovery struct {
+ Username string `yaml:"username" schema:"type:string" description:"username"`
+ Password string `yaml:"password" schema:"type:string" description:"password"`
+ UsernameInitiator string `yaml:"usernameInitiator" schema:"type:string" description:"usernameInitiator"`
+ PasswordInitiator string `yaml:"passwordInitiator" schema:"type:string" description:"passwordInitiator"`
+}
+
+// AutoPermissions represents the schema for Automatic Permissions Configuration.
+type AutoPermissions struct {
+ Enabled bool `yaml:"enabled" schema:"type:boolean,default:false,show_subquestions_if:true" description:"enabled"`
+ Chown bool `yaml:"chown" schema:"show_if:[[enabled,=,true]],type:boolean,default:false" description:"Run CHOWN"`
+ Chmod string `yaml:"chmod" schema:"show_if:[[enabled,=,true]],type:string,valid_chars:'[0-9]{3}'" description:"Run CHMOD"`
+ Recursive bool `yaml:"recursive" schema:"show_if:[[enabled,=,true]],type:boolean,default:false" description:"Recursive"`
+}
+
+// HostPathOptions represents the schema for Host Path Options.
+type HostPathOptions struct {
+ Path string `yaml:"path" schema:"type:string,required:true" description:"Path inside the container the storage is mounted"`
+}
+
+// StaticBinding represents the schema for Static Fixed PVC Bindings.
+type StaticBinding struct {
+ Mode string `yaml:"mode" schema:"type:string,default:disabled,enum:disabled,smb,nfs" description:"mode"`
+ Server string `yaml:"server" schema:"show_if:[[mode,!=,disabled]],type:string,default:'myserver'" description:"Server"`
+ Share string `yaml:"share" schema:"show_if:[[mode,!=,disabled]],type:string,default:'/myshare'" description:"Share"`
+ User string `yaml:"user" schema:"show_if:[[mode,=,smb]],type:string,default:'myuser'" description:"User"`
+ Domain string `yaml:"domain" schema:"show_if:[[mode,=,smb]],type:string" description:"Domain"`
+ Password string `yaml:"password" schema:"show_if:[[mode,=,smb]],type:string" description:"Password"`
+}
+
+// VolumeSnapshot represents the schema for Volume Snapshots.
+type VolumeSnapshot struct {
+ Name string `yaml:"name" schema:"type:string,default:mysnapshot" description:"Name"`
+ VolumeSnapshotClassName string `yaml:"volumeSnapshotClassName" schema:"type:string" description:"volumeSnapshot Class Name (Advanced)"`
+}
+
+// Persistence represents the schema for Integrated Persistent Storage.
+type Persistence struct {
+ Name string `yaml:"name,omitempty" schema:"type:string" description:"Custom storage name"`
+ Enabled bool `yaml:"enabled" schema:"type:boolean,default:true,hidden:true" description:"Enable Integrated Persistent Storage"`
+ Type string `yaml:"type" schema:"type:string,default:pvc,enum:pvc,hostPath,emptyDir,nfs,iscsi" description:"Sets the persistence type, Anything other than PVC could break rollback!"`
+ Server string `yaml:"server" schema:"show_if:[[type,=,nfs]]type:string default:''" description:"NFS Server"`
+ Path string `yaml:"path" schema:"show_if:[[type,=,nfs]],type:string" description:"Path on NFS Server"`
+ ISCSI ISCSIOptions `yaml:"iscsi" schema:"show_if:[[type,=,iscsi]],type:dict,additional_attrs:true" description:"iSCSI Options"`
+ AutoPermissions AutoPermissions `yaml:"autoPermissions" schema:"show_if:[[type,!=,pvc]],type:dict,additional_attrs:true" description:"Automatic Permissions Configuration"`
+ ReadOnly bool `yaml:"readOnly" schema:"type:boolean,default:false" description:"Read Only"`
+ HostPath HostPathOptions `yaml:"hostPath" schema:"show_if:[[type,=,hostPath]],type:hostpath" description:"Host Path"`
+ MountPath string `yaml:"mountPath" schema:"type:string,required:true,valid_chars:^\\/([a-zA-Z0-9._-]+(\\s?[a-zA-Z0.9._-]+|\\/?)$" description:"Path inside the container the storage is mounted"`
+ Medium string `yaml:"medium" schema:"show_if:[[type,=,emptyDir]],type:string,enum:'Memory'" description:"EmptyDir Medium"`
+ Size string `yaml:"size" schema:"show_if:[[type,=,pvc]],type:string,default:256Gi" description:"Size Quotum of Storage"`
+ StorageClass string `yaml:"storageClass" schema:"show_if:[[type,=,pvc]],type:string" description:"storageClass (Advanced)"`
+ Static StaticBinding `yaml:"static" schema:"show_if:[[type,=,pvc]],type:dict,additional_attrs:true" description:"Static Fixed PVC Bindings (Experimental)"`
+ VolumeSnapshots []VolumeSnapshot `yaml:"volumeSnapshots" schema:"show_if:[[type,=,pvc]],type:list,default:[]" description:"Volume Snapshots (Experimental)"`
+}
diff --git a/clustertool/pkg/charts/valuesYaml/podOptionsValues.go b/clustertool/pkg/charts/valuesYaml/podOptionsValues.go
new file mode 100644
index 0000000000000..859e82d92f348
--- /dev/null
+++ b/clustertool/pkg/charts/valuesYaml/podOptionsValues.go
@@ -0,0 +1,23 @@
+package valuesYaml
+
+// DNSConfigEntry represents an entry in the DNS configuration.
+type DNSConfigEntry struct {
+ Name string `yaml:"name,omitempty" validate:"required" description:"Name"`
+ Value string `yaml:"value,omitempty" validate:"required" description:"Value"`
+}
+
+// PodOptions represents the "Global Pod Options (Advanced)" section of the schema.
+type PodOptions struct {
+ ExpertPodOpts struct {
+ Type bool `yaml:"expertPodOpts" description:"Expert - Pod Options" default:"false" show_subquestions_if:"true"`
+ HostNetwork bool `yaml:"hostNetwork,omitempty" description:"Host Networking" default:"false"`
+ DNSConfig DNSConfig `yaml:"dnsConfig,omitempty" description:"DNS Configuration"`
+ } `yaml:"podOptions,omitempty" description:"Global Pod Options (Advanced)"`
+}
+
+// DNSConfig represents the DNS configuration.
+type DNSConfig struct {
+ Options []DNSConfigEntry `yaml:"options,omitempty" validate:"dive" description:"Options"`
+ Nameservers []string `yaml:"nameservers,omitempty" validate:"dive,required" description:"Nameservers"`
+ Searches []string `yaml:"searches,omitempty" validate:"dive,required" description:"Searches"`
+}
diff --git a/clustertool/pkg/charts/valuesYaml/resourceValues.go b/clustertool/pkg/charts/valuesYaml/resourceValues.go
new file mode 100644
index 0000000000000..88ab46385b7a0
--- /dev/null
+++ b/clustertool/pkg/charts/valuesYaml/resourceValues.go
@@ -0,0 +1,43 @@
+package valuesYaml
+
+// Resources represents the schema for resource settings.
+type Resources struct {
+ Limits ResourceLimits `yaml:"limits" schema:"additional_attrs:true,type:dict"`
+ Requests ResourceLimits `yaml:"requests" schema:"additional_attrs:true,type:dict,hidden:true"`
+}
+
+// ResourceLimits represents the schema for resource limit settings.
+type ResourceLimits struct {
+ CPU string `yaml:"cpu" schema:"type:string,default:4000m,valid_chars:^(?!^0(\\.0|m|)$)([0-9]+)(\\.[0-9]|m?)$"`
+ Memory string `yaml:"memory" schema:"type:string,default:8Gi,valid_chars:^(?!^0(e[0-9]|[EPTGMK]i?|)$)([0-9]+)(|[EPTGMK]i?|e[0-9]+)$"`
+}
+
+// DeviceList represents the schema for the list of devices.
+type DeviceList struct {
+ DeviceListEntry []DeviceEntry `yaml:"deviceList" schema:"type:list,default:[]"`
+}
+
+// DeviceEntry represents the schema for a device entry.
+type DeviceEntry struct {
+ Enabled bool `yaml:"enabled" schema:"type:boolean,default:true"`
+ Type string `yaml:"type" schema:"type:string,default:device,hidden:true"`
+ ReadOnly bool `yaml:"readOnly" schema:"type:boolean,default:false"`
+ HostPath string `yaml:"hostPath" schema:"type:path"`
+ MountPath string `yaml:"mountPath" schema:"type:string,default:/dev/ttyACM0"`
+}
+
+// ScaleGPU represents the schema for GPU configuration.
+type ScaleGPU struct {
+ ScaleGPUEntry []GPUEntry `yaml:"scaleGPU" schema:"type:list,default:[]"`
+}
+
+// GPUEntry represents the schema for a GPU entry.
+type GPUEntry struct {
+ GPU GPUConfiguration `yaml:"gpu" schema:"additional_attrs:true,type:dict"`
+ Workaround string `yaml:"workaround" schema:"type:string,default:workaround,hidden:true"`
+}
+
+// GPUConfiguration represents the schema for GPU configuration.
+type GPUConfiguration struct {
+ // Specify GPU configuration here
+}
diff --git a/clustertool/pkg/charts/valuesYaml/securityContextValues.go b/clustertool/pkg/charts/valuesYaml/securityContextValues.go
new file mode 100644
index 0000000000000..a84f567caaab5
--- /dev/null
+++ b/clustertool/pkg/charts/valuesYaml/securityContextValues.go
@@ -0,0 +1,25 @@
+package valuesYaml
+
+// Container represents the schema for container settings.
+type Container struct {
+ RunAsUser int `yaml:"runAsUser" schema:"type:int,default:568"`
+ RunAsGroup int `yaml:"runAsGroup" schema:"type:int,default:568"`
+ PUID int `yaml:"PUID" schema:"type:int,default:568,show_if:[[runAsUser,=,0]]"`
+ UMASK string `yaml:"UMASK" schema:"type:string,default:0022"`
+ Advanced bool `yaml:"advanced" schema:"type:boolean,default:false,show_subquestions_if:true"`
+ Privileged bool `yaml:"privileged" schema:"type:boolean,default:false,show_if:[[advanced,=,true]]"`
+ ReadOnlyRootFilesystem bool `yaml:"readOnlyRootFilesystem" schema:"type:boolean,default:true,show_if:[[advanced,=,true]]"`
+}
+
+// Pod represents the schema for pod settings.
+type Pod struct {
+ FsGroupChangePolicy string `yaml:"fsGroupChangePolicy" schema:"type:string,default:OnRootMismatch,enum:OnRootMismatch,Always"`
+ SupplementalGroups []int `yaml:"supplementalGroups" schema:"type:list,default:[],items:type:int"`
+ FsGroup int `yaml:"fsGroup" schema:"type:int,default:568"`
+}
+
+// SecurityContext represents the schema for security context settings.
+type SecurityContext struct {
+ Container Container `yaml:"container" schema:"additional_attrs:true,type:dict"`
+ Pod Pod `yaml:"pod" schema:"additional_attrs:true,type:dict"`
+}
diff --git a/clustertool/pkg/charts/valuesYaml/serviceValues.go b/clustertool/pkg/charts/valuesYaml/serviceValues.go
new file mode 100644
index 0000000000000..f3ed0ecf7727e
--- /dev/null
+++ b/clustertool/pkg/charts/valuesYaml/serviceValues.go
@@ -0,0 +1,28 @@
+package valuesYaml
+
+// PortConfiguration represents the configuration for a service port.
+type PortConfiguration struct {
+ Enabled bool `yaml:"enabled" schema:"type:boolean" default:"true" hidden:"true" description:"Enable the Port"`
+ Name string `yaml:"name" schema:"type:string" default:"" description:"Port Name"`
+ Protocol string `yaml:"protocol" schema:"type:string" default:"tcp" enum:"[http, https, tcp, udp]" description:"Port Type"`
+ TargetPort int `yaml:"targetPort" schema:"type:int" required:"true" description:"Target Port"`
+ Port int `yaml:"port" schema:"type:int" required:"true" description:"Container Port"`
+}
+
+// AdvancedServiceSettings represents the advanced settings for a service.
+type AdvancedServiceSettings struct {
+ ExternalIPs []string `yaml:"externalIPs" schema:"type:list" default:"[]" items:"type:string" description:"External IP's"`
+ IPFamilyPolicy string `yaml:"ipFamilyPolicy" schema:"type:string" default:"SingleStack" enum:"[SingleStack, PreferDualStack, RequireDualStack]" description:"IP Family Policy"`
+ IPFamilies []string `yaml:"ipFamilies" schema:"type:list" default:"[]" items:"type:string" description:"(Advanced) The IP Families that should be used"`
+}
+
+// ServiceConfiguration represents the configuration for a service.
+type ServiceConfiguration struct {
+ Enabled bool `yaml:"enabled" schema:"type:boolean" default:"true" hidden:"true" description:"Enable the service"`
+ Name string `yaml:"name" schema:"type:string" default:"" description:"Name"`
+ Type string `yaml:"type" schema:"type:string" default:"LoadBalancer" enum:"[LoadBalancer, ClusterIP, Simple]" description:"Service Type"`
+ LoadBalancerIP string `yaml:"loadBalancerIP" schema:"type:string" show_if:"[['type', '=', 'LoadBalancer']]" default:"" description:"LoadBalancer IP"`
+ AdvancedSvcSet AdvancedServiceSettings `yaml:"advancedsvcset" schema:"type:boolean" default:"false" show_subquestions_if:"true" description:"Show Advanced Service Settings"`
+ Ports PortConfiguration `yaml:"ports" schema:"type:dict" description:"Service's Port(s) Configuration"`
+ PortsList []PortConfiguration `yaml:"portsList" schema:"type:list" default:"[]" items:"type:dict" description:"Additional Service Ports"`
+}
diff --git a/clustertool/pkg/charts/valuesYaml/updater.go b/clustertool/pkg/charts/valuesYaml/updater.go
new file mode 100644
index 0000000000000..ed3fe3d1ca2ab
--- /dev/null
+++ b/clustertool/pkg/charts/valuesYaml/updater.go
@@ -0,0 +1,44 @@
+package valuesYaml
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/rs/zerolog/log"
+)
+
+// UpdatevaluesFile updates the specified values.yaml file with an optional bump parameter.
+func UpdatevaluesFile(valuesPathOrFolder, bump string) error {
+ var chartFolder string
+ var valuesPath string
+
+ fileInfo, err := os.Stat(valuesPathOrFolder)
+ if err != nil {
+ return err
+ }
+
+ if fileInfo.IsDir() {
+ chartFolder = valuesPathOrFolder
+ } else {
+ chartFolder = filepath.Dir(valuesPathOrFolder)
+
+ }
+ valuesPath = filepath.Join(chartFolder, "values.yaml")
+
+ log.Printf("Processing: %s\n", valuesPath)
+ values := NewValuesFile()
+ if err := values.LoadFromFile(valuesPath); err != nil {
+ log.Info().Msgf("Error loading values: %v\n", err)
+ return err
+ }
+
+ // Save the modified metadata back to the file
+ if err := values.SaveToFile(valuesPath); err != nil {
+ return fmt.Errorf("error saving values.yaml: %s", err)
+ }
+
+ log.Printf("values file updated and saved to %s\n", valuesPath)
+
+ return nil
+}
diff --git a/clustertool/pkg/charts/valuesYaml/workloadValues.go b/clustertool/pkg/charts/valuesYaml/workloadValues.go
new file mode 100644
index 0000000000000..b0552ffc37f1b
--- /dev/null
+++ b/clustertool/pkg/charts/valuesYaml/workloadValues.go
@@ -0,0 +1,53 @@
+package valuesYaml
+
+// WorkloadRootReference represents the schema for the root reference in workload settings.
+type WorkloadRootReference struct {
+ Type string `yaml:"type,omitempty" schema:"type:string,default:Deployment,enum:,Deployment,DaemonSet"`
+ Replicas int `yaml:"replicas,omitempty" schema:"type:int,show_if:[[type,!=,DaemonSet]],default:1"`
+ PodSpec WorkloadPodSpec `yaml:"podSpec,omitempty" schema:"additional_attrs:true,type:dict"`
+ UpdateStrategy string `yaml:"updateStrategy,omitempty"`
+}
+
+// WorkloadPodSpec represents the schema for the pod spec in workload settings.
+type WorkloadPodSpec struct {
+ Containers WorkloadContainers `yaml:"containers,omitempty" schema:"additional_attrs:true,type:dict"`
+}
+
+// WorkloadContainers represents the schema for containers in workload settings.
+type WorkloadContainers struct {
+ ContainerItem WorkloadContainerItem `yaml:"containerItem,omitempty" schema:"additional_attrs:true,type:dict"`
+}
+
+// WorkloadContainerItem represents the schema for a container item in workload settings.
+type WorkloadContainerItem struct {
+ Env map[string]string `yaml:"env,omitempty" schema:"additional_attrs:true,type:dict"`
+ EnvList []EnvList `yaml:"envList,omitempty" schema:"type:list,default:[],items:type:dict"`
+ ExtraArgs []string `yaml:"extraArgs,omitempty" schema:"type:list,default:[]"`
+ Advanced WorkloadContainerAdvanced `yaml:"advanced,omitempty" schema:"type:boolean,default:false,show_subquestions_if:true"`
+ Probes WorkloadProbes `yaml:"probes,omitempty"`
+ UpdateStrategy string `yaml:"updateStrategy,omitempty"`
+}
+
+// EnvList represents the schema for an environment variable list item in workload settings.
+type EnvList struct {
+ Name string `yaml:"name,omitempty" schema:"type:string"`
+ Value string `yaml:"value,omitempty" schema:"type:string"`
+}
+
+// WorkloadContainerAdvanced represents the schema for advanced settings in the workload container settings.
+type WorkloadContainerAdvanced struct {
+ Command []string `yaml:"command,omitempty" schema:"type:list,default:[],items:type:string"`
+ ExtraSettings map[string]string `yaml:"extraSettings,omitempty" schema:"type:dict"`
+}
+
+// WorkloadProbes represents the schema for probes in workload container settings.
+type WorkloadProbes struct {
+ Liveness Probe `yaml:"liveness,omitempty"`
+ Readiness Probe `yaml:"readiness,omitempty"`
+ Startup Probe `yaml:"startup,omitempty"`
+}
+
+// Probe represents the schema for a probe in workload container settings.
+type Probe struct {
+ Path string `yaml:"path,omitempty"`
+}
diff --git a/clustertool/pkg/charts/version/bump.go b/clustertool/pkg/charts/version/bump.go
new file mode 100644
index 0000000000000..00923c0580b80
--- /dev/null
+++ b/clustertool/pkg/charts/version/bump.go
@@ -0,0 +1,51 @@
+package version
+
+import (
+ "fmt"
+ "regexp"
+
+ "github.com/Masterminds/semver/v3"
+ "github.com/rs/zerolog/log"
+)
+
+const (
+ Major = "major"
+ Minor = "minor"
+ Patch = "patch"
+)
+
+func IncrementVersion(version, kind string) (string, error) {
+ // Validate SemVer format
+ semVerPattern := regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)$`)
+ if !semVerPattern.MatchString(version) {
+ return "", fmt.Errorf("invalid SemVer format (Major.Minor.Patch): %s", version)
+ }
+
+ v, err := semver.NewVersion(version)
+ if err != nil {
+ return "", err
+ }
+
+ // Increment the specified version component
+ switch kind {
+ case Major:
+ return v.IncMajor().String(), nil
+ case Minor:
+ return v.IncMinor().String(), nil
+ case Patch:
+ return v.IncPatch().String(), nil
+ default:
+ return "", fmt.Errorf("invalid bump kind: %s", kind)
+ }
+}
+
+func Bump(semVer, kind string) error {
+
+ newVersion, err := IncrementVersion(semVer, kind)
+ if err != nil {
+ log.Fatal().Err(err).Msg("Failed to increment version")
+ }
+
+ log.Info().Msgf("🆚 Updated SemVer from [%s] to [%s]", semVer, newVersion)
+ return nil
+}
diff --git a/clustertool/pkg/charts/version/bump_test.go b/clustertool/pkg/charts/version/bump_test.go
new file mode 100644
index 0000000000000..24c3eb819f7b4
--- /dev/null
+++ b/clustertool/pkg/charts/version/bump_test.go
@@ -0,0 +1,78 @@
+package version
+
+import "testing"
+
+func TestIncrementVersion(t *testing.T) {
+ type args struct {
+ version string
+ kind string
+ }
+
+ tests := []struct {
+ name string
+ args args
+ want string
+ wantErr bool
+ }{
+ {
+ name: "Test increment major",
+ args: args{
+ version: "1.2.3",
+ kind: Major,
+ },
+ want: "2.0.0",
+ wantErr: false,
+ },
+ {
+ name: "Test increment minor",
+ args: args{
+ version: "1.2.3",
+ kind: Minor,
+ },
+ want: "1.3.0",
+ wantErr: false,
+ },
+ {
+ name: "Test increment patch",
+ args: args{
+ version: "1.2.3",
+ kind: Patch,
+ },
+ want: "1.2.4",
+ wantErr: false,
+ },
+ {
+ name: "Test increment invalid",
+ args: args{
+ version: "1.2.3",
+ kind: "invalid",
+ },
+ want: "",
+ wantErr: true,
+ },
+ {
+ name: "Test increment incomplete",
+ args: args{
+ version: "1.2",
+ kind: "invalid",
+ },
+ want: "",
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+
+ got, err := IncrementVersion(tt.args.version, tt.args.kind)
+ // If we expected an error, but didn't get one, fail the test
+ if (err != nil) != tt.wantErr {
+ t.Errorf("IncrementVersion() error = %v, wantErr %t", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("IncrementVersion() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/clustertool/pkg/fluxhandler/bootstrap.go b/clustertool/pkg/fluxhandler/bootstrap.go
new file mode 100644
index 0000000000000..5708604f453ce
--- /dev/null
+++ b/clustertool/pkg/fluxhandler/bootstrap.go
@@ -0,0 +1,132 @@
+package fluxhandler
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+ "github.com/truecharts/private/clustertool/pkg/kubectlcmds"
+)
+
+func init() {
+ // Configure zerolog to output to stdout with a timestamp and log level
+ log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Logger()
+}
+
+// FluxBootstrap initializes the FluxCD bootstrapping process if GITHUB_REPOSITORY is set in TalEnv.
+func FluxBootstrap(ctx context.Context) {
+ if helper.TalEnv["GITHUB_REPOSITORY"] != "" {
+ log.Info().Msg("GITHUB_Repository for Flux configured.")
+ if helper.GetYesOrNo("Do you want to bootstrap FluxCD as well? (yes/no) [y/n]: ") {
+ if err := bootstrapFluxCD(ctx); err != nil {
+ log.Fatal().Err(err).Msg("Error during FluxCD bootstrap")
+ if helper.GetYesOrNo("Do you want to retry? (yes/no) [y/n]: ") {
+ if err2 := bootstrapFluxCD(ctx); err2 != nil {
+ log.Fatal().Err(err2).Msg("Error during FluxCD bootstrap")
+ }
+ }
+ }
+ log.Info().Msg("FluxCD Bootstrapped successfully")
+ }
+ }
+}
+
+// bootstrapFluxCD handles the entire FluxCD bootstrapping process.
+func bootstrapFluxCD(ctx context.Context) error {
+ if err := checkGitRepo(); err != nil {
+ return err
+ }
+
+ fluxPath := filepath.Join(helper.ClusterPath, "kubernetes", "flux-system", "flux")
+ if err := setupFluxCD(ctx, fluxPath); err != nil {
+ return err
+ }
+
+ reposFilePath := "repositories"
+ if err := setupRepositories(ctx, reposFilePath); err != nil {
+ return err
+ }
+
+ clusterEntryFile := filepath.Join(helper.ClusterPath, "kubernetes", "flux-entry.yaml")
+ if err := kubectlcmds.KubectlApply(ctx, clusterEntryFile); err != nil {
+ log.Error().Err(err).Str("path", clusterEntryFile).Msg("Error applying Kubernetes flux-entry manifest")
+ return err
+ }
+
+ return nil
+}
+
+// checkGitRepo verifies if the current directory is a valid Git repository.
+func checkGitRepo() error {
+ isRepo, err := helper.IsCurrentDirGitRepo()
+ if err != nil {
+ log.Error().Err(err).Msg("Error checking Git repository")
+ return err
+ }
+ if !isRepo {
+ errMsg := "Bootstrap: ERROR The current directory is not a Git repository. Cannot bootstrap fluxcd"
+ log.Error().Msg(errMsg)
+ return err
+ }
+ log.Info().Msg("Bootstrap: The current directory is a valid GIT repository, continuing...")
+ return nil
+}
+
+// setupFluxCD handles the setup of FluxCD manifests.
+func setupFluxCD(ctx context.Context, fluxPath string) error {
+ bootstrapFile := "bootstrap.yaml.ct"
+ kustomFile := "kustomization.yaml"
+ tmpFile := "placeholder"
+
+ log.Info().Msg("Bootstrap: Loading fluxcd onto the cluster...")
+
+ // Rename files for kustomize application
+ if err := os.Rename(filepath.Join(fluxPath, kustomFile), filepath.Join(fluxPath, tmpFile)); err != nil {
+ log.Error().Err(err).Msg("Error renaming kustomization file")
+ return err
+ }
+ if err := os.Rename(filepath.Join(fluxPath, bootstrapFile), filepath.Join(fluxPath, kustomFile)); err != nil {
+ log.Error().Err(err).Msg("Error renaming bootstrap file")
+ return err
+ }
+
+ if err := kubectlcmds.KubectlApplyKustomize(ctx, fluxPath); err != nil {
+ log.Error().Err(err).Str("path", fluxPath).Msg("Error applying FluxCD manifest")
+ return err
+ }
+
+ // Revert file renames
+ if err := os.Rename(filepath.Join(fluxPath, kustomFile), filepath.Join(fluxPath, bootstrapFile)); err != nil {
+ log.Error().Err(err).Msg("Error renaming kustomization file back")
+ return err
+ }
+ if err := os.Rename(filepath.Join(fluxPath, tmpFile), filepath.Join(fluxPath, kustomFile)); err != nil {
+ log.Error().Err(err).Msg("Error renaming placeholder file back")
+ return err
+ }
+
+ return nil
+}
+
+// setupRepositories handles the setup of repository manifests.
+func setupRepositories(ctx context.Context, reposFilePath string) error {
+ log.Info().Msg("Bootstrap: Loading git-repo manifests onto the cluster...")
+
+ gitRepoFile := filepath.Join(reposFilePath, "git", "this-repo.yaml")
+ if err := kubectlcmds.KubectlApply(ctx, gitRepoFile); err != nil {
+ log.Error().Err(err).Str("path", reposFilePath).Msg("Error applying repositories manifest")
+ return err
+ }
+
+ log.Info().Msg("Bootstrap: Loading repositories flux-entry onto the cluster...")
+ reposEntryFile := filepath.Join(reposFilePath, "flux-entry.yaml")
+ if err := kubectlcmds.KubectlApply(ctx, reposEntryFile); err != nil {
+ log.Error().Err(err).Str("path", reposEntryFile).Msg("Error applying repositories flux-entry manifest")
+ return err
+ }
+
+ return nil
+}
diff --git a/clustertool/pkg/fluxhandler/helm.go b/clustertool/pkg/fluxhandler/helm.go
new file mode 100644
index 0000000000000..2ca512a668c3d
--- /dev/null
+++ b/clustertool/pkg/fluxhandler/helm.go
@@ -0,0 +1,565 @@
+package fluxhandler
+
+import (
+ "context"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/rs/zerolog/log"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+ "helm.sh/helm/v3/pkg/action"
+ "helm.sh/helm/v3/pkg/chart/loader"
+ "helm.sh/helm/v3/pkg/cli"
+ "helm.sh/helm/v3/pkg/cli/values"
+ "helm.sh/helm/v3/pkg/getter"
+ "helm.sh/helm/v3/pkg/registry"
+ "helm.sh/helm/v3/pkg/release"
+ "helm.sh/helm/v3/pkg/repo"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "sigs.k8s.io/kustomize/kyaml/yaml"
+)
+
+func newDefaultRegistryClient(plainHTTP bool, settings *cli.EnvSettings) (*registry.Client, error) {
+ opts := []registry.ClientOption{
+ registry.ClientOptDebug(settings.Debug),
+ registry.ClientOptEnableCache(true),
+ registry.ClientOptWriter(os.Stdout),
+ registry.ClientOptCredentialsFile(settings.RegistryConfig),
+ }
+ if plainHTTP {
+ opts = append(opts, registry.ClientOptPlainHTTP())
+ }
+
+ // Create a new registry client
+ registryClient, err := registry.NewClient(opts...)
+ if err != nil {
+ return nil, err
+ }
+ return registryClient, nil
+}
+
+// HelmPull downloads a Helm chart from a repository
+func HelmPull(repo string, name string, version string, dest string, silent bool) error {
+ settings := cli.New()
+ actionConfig := new(action.Configuration)
+
+ // Define logger based on the silent parameter
+ var logger func(string, ...interface{})
+ if silent {
+ logger = noOpLog
+ } else {
+ logger = log.Printf
+ }
+
+ // Initialize actionConfig with the appropriate logger
+ if err := actionConfig.Init(settings.RESTClientGetter(), "", os.Getenv("HELM_DRIVER"), logger); err != nil {
+ return fmt.Errorf("failed to initialize Helm action config: %w", err)
+ }
+
+ registryClient, err := newDefaultRegistryClient(false, settings)
+ if err != nil {
+ return err
+ }
+ actionConfig.RegistryClient = registryClient
+
+ client := action.NewPullWithOpts(action.WithConfig(actionConfig))
+ client.Settings = settings
+ client.RepoURL = repo
+ client.Version = version
+ client.DestDir = filepath.Join(helper.HelmCache, dest)
+
+ // Create cache directory
+ if err := os.MkdirAll(client.DestDir, os.ModePerm); err != nil {
+ return fmt.Errorf("❌ Failed to create cache directory: %s", err)
+ }
+
+ switch repo {
+ case "https://charts.truecharts.org",
+ "https://library-charts.truecharts.org",
+ "https://deps.truecharts.org":
+ client.Keyring = helper.GpgDir + "/pubring.gpg"
+ client.Verify = true
+ case "https://charts.jetstack.io":
+ client.Keyring = helper.GpgDir + "/certman.gpg"
+ client.Verify = true
+ default:
+ // Do nothing for other repositories
+ }
+
+ link := ""
+ if strings.HasPrefix(repo, "http") {
+ link = name
+ repoName := cleanRepoURL(repo)
+ updateHelmRepo(repoName, repo, silent)
+ repo = repoName
+ } else {
+ link = repo + "/" + name
+ client.RepoURL = ""
+ }
+
+ output, err := client.Run(link)
+
+ if err != nil {
+ os.Remove(path.Join(dest, fmt.Sprintf("%s-%s.tgz", name, version)))
+ return err
+ }
+ if !silent {
+ log.Info().Msg("✅ Dependency Downloaded!")
+ }
+ if client.Keyring != "" && client.Keyring != "nil" {
+ if !silent {
+ log.Info().Msg("✅ Dependency Verified")
+ }
+ }
+
+ if output != "" {
+ log.Info().Msgf("☸ Helm output: %s", output)
+ }
+ return nil
+}
+
+func noOpLog(format string, v ...interface{}) {}
+
+// HelmInstall installs a Helm chart with provided parameters
+func HelmInstall(repoURL string, chartName string, releaseName string, namespace string, valuesFile string, version string, dryRun bool, wait bool, silent bool) error {
+ if dryRun {
+ log.Info().Msg("dryRun not possible...")
+ return nil
+ }
+
+ settings := cli.New()
+ actionConfig := new(action.Configuration)
+
+ settings.SetNamespace(namespace)
+
+ var logger func(string, ...interface{})
+ if silent {
+ logger = noOpLog
+
+ } else {
+ logger = log.Printf
+ }
+
+ if err := actionConfig.Init(settings.RESTClientGetter(), namespace,
+ os.Getenv("HELM_DRIVER"), logger); err != nil {
+ return fmt.Errorf("failed to initialize Helm action config: %w", err)
+ }
+
+ // Ensure namespace exists or create it
+ if err := ensureNamespace(actionConfig, namespace); err != nil {
+ return fmt.Errorf("failed to ensure namespace exists: %w", err)
+ }
+
+ var chartPath string
+ var err error
+
+ // Determine chart path based on chartName format
+ if strings.HasPrefix(repoURL, "http://") || strings.HasPrefix(repoURL, "https://") || strings.HasPrefix(repoURL, "oci://") {
+ // Handle HTTP or OCI URL
+ err = HelmPull(repoURL, chartName, version, "", true)
+ if err != nil {
+ return fmt.Errorf("failed to pull chart %s: %w", chartName, err)
+ }
+ chartPath = path.Join(helper.HelmCache, fmt.Sprintf("%s-%s.tgz", chartName, version))
+
+ } else {
+ // Local chart path
+ chartPath = repoURL
+ }
+
+ // Load the Helm chart using loader.Load
+ chart, err := loader.Load(chartPath)
+ if err != nil {
+ return fmt.Errorf("failed to load chart: %w", err)
+ }
+
+ // Set up Helm install action
+ client := action.NewInstall(actionConfig)
+ client.Namespace = namespace
+ client.ReleaseName = releaseName
+ client.DryRun = dryRun
+ client.Version = version
+
+ tempValuesName := releaseName + "tempvalues.yaml"
+ tempValuesPath := path.Join(helper.HelmCache, tempValuesName)
+ // Create values.yaml from chart.Values
+ err = createValuesYAML(chart.Values, tempValuesPath)
+ if err != nil {
+ return fmt.Errorf("error creating tempvalues.yaml: %w", err)
+ }
+ valueFiles := []string{tempValuesPath}
+
+ // Get the directory part of the path
+ directory := filepath.Dir(valuesFile)
+
+ helmreleasePath := path.Join(directory, "helm-release.yaml")
+
+ helmRelease, err := LoadHelmRelease(helmreleasePath)
+ if err != nil {
+
+ }
+ tempHRValuesName := releaseName + "temphrvalues.yaml"
+ tempHRValuesPath := path.Join(helper.HelmCache, tempHRValuesName)
+ err = createValuesYAML(helmRelease.Spec.Values, tempHRValuesPath)
+ if err != nil {
+ return fmt.Errorf("error creating temphrvalues.yaml: %w", err)
+ }
+ helper.EnvSubst(tempHRValuesPath, helper.TalEnv)
+ valueFiles = append(valueFiles, tempHRValuesPath)
+
+ if _, err := os.Stat(valuesFile); err == nil {
+ valueFiles = append(valueFiles, valuesFile)
+
+ }
+
+ overrideValuesPath := path.Join(directory, "bootstrap-values.yaml.ct")
+
+ if _, err := os.Stat(overrideValuesPath); err == nil {
+ valueFiles = append(valueFiles, overrideValuesPath)
+ }
+
+ // Prepare values for installation
+ valOpts := &values.Options{
+ ValueFiles: valueFiles, // Specify value file to merge
+ }
+
+ // Create getter to fetch values from file
+ valProviders := getter.All(settings)
+
+ // Merge values with chart's default values
+ vals, err := valOpts.MergeValues(valProviders)
+ if err != nil {
+ return fmt.Errorf("failed to merge values: %w", err)
+ }
+
+ // Install the chart with merged values
+ release, err := client.Run(chart, vals)
+ if err != nil {
+ return fmt.Errorf("failed to install chart: %w", err)
+ }
+
+ if wait {
+ waitForRelease(actionConfig, release.Name, client.Namespace)
+ }
+
+ log.Printf("Installed Chart: %s in namespace: %s\n", release.Name, release.Namespace)
+ log.Printf("Installed Chart values: %v\n", release.Config)
+
+ return nil
+}
+
+func ensureNamespace(actionConfig *action.Configuration, namespace string) error {
+ // Check if the namespace exists
+ exists, err := namespaceExists(actionConfig, namespace)
+ if err != nil {
+ return fmt.Errorf("failed to check if namespace exists: %w", err)
+ }
+
+ if !exists {
+ // Create the namespace if it does not exist
+ err := createNamespace(actionConfig, namespace)
+ if err != nil {
+ return fmt.Errorf("failed to create namespace: %w", err)
+ }
+ }
+
+ return nil
+}
+
+// HelmUpgrade upgrades a Helm release with provided parameters
+// HelmUpgrade upgrades a Helm release with provided parameters
+func HelmUpgrade(repoURL string, chartName string, releaseName string, namespace string, valuesFile string, version string, wait bool, silent bool) error {
+ settings := cli.New()
+ actionConfig := new(action.Configuration)
+
+ settings.SetNamespace(namespace)
+
+ var logger func(string, ...interface{})
+ if silent {
+ logger = noOpLog
+ } else {
+ logger = log.Printf
+ }
+
+ if err := actionConfig.Init(settings.RESTClientGetter(), namespace,
+ os.Getenv("HELM_DRIVER"), logger); err != nil {
+ return fmt.Errorf("failed to initialize Helm action config: %w", err)
+ }
+
+ // Ensure namespace exists or create it
+ if err := ensureNamespace(actionConfig, namespace); err != nil {
+ return fmt.Errorf("failed to ensure namespace exists: %w", err)
+ }
+
+ var chartPath string
+ var err error
+
+ // Determine chart path based on chartName format
+ if strings.HasPrefix(repoURL, "http://") || strings.HasPrefix(repoURL, "https://") || strings.HasPrefix(repoURL, "oci://") {
+ // Handle HTTP or OCI URL
+ err = HelmPull(repoURL, chartName, version, "", true)
+ if err != nil {
+ return fmt.Errorf("failed to pull chart %s: %w", chartName, err)
+ }
+ chartPath = path.Join(helper.HelmCache, fmt.Sprintf("%s-%s.tgz", chartName, version))
+
+ } else {
+ // Local chart path
+ chartPath = repoURL
+ }
+
+ // Load the Helm chart using loader.Load
+ chart, err := loader.Load(chartPath)
+ if err != nil {
+ return fmt.Errorf("failed to load chart: %w", err)
+ }
+
+ // Set up Helm upgrade action
+ client := action.NewUpgrade(actionConfig)
+ client.Namespace = namespace
+ client.Version = version
+
+ tempValuesName := releaseName + "tempvalues.yaml"
+ tempValuesPath := path.Join(helper.HelmCache, tempValuesName)
+ err = createValuesYAML(chart.Values, tempValuesPath)
+ if err != nil {
+ return fmt.Errorf("error creating tempvalues.yaml: %w", err)
+ }
+ valueFiles := []string{tempValuesPath}
+
+ // Get the directory part of the path
+ directory := filepath.Dir(valuesFile)
+
+ helmreleasePath := path.Join(directory, "helm-release.yaml")
+
+ helmRelease, err := LoadHelmRelease(helmreleasePath)
+ if err != nil {
+ return fmt.Errorf("error loading helm-release.yaml: %w", err)
+ }
+
+ tempHRValuesName := releaseName + "temphrvalues.yaml"
+ tempHRValuesPath := path.Join(helper.HelmCache, tempHRValuesName)
+ err = createValuesYAML(helmRelease.Spec.Values, tempHRValuesPath)
+ if err != nil {
+ return fmt.Errorf("error creating temphrvalues.yaml: %w", err)
+ }
+ helper.EnvSubst(tempHRValuesPath, helper.TalEnv)
+ valueFiles = append(valueFiles, tempHRValuesPath)
+
+ if _, err := os.Stat(valuesFile); err == nil {
+ valueFiles = append(valueFiles, valuesFile)
+ }
+
+ overrideValuesPath := path.Join(directory, "bootstrap-values.yaml.ct")
+
+ if _, err := os.Stat(overrideValuesPath); err == nil {
+ valueFiles = append(valueFiles, overrideValuesPath)
+ }
+
+ // Prepare values for upgrade
+ valOpts := &values.Options{
+ ValueFiles: valueFiles, // Specify value file to merge
+ }
+
+ // Create getter to fetch values from file
+ valProviders := getter.All(settings)
+
+ // Merge values with chart's default values
+ vals, err := valOpts.MergeValues(valProviders)
+ if err != nil {
+ return fmt.Errorf("failed to merge values: %w", err)
+ }
+
+ // Perform the upgrade with merged values
+ release, err := client.Run(releaseName, chart, vals)
+ if err != nil {
+ return fmt.Errorf("failed to upgrade chart: %w", err)
+ }
+
+ if wait {
+ waitForRelease(actionConfig, release.Name, client.Namespace)
+ }
+
+ log.Printf("Upgraded Chart: %s in namespace: %s\n", release.Name, release.Namespace)
+ log.Printf("Upgraded Chart values: %v\n", release.Config)
+
+ return nil
+}
+
+func namespaceExists(actionConfig *action.Configuration, namespace string) (bool, error) {
+ // Retrieve Kubernetes client set from actionConfig
+ clientset, err := actionConfig.KubernetesClientSet()
+ if err != nil {
+ return false, fmt.Errorf("failed to get Kubernetes client set: %w", err)
+ }
+
+ // Use clientset to check if the namespace exists
+ _, err = clientset.CoreV1().Namespaces().Get(context.Background(), namespace, metav1.GetOptions{})
+ if err != nil {
+ return false, nil // Namespace doesn't exist or other error occurred
+ }
+ return true, nil // Namespace exists
+}
+
+func createNamespace(actionConfig *action.Configuration, namespace string) error {
+ // Retrieve Kubernetes client set from actionConfig
+ clientset, err := actionConfig.KubernetesClientSet()
+ if err != nil {
+ return fmt.Errorf("failed to get Kubernetes client set: %w", err)
+ }
+
+ // Create the namespace using clientset
+ _, err = clientset.CoreV1().Namespaces().Create(context.Background(), &corev1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: namespace,
+ },
+ }, metav1.CreateOptions{})
+ if err != nil {
+ if strings.Contains(err.Error(), "already exists") {
+
+ } else {
+ return fmt.Errorf("failed to create namespace: %w", err)
+ }
+ }
+ return nil
+}
+
+func createValuesYAML(values map[string]interface{}, fileName string) error {
+ removeFileIfExists(fileName)
+ // Marshal values map into YAML format
+ data, err := yaml.Marshal(values)
+ if err != nil {
+ return err
+ }
+
+ // Write YAML data into the file
+ err = ioutil.WriteFile(fileName, data, 0644)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func removeFileIfExists(fileName string) error {
+ // Check if the file exists
+ _, err := os.Stat(fileName)
+ if err == nil {
+ // Delete the file
+ err = os.Remove(fileName)
+ if err != nil {
+ return err
+ }
+ } else if !os.IsNotExist(err) {
+ // Return other errors if the file check failed for a reason other than not existing
+ return err
+ }
+
+ return nil
+}
+
+func updateHelmRepo(name string, url string, silent bool) error {
+ // Create a Helm repository configuration
+ repoConfig := &repo.Entry{
+ Name: name,
+ URL: url,
+ }
+
+ // Initialize Helm settings
+ settings := cli.New()
+
+ // Create a repository object
+ r, err := repo.NewChartRepository(repoConfig, getter.All(settings))
+ if err != nil {
+ return fmt.Errorf("failed to create chart repository: %w", err)
+ }
+
+ // Ensure the repository cache directory exists
+ cacheDir := settings.RepositoryCache
+ if err := os.MkdirAll(cacheDir, os.ModePerm); err != nil {
+ return fmt.Errorf("failed to create cache directory: %w", err)
+ }
+
+ // Download the latest index file
+ if _, err := r.DownloadIndexFile(); err != nil {
+ return fmt.Errorf("failed to download index file: %w", err)
+ }
+
+ // Load existing repositories file or create a new one
+ repoFile := settings.RepositoryConfig
+ repoFileContent := repo.NewFile()
+ if _, err := os.Stat(repoFile); err == nil {
+ repoFileContent, err = repo.LoadFile(repoFile)
+ if err != nil {
+ return fmt.Errorf("failed to load repositories file: %w", err)
+ }
+ }
+
+ // Update the repositories file with the new repository
+ if !repoFileContent.Has(name) {
+ repoFileContent.Update(repoConfig)
+ }
+
+ if err := repoFileContent.WriteFile(repoFile, 0644); err != nil {
+ return fmt.Errorf("failed to write repositories file: %w", err)
+ }
+
+ if !silent {
+ log.Info().Msgf("Successfully updated repository '%s' from %s\n", name, url)
+ }
+ return nil
+}
+
+// cleanRepoURL performs the specified operations on the input URL
+func cleanRepoURL(url string) string {
+ // Remove http:// or https:// prefix
+ url = strings.TrimPrefix(url, "http://")
+ url = strings.TrimPrefix(url, "https://")
+
+ // Remove charts. prefix if present
+ url = strings.TrimPrefix(url, "charts.")
+
+ // Remove helm. prefix if present
+ url = strings.TrimPrefix(url, "helm.")
+
+ // Remove everything after the last dot
+ lastDotIndex := strings.LastIndex(url, ".")
+ if lastDotIndex != -1 {
+ url = url[:lastDotIndex]
+ }
+
+ url = repoURL(url)
+
+ return url
+}
+
+func repoURL(url string) string {
+ parts := strings.SplitN(url, "/", 2) // Split into two parts at the first "/"
+ if len(parts) > 0 {
+ url = parts[0]
+ }
+
+ return url
+}
+
+func waitForRelease(actionConfig *action.Configuration, releaseName, namespace string) {
+ statusClient := action.NewStatus(actionConfig)
+ for {
+ rel, err := statusClient.Run(releaseName)
+ if err != nil {
+ log.Info().Msgf("failed to get release status: %v", err)
+ }
+ if rel.Info.Status == release.StatusDeployed {
+ log.Info().Msgf("Release %s is now deployed\n", releaseName)
+ break
+ }
+ log.Info().Msgf("Waiting for release %s to be deployed (current status: %s)\n", releaseName, rel.Info.Status)
+ time.Sleep(5 * time.Second)
+ }
+}
diff --git a/clustertool/pkg/fluxhandler/helmrelease.go b/clustertool/pkg/fluxhandler/helmrelease.go
new file mode 100644
index 0000000000000..2024fb8c92364
--- /dev/null
+++ b/clustertool/pkg/fluxhandler/helmrelease.go
@@ -0,0 +1,200 @@
+package fluxhandler
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "github.com/rs/zerolog/log"
+ "gopkg.in/yaml.v3"
+)
+
+type HelmChart struct {
+ ChartPath string
+ Retry bool
+ Wait bool
+}
+
+type SourceRef struct {
+ Kind string `yaml:"kind,omitempty"`
+ Name string `yaml:"name,omitempty"`
+ Namespace string `yaml:"namespace,omitempty"`
+}
+
+type ChartSpec struct {
+ Chart string `yaml:"chart,omitempty"`
+ Version string `yaml:"version,omitempty"`
+ SourceRef SourceRef `yaml:"sourceRef,omitempty"`
+}
+
+type Chart struct {
+ Spec ChartSpec `yaml:"spec,omitempty"`
+}
+
+type Spec struct {
+ Interval string `yaml:"interval,omitempty"`
+ Chart Chart `yaml:"chart,omitempty"`
+ ReleaseName string `yaml:"releaseName,omitempty"`
+ Values map[string]interface{} `yaml:"values,omitempty"`
+}
+
+type Metadata struct {
+ Name string `yaml:"name,omitempty"`
+ Namespace string `yaml:"namespace,omitempty"`
+}
+
+type HelmRelease struct {
+ Metadata Metadata `yaml:"metadata,omitempty"`
+ Spec Spec `yaml:"spec,omitempty"`
+}
+
+func LoadHelmRelease(filename string) (*HelmRelease, error) {
+ // Read YAML file
+ yamlFile, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read file: %w", err)
+ }
+
+ // Initialize HelmRelease struct
+ config := &HelmRelease{}
+
+ // Unmarshal YAML into struct
+ err = yaml.Unmarshal(yamlFile, config)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshal YAML: %w", err)
+ }
+
+ // Ensure Values is not nil
+ if config.Spec.Values == nil {
+ config.Spec.Values = make(map[string]interface{})
+ }
+ return config, nil
+}
+
+func InstallCharts(charts []HelmChart, HelmRepos map[string]*HelmRepo, async bool) {
+ var wg sync.WaitGroup
+ for _, chart := range charts {
+ wg.Add(1)
+ go func(chart HelmChart) {
+ defer wg.Done()
+ valuesFile := filepath.Join(chart.ChartPath, "values.yaml")
+ helmreleaseFile := filepath.Join(chart.ChartPath, "helm-release.yaml")
+ helmRelease, err := LoadHelmRelease(helmreleaseFile)
+ if err != nil {
+ log.Info().Msgf("ERROR LOADING helmRelease for: %v", chart)
+ os.Exit(1)
+ }
+ if helmRelease == nil {
+ log.Info().Msgf("ERROR Empty helmRelease for: %v", chart)
+ os.Exit(1)
+ }
+
+ releaseName := helmRelease.Metadata.Name
+ if helmRelease.Spec.ReleaseName != "" {
+ releaseName = helmRelease.Spec.ReleaseName
+ }
+
+ if HelmRepos[helmRelease.Spec.Chart.Spec.SourceRef.Name] == nil {
+ log.Info().Msgf("ERROR Empty helmRepo for: ", helmRelease.Spec.Chart.Spec.SourceRef.Name)
+ os.Exit(1)
+ }
+
+ if HelmRepos[helmRelease.Spec.Chart.Spec.SourceRef.Name].Spec.URL == "" {
+ log.Info().Msgf("ERROR Empty repoURL for: ", helmRelease.Spec.Chart.Spec.SourceRef.Name)
+ os.Exit(1)
+ }
+
+ log.Info().Msgf("Bootstrap: Installing %s\n", helmRelease.Metadata.Name)
+ // We need to split install from dependency downloading, so we can parallel downloading
+ if err := HelmInstall(HelmRepos[helmRelease.Spec.Chart.Spec.SourceRef.Name].Spec.URL, helmRelease.Spec.Chart.Spec.Chart, releaseName, helmRelease.Metadata.Namespace, valuesFile, helmRelease.Spec.Chart.Spec.Version, chart.Retry, chart.Wait, true); err != nil {
+ if strings.Contains(err.Error(), "webhook") {
+ } else {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ if !async {
+ os.Exit(1)
+ }
+ }
+ }
+ }(chart)
+ if !async {
+ wg.Wait()
+ }
+ }
+ if async {
+ wg.Wait()
+ }
+}
+
+// UpgradeCharts upgrades Helm releases with provided Helm charts and repositories
+func UpgradeCharts(charts []HelmChart, HelmRepos map[string]*HelmRepo, async bool) {
+ var wg sync.WaitGroup
+ for _, chart := range charts {
+ wg.Add(1)
+ go func(chart HelmChart) {
+ defer wg.Done()
+
+ // Determine paths
+ valuesFile := filepath.Join(chart.ChartPath, "values.yaml")
+ helmreleaseFile := filepath.Join(chart.ChartPath, "helm-release.yaml")
+
+ // Load Helm release
+ helmRelease, err := LoadHelmRelease(helmreleaseFile)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error loading Helm release for chart at %s: %v\n", chart.ChartPath, err)
+ if !async {
+ os.Exit(1)
+ }
+ return
+ }
+ if helmRelease == nil {
+ fmt.Fprintf(os.Stderr, "Empty Helm release for chart at %s\n", chart.ChartPath)
+ if !async {
+ os.Exit(1)
+ }
+ return
+ }
+
+ // Determine release name
+ releaseName := helmRelease.Metadata.Name
+ if helmRelease.Spec.ReleaseName != "" {
+ releaseName = helmRelease.Spec.ReleaseName
+ }
+
+ // Determine chart name
+ chartName := helmRelease.Spec.Chart.Spec.Chart
+
+ // Validate Helm repository
+ repoName := helmRelease.Spec.Chart.Spec.SourceRef.Name
+ repo, ok := HelmRepos[repoName]
+ if !ok || repo.Spec.URL == "" {
+ fmt.Fprintf(os.Stderr, "Empty or invalid Helm repository for %s\n", repoName)
+ if !async {
+ os.Exit(1)
+ }
+ return
+ }
+
+ // Perform Helm upgrade
+ log.Info().Msgf("Upgrading %s\n", helmRelease.Metadata.Name)
+ err = HelmUpgrade(repo.Spec.URL, chartName, releaseName, helmRelease.Metadata.Namespace, valuesFile, helmRelease.Spec.Chart.Spec.Version, chart.Wait, true)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error upgrading %s: %v\n", helmRelease.Metadata.Name, err)
+ if !async {
+ os.Exit(1)
+ }
+ return
+ }
+ }(chart)
+
+ if !async {
+ wg.Wait()
+ }
+ }
+
+ if async {
+ wg.Wait()
+ }
+}
diff --git a/clustertool/pkg/fluxhandler/helmrepo.go b/clustertool/pkg/fluxhandler/helmrepo.go
new file mode 100644
index 0000000000000..7b7ab8fb8fe3d
--- /dev/null
+++ b/clustertool/pkg/fluxhandler/helmrepo.go
@@ -0,0 +1,76 @@
+package fluxhandler
+
+import (
+ "io/ioutil"
+ "path/filepath"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+ "gopkg.in/yaml.v3"
+)
+
+type HelmRepoMetadata struct {
+ Name string `yaml:"name,omitempty"`
+ Namespace string `yaml:"namespace,omitempty"`
+}
+
+type HelmRepoSpec struct {
+ Interval string `yaml:"interval,omitempty"`
+ URL string `yaml:"url,omitempty"`
+}
+
+type HelmRepo struct {
+ Metadata HelmRepoMetadata `yaml:"metadata,omitempty"`
+ Spec HelmRepoSpec `yaml:"spec,omitempty"`
+}
+
+// LoadAllHelmRepos loads all .yaml files under a directory into a map of HelmRepo structs,
+// ignoring kustomize.yaml and logging errors without stopping the entire process.
+func LoadAllHelmRepos(dirPath string) (map[string]*HelmRepo, error) {
+ files, err := ioutil.ReadDir(dirPath)
+ if err != nil {
+ return nil, err
+ }
+
+ repos := make(map[string]*HelmRepo)
+
+ for _, file := range files {
+ if !file.IsDir() && strings.HasSuffix(file.Name(), ".yaml") {
+ // Ignore kustomize.yaml file
+ if file.Name() == "kustomize.yaml" {
+ continue
+ }
+ filename := filepath.Join(dirPath, file.Name())
+ repo, err := LoadHelmRepo(filename)
+ if err != nil {
+ // Log the error but continue processing other files
+ log.Info().Msgf("Error loading repo from file %s: %v\n", file.Name(), err)
+ continue
+ }
+ // Use metadata.name as the key in the map
+ repos[repo.Metadata.Name] = repo
+ }
+ }
+
+ return repos, nil
+}
+
+// LoadHelmRepo loads a single HelmRepo struct from a YAML file
+func LoadHelmRepo(filename string) (*HelmRepo, error) {
+ // Read YAML file
+ yamlFile, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return nil, err
+ }
+
+ // Initialize HelmRepo struct
+ repo := &HelmRepo{}
+
+ // Unmarshal YAML into struct
+ err = yaml.Unmarshal(yamlFile, repo)
+ if err != nil {
+ return nil, err
+ }
+
+ return repo, nil
+}
diff --git a/clustertool/pkg/fluxhandler/kustomizations.go b/clustertool/pkg/fluxhandler/kustomizations.go
new file mode 100644
index 0000000000000..5b749aa64d912
--- /dev/null
+++ b/clustertool/pkg/fluxhandler/kustomizations.go
@@ -0,0 +1,160 @@
+package fluxhandler
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// check if a file exists
+func fileExists(filename string) bool {
+ info, err := os.Stat(filename)
+ if os.IsNotExist(err) {
+ return false
+ }
+ return !info.IsDir()
+}
+
+// createKsYaml creates ks.yaml file with Flux Kustomization
+func createKsYaml(path, parentFolder string) error {
+ // Ensure the path uses forward slashes
+ linuxPath := filepath.ToSlash(path)
+
+ content := fmt.Sprintf(`apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+ name: %s
+ namespace: flux-system
+spec:
+ interval: 10m
+ path: %s/app
+ prune: true
+ sourceRef:
+ kind: GitRepository
+ name: cluster
+
+`, parentFolder, linuxPath)
+ return ioutil.WriteFile(filepath.Join(path, "ks.yaml"), []byte(content), 0644)
+}
+
+// createOrUpdateKustomizationYaml creates or updates kustomization.yaml file
+func createOrUpdateKustomizationYaml(path string) error {
+ kustomizationPath := filepath.Join(path, "kustomization.yaml")
+ var content string
+ if fileExists(kustomizationPath) {
+ // Read existing kustomization.yaml file
+ data, err := ioutil.ReadFile(kustomizationPath)
+ if err != nil {
+ return err
+ }
+ content = string(data)
+ } else {
+ content = `apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+`
+ }
+
+ // List all files and folders in the current directory
+ files, err := ioutil.ReadDir(path)
+ if err != nil {
+ return err
+ }
+
+ // Collect resources to add to kustomization.yaml
+ var resources []string
+ for _, file := range files {
+ name := file.Name()
+ // Ignore kustomization.yaml and ks.yaml files
+ if name == "kustomization.yaml" || name == "ks.yaml" {
+ continue
+ }
+ // Include only YAML files and directories
+ if strings.HasSuffix(name, ".yaml") || file.IsDir() {
+ if file.IsDir() && fileExists(filepath.Join(path, name, "ks.yaml")) {
+ // Update folder entry to include ks.yaml
+ name = fmt.Sprintf("%s/ks.yaml", name)
+ }
+ // Check if the file/folder is already listed
+ if !strings.Contains(content, name) {
+ if name == "namespace.yaml" {
+ resources = append([]string{name}, resources...)
+ } else {
+ resources = append(resources, name)
+ }
+ }
+ }
+ }
+
+ // Update kustomization.yaml content
+ for _, resource := range resources {
+ content += fmt.Sprintf(" - %s\n", resource)
+ }
+
+ contentLines := strings.Split(content, "\n")
+ for _, item := range contentLines {
+
+ if strings.HasSuffix(item, "/ks.yaml") {
+ prefix := strings.TrimSuffix(item, "/ks.yaml")
+
+ // Remove other resources with the same prefix from content
+ var updatedContent []string
+ for _, line := range contentLines {
+ if line != prefix {
+ updatedContent = append(updatedContent, line)
+ }
+ }
+ contentLines = updatedContent
+ }
+ }
+ content = strings.Join(contentLines, "\n")
+
+ // Write back the updated kustomization.yaml file
+ return ioutil.WriteFile(kustomizationPath, []byte(content), 0644)
+}
+
+// processDirectory processes each directory recursively
+func ProcessDirectory(path string) error {
+ hasAppFolder := false
+ hasKsYaml := false
+
+ // Check for "app" folder and "ks.yaml" file
+ files, err := ioutil.ReadDir(path)
+ if err != nil {
+ return err
+ }
+ for _, file := range files {
+ if file.IsDir() && file.Name() == "app" {
+ hasAppFolder = true
+ }
+ if file.Name() == "ks.yaml" {
+ hasKsYaml = true
+ }
+ }
+
+ // Create ks.yaml if "app" folder exists and ks.yaml does not exist
+ if hasAppFolder && !hasKsYaml {
+ parentFolder := filepath.Base(path)
+ if err := createKsYaml(path, parentFolder); err != nil {
+ return err
+ }
+ } else if !hasKsYaml { // Only create/update kustomization.yaml if ks.yaml does not exist
+ // Create or update kustomization.yaml
+ if err := createOrUpdateKustomizationYaml(path); err != nil {
+ return err
+ }
+ }
+
+ // Recurse into subdirectories
+ for _, file := range files {
+ if file.IsDir() {
+ if err := ProcessDirectory(filepath.Join(path, file.Name())); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/clustertool/pkg/fluxhandler/sshsecretgen.go b/clustertool/pkg/fluxhandler/sshsecretgen.go
new file mode 100644
index 0000000000000..e57f74287146a
--- /dev/null
+++ b/clustertool/pkg/fluxhandler/sshsecretgen.go
@@ -0,0 +1,238 @@
+package fluxhandler
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/x509"
+ "encoding/pem"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+
+ "github.com/truecharts/private/clustertool/pkg/helper"
+ "golang.org/x/crypto/ssh"
+ corev1 "k8s.io/api/core/v1"
+ "sigs.k8s.io/yaml"
+)
+
+// Define a struct to map the YAML content
+type Config struct {
+ StringData map[string]string `yaml:"stringData"`
+}
+
+// CreateGitSecret generates a Kubernetes secret YAML file and a public key text file.
+func CreateGitSecret(gitURL string) error {
+ if gitURL == "" {
+ gitURL = "github.com"
+ }
+
+ // Paths for files
+ secretPath := filepath.Join(helper.ClusterPath, "kubernetes", "flux-system", "flux", "deploykey.secret.yaml")
+ publicKeyPath := filepath.Join(".", "ssh-public-key.txt")
+
+ // Check if secret YAML already exists
+ if _, err := os.Stat(secretPath); os.IsNotExist(err) {
+ // Generate ECDSA private key
+ privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
+ if err != nil {
+ return fmt.Errorf("failed to generate ECDSA private key: %w", err)
+ }
+
+ // Encode private key to PEM format
+ privateKeyPEMBlock, err := pemBlockForKey(privateKey)
+ if err != nil {
+ return fmt.Errorf("failed to create PEM block for private key: %w", err)
+ }
+
+ // Generate OpenSSH formatted public key
+ publicKey, err := publicKeyToOpenSSH(&privateKey.PublicKey)
+ if err != nil {
+ return fmt.Errorf("failed to generate OpenSSH public key: %w", err)
+ }
+
+ // Write public key to file
+ err = os.WriteFile(publicKeyPath, []byte(publicKey), 0644)
+ if err != nil {
+ return fmt.Errorf("failed to write public key to file: %w", err)
+ }
+ log.Info().Msgf("Public key saved to: %s\n", publicKeyPath)
+
+ // Generate known_hosts entry
+ knownHosts := getKnownHostsEntry(gitURL)
+
+ // Generate Kubernetes secret YAML content
+ secret := map[string]interface{}{
+ "apiVersion": "v1",
+ "kind": "Secret",
+ "metadata": map[string]interface{}{
+ "name": "deploy-key",
+ "namespace": "flux-system",
+ },
+ "stringData": map[string]interface{}{
+ "identity": string(privateKeyPEMBlock),
+ "identity.pub": publicKey,
+ "known_hosts": knownHosts,
+ },
+ "type": string(corev1.SecretTypeOpaque),
+ }
+
+ secretYAML, err := yaml.Marshal(secret)
+ if err != nil {
+ return fmt.Errorf("failed to marshal secret to YAML: %w", err)
+ }
+
+ // Write Kubernetes secret YAML to file
+ err = os.MkdirAll(filepath.Dir(secretPath), 0755)
+ if err != nil {
+ return fmt.Errorf("failed to create directories: %w", err)
+ }
+ err = os.WriteFile(secretPath, secretYAML, 0644)
+ if err != nil {
+ return fmt.Errorf("failed to write secret YAML to file: %w", err)
+ }
+ log.Info().Msgf("Kubernetes secret YAML saved to: %s\n", secretPath)
+ } else {
+ // Secret YAML already exists, check if public key file exists
+ if _, err := os.Stat(publicKeyPath); os.IsNotExist(err) {
+ // Public key file does not exist, generate from existing secret
+ secretYAML, err := os.ReadFile(secretPath)
+ if err != nil {
+ return fmt.Errorf("failed to read existing secret YAML: %w", err)
+ }
+
+ var secret corev1.Secret
+ if err := yaml.Unmarshal(secretYAML, &secret); err != nil {
+ return fmt.Errorf("failed to unmarshal secret YAML: %w", err)
+ }
+
+ if ppk, ok := secret.StringData["identity.pub"]; ok {
+ err = os.WriteFile(publicKeyPath, []byte(ppk), 0644)
+ if err != nil {
+ return fmt.Errorf("failed to write public key to file: %w", err)
+ }
+ log.Info().Msgf("Public key saved to: %s\n", publicKeyPath)
+ } else {
+ return fmt.Errorf("identity.pub not found in existing secret YAML")
+ }
+ } else {
+ log.Info().Msgf("Public key file already exists: %s\n", publicKeyPath)
+ }
+ }
+
+ return nil
+}
+
+func CreateSshPatch() {
+ log.Info().Msg("generating talospatch for flux ssh key...")
+ // Paths to the YAML files
+ secretPath := filepath.Join(helper.ClusterPath, "kubernetes", "flux-system", "flux", "deploykey.secret.yaml")
+ sopsPatchPath := filepath.Join(helper.ClusterPath, "talos", "patches", "sopssecret.yaml")
+
+ // Read the YAML file
+ yamlFile, err := ioutil.ReadFile(secretPath)
+ if err != nil {
+ log.Fatal().Err(err).Msg("error: %v")
+ }
+
+ // Unmarshal the YAML content into a Config struct
+ var config Config
+ err = yaml.Unmarshal(yamlFile, &config)
+ if err != nil {
+ log.Fatal().Err(err).Msg("error: %v")
+ }
+
+ // Extract the stringData content and convert it to a multi-line string
+ stringData, err := yaml.Marshal(config.StringData)
+ if err != nil {
+ log.Fatal().Err(err).Msg("error: %v")
+ }
+
+ // Convert byte array to string
+ deployKeyData := string(stringData)
+
+ // Replace the placeholder in sopspath.yaml
+ err = ReplacePlaceholder(sopsPatchPath, "REPLACEWITHDEPLOYKEY", indentYaml(deployKeyData, " "))
+ if err != nil {
+ log.Fatal().Err(err).Msg("Failed to replace placeholder: %v")
+ }
+}
+
+// indentYaml indents each line of the YAML string with the specified indentation.
+func indentYaml(yamlStr, indent string) string {
+ lines := strings.Split(yamlStr, "\n")
+ for i, line := range lines {
+ if line != "" {
+ lines[i] = indent + line
+ }
+ }
+ return strings.Join(lines, "\n")
+}
+
+// ReplacePlaceholder replaces the placeholder in the file at the given path with the specified replacement string.
+func ReplacePlaceholder(filePath, placeholder, replacement string) error {
+ data, err := ioutil.ReadFile(filePath)
+ if err != nil {
+ return err
+ }
+
+ fileContent := string(data)
+ fileContent = strings.Replace(fileContent, placeholder, replacement, -1)
+
+ return ioutil.WriteFile(filePath, []byte(fileContent), 0644)
+}
+
+// pemBlockForKey creates a PEM block for the given private key
+func pemBlockForKey(key *ecdsa.PrivateKey) ([]byte, error) {
+ der, err := x509.MarshalECPrivateKey(key)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal ECDSA private key: %w", err)
+ }
+
+ block := &pem.Block{
+ Type: "EC PRIVATE KEY",
+ Bytes: der,
+ }
+ return pem.EncodeToMemory(block), nil
+}
+
+// publicKeyToOpenSSH converts an ECDSA public key to OpenSSH format
+func publicKeyToOpenSSH(pub *ecdsa.PublicKey) (string, error) {
+ pubKey, err := ssh.NewPublicKey(pub)
+ if err != nil {
+ return "", fmt.Errorf("failed to convert ECDSA public key to SSH format: %w", err)
+ }
+ return string(ssh.MarshalAuthorizedKey(pubKey)), nil
+}
+
+// getKnownHostsEntry generates the known_hosts entry for the given URL
+func getKnownHostsEntry(url string) string {
+ if url == "github.com" {
+ return getGithubKnownHostsEntry()
+ }
+ return generateKnownHostsEntry(url)
+}
+
+// getGithubKnownHostsEntry generates the known_hosts entry specifically for github.com
+func getGithubKnownHostsEntry() string {
+ return "github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg="
+}
+
+// generateKnownHostsEntry generates an SSH known_hosts entry for the given URL
+func generateKnownHostsEntry(url string) string {
+ return fmt.Sprintf("%s ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=", url)
+}
+
+// encodeToBase64 encodes data to a base64 string
+func encodeToBase64(data []byte) string {
+ return string(data)
+}
+
+// decodeBase64 decodes a base64 string
+func decodeBase64(data string) ([]byte, error) {
+ return []byte(data), nil
+}
diff --git a/clustertool/pkg/gencmd/apply.go b/clustertool/pkg/gencmd/apply.go
new file mode 100644
index 0000000000000..9d1e2fd5248e0
--- /dev/null
+++ b/clustertool/pkg/gencmd/apply.go
@@ -0,0 +1,49 @@
+package gencmd
+
+import (
+ "io"
+ "os"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+
+ talhelperCfg "github.com/budimanjojo/talhelper/v3/pkg/config"
+ "github.com/budimanjojo/talhelper/v3/pkg/generate"
+ "github.com/truecharts/private/clustertool/embed"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+ "github.com/truecharts/private/clustertool/pkg/initfiles"
+)
+
+func GenApply(node string, extraFlags []string) []string {
+ initfiles.LoadTalEnv()
+ cfg, err := talhelperCfg.LoadAndValidateFromFile(helper.TalConfigFile, []string{helper.ClusterEnvFile}, false)
+ if err != nil {
+ log.Fatal().Err(err).Msg("failed to parse talconfig or talenv file: %s")
+ }
+
+ applyStdout := os.Stdout
+ r, w, _ := os.Pipe()
+ os.Stdout = w
+
+ err = generate.GenerateApplyCommand(cfg, helper.TalosGenerated, node, extraFlags)
+
+ w.Close()
+ out, _ := io.ReadAll(r)
+ os.Stdout = applyStdout
+
+ sliceOut := strings.Split(string(out), ";\n")
+ talosPath := embed.GetTalosExec()
+ var slice []string
+ for _, str := range sliceOut {
+ if str != "" {
+ str = strings.ReplaceAll(str, "talosctl", talosPath)
+ slice = append(slice, str)
+ }
+
+ }
+
+ if err != nil {
+ log.Fatal().Err(err).Msg("failed to generate talosctl apply command: %s")
+ }
+ return slice
+}
diff --git a/clustertool/pkg/gencmd/bootstrap.go b/clustertool/pkg/gencmd/bootstrap.go
new file mode 100644
index 0000000000000..f847c9a83a678
--- /dev/null
+++ b/clustertool/pkg/gencmd/bootstrap.go
@@ -0,0 +1,232 @@
+package gencmd
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+
+ talhelperCfg "github.com/budimanjojo/talhelper/v3/pkg/config"
+ "github.com/budimanjojo/talhelper/v3/pkg/generate"
+ "github.com/truecharts/private/clustertool/embed"
+ "github.com/truecharts/private/clustertool/pkg/fluxhandler"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+ "github.com/truecharts/private/clustertool/pkg/kubectlcmds"
+ "github.com/truecharts/private/clustertool/pkg/nodestatus"
+ "github.com/truecharts/private/clustertool/pkg/sops"
+)
+
+var HelmRepos map[string]*fluxhandler.HelmRepo
+
+func GenBootstrap(node string, extraFlags []string) string {
+ cfg, err := talhelperCfg.LoadAndValidateFromFile(helper.TalConfigFile, []string{helper.ClusterEnvFile}, false)
+ if err != nil {
+ log.Fatal().Err(err).Msg("failed to parse talconfig or talenv file: %s")
+ }
+
+ applyStdout := os.Stdout
+ r, w, _ := os.Pipe()
+ os.Stdout = w
+
+ err = generate.GenerateBootstrapCommand(cfg, helper.TalosGenerated, node, extraFlags)
+
+ w.Close()
+ out, _ := io.ReadAll(r)
+ os.Stdout = applyStdout
+
+ talosPath := embed.GetTalosExec()
+ strout := strings.ReplaceAll(string(out), "talosctl", talosPath)
+ strout = strings.ReplaceAll(strout, "\n", "")
+ strout = strings.ReplaceAll(strout, ";", "")
+
+ if err != nil {
+ log.Fatal().Err(err).Msg("failed to generate talosctl bootstrap command: %s")
+ }
+ return strout
+}
+
+func RunBootstrap(args []string) {
+ var extraArgs []string
+ if len(args) > 1 {
+ extraArgs = args[1:]
+ }
+ if err := sops.DecryptFiles(); err != nil {
+ log.Info().Msgf("Error decrypting files: %v\n", err)
+ }
+
+ bootstrapcmds := GenBootstrap("", extraArgs)
+ bootstrapNode := helper.ExtractNode(bootstrapcmds)
+
+ nodestatus.WaitForHealth(bootstrapNode, []string{"maintenance"})
+
+ taloscmds := GenApply(bootstrapNode, extraArgs)
+ ExecCmds(taloscmds, false)
+
+ nodestatus.WaitForHealth(bootstrapNode, []string{"booting"})
+
+ log.Info().Msgf("Bootstrap: At this point your system is installed to disk, please make sure not to reboot into the installer ISO/USB %s", bootstrapNode)
+
+ log.Info().Msgf("Bootstrap: running bootstrap on node: %s", bootstrapNode)
+ ExecCmd(bootstrapcmds)
+
+ log.Info().Msgf("Bootstrap: waiting for VIP %v to come online...", helper.TalEnv["VIP_IP"])
+ nodestatus.WaitForHealth(helper.TalEnv["VIP_IP"], []string{"running"})
+
+ log.Info().Msgf("Bootstrap: Configuring kubectl for VIP: %v", helper.TalEnv["VIP_IP"])
+ // Ensure kubeconfig is loaded
+ kubeconfigcmds := GenKubeConfig(helper.TalEnv["VIP_IP"])
+ ExecCmd(kubeconfigcmds)
+
+ // Desired pod names
+ requiredPods := []string{
+ "kube-controller-manager",
+ "kube-scheduler",
+ "kube-apiserver",
+ }
+
+ log.Info().Msgf("Bootstrap: Waiting for system Pods to be running for: %v", helper.TalEnv["VIP_IP"])
+ if err := kubectlcmds.CheckStatus(requiredPods, []string{}, 600); err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ os.Exit(1)
+ }
+
+ log.Info().Msg("Bootstrap: Starting Cluster configuration...")
+ // Start process to approve any cert requests till our manifests are loaded
+ // Set up a signal handler to handle termination gracefully
+ stopCh := make(chan struct{})
+
+ // Get Kubernetes clientset
+ clientset, err := kubectlcmds.GetClientset()
+ if err != nil {
+ log.Info().Msgf("Error getting Kubernetes clientset: %v", err)
+ return
+ }
+ ctx := context.Background()
+
+ helmRepoPath := filepath.Join("./repositories", "helm")
+ HelmRepos, err = fluxhandler.LoadAllHelmRepos(helmRepoPath)
+
+ // Call ApprovePendingCertificates with clientset and stopCh
+ go kubectlcmds.ApprovePendingCertificates(clientset, stopCh)
+
+ baseCharts := []fluxhandler.HelmChart{
+ // Pulled directly from upstream, due to this being very complex and important
+ {filepath.Join(helper.ClusterPath, "/kubernetes/kube-system/cilium/app"), false, true},
+ {filepath.Join(helper.ClusterPath, "/kubernetes/kube-system/kubelet-csr-approver/app"), false, true},
+ {filepath.Join(helper.ClusterPath, "/kubernetes/system/traefik-crds/app"), false, false},
+ {filepath.Join(helper.ClusterPath, "/kubernetes/system/prometheus-operator/app"), false, false},
+ }
+
+ fluxhandler.InstallCharts(baseCharts, HelmRepos, true)
+
+ log.Info().Msg("Bootstrap: Creating Namespaces...")
+
+ var namespaceFilePaths []string
+ var VSCfilePaths []string
+
+ // Walk through the directory recursively and find all namespace.yaml files
+ err = filepath.WalkDir(helper.ClusterPath, func(path string, d os.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if d.IsDir() {
+ return nil
+ }
+ if filepath.Base(path) == "namespace.yaml" {
+ namespaceFilePaths = append(namespaceFilePaths, path)
+ }
+ if filepath.Base(path) == "volumeSnapshotClass.yaml" {
+ VSCfilePaths = append(VSCfilePaths, path)
+ }
+ return nil
+ })
+
+ if err != nil {
+ fmt.Printf("Error walking the path: %v\n", err)
+ return
+ }
+
+ for _, filePath := range namespaceFilePaths {
+ log.Info().Msgf("Bootstrap: Loading namespace: %v", filePath)
+ if err := kubectlcmds.KubectlApply(ctx, filePath); err != nil {
+ log.Info().Msgf("Error applying manifest for %s: %v\n", filepath.Base(filePath), err)
+ os.Exit(1)
+ }
+ }
+
+ log.Info().Msg("Bootstrap: Base Cluster Configuration Completed, continuing setup...")
+ log.Info().Msg("Bootstrap: Confirming cluster health...")
+ healthcmd := GenHealth(helper.TalEnv["VIP_IP"])
+ ExecCmd(healthcmd)
+ close(stopCh)
+
+ prioCharts := []fluxhandler.HelmChart{
+ {filepath.Join(helper.ClusterPath, "/kubernetes/kube-system/spegel/app"), false, true},
+ {filepath.Join(helper.ClusterPath, "/kubernetes/system/cert-manager/app"), false, false},
+ }
+ fluxhandler.InstallCharts(prioCharts, HelmRepos, false)
+
+ intermediateCharts := []fluxhandler.HelmChart{
+ {filepath.Join(helper.ClusterPath, "/kubernetes/system/metallb/app"), false, false},
+ {filepath.Join(helper.ClusterPath, "/kubernetes/system/cloudnative-pg/app"), false, false},
+ {filepath.Join(helper.ClusterPath, "/kubernetes/kube-system/node-feature-discovery/app"), false, false},
+ {filepath.Join(helper.ClusterPath, "/kubernetes/kube-system/metrics-server/app"), false, false},
+ {filepath.Join(helper.ClusterPath, "/kubernetes/kube-system/descheduler/app"), false, false},
+ {filepath.Join(helper.ClusterPath, "/kubernetes/system/kubernetes-reflector/app"), false, false},
+ {filepath.Join(helper.ClusterPath, "/kubernetes/system/volsync/app"), false, true},
+ {filepath.Join(helper.ClusterPath, "/kubernetes/system/snapshot-controller/app"), false, true},
+ {filepath.Join(helper.ClusterPath, "/kubernetes/system/openebs/app"), false, true},
+ {filepath.Join(helper.ClusterPath, "/kubernetes/system/longhorn/app"), false, true},
+ {filepath.Join(helper.ClusterPath, "/kubernetes/system/csi-driver-smb/app"), false, true},
+ {filepath.Join(helper.ClusterPath, "/kubernetes/system/csi-driver-nfs/app"), false, false},
+ {filepath.Join(helper.ClusterPath, "/kubernetes/system/topolvm/app"), false, true},
+ }
+
+ fluxhandler.InstallCharts(intermediateCharts, HelmRepos, true)
+
+ // Desired pod names
+ requiredMLBPods := []string{
+ "metallb-controller",
+ "metallb-speaker",
+ }
+
+ log.Info().Msgf("Bootstrap: Waiting for MetalLB Pods to be running for: %v", helper.TalEnv["VIP_IP"])
+ if err := kubectlcmds.CheckStatus(requiredMLBPods, []string{}, 600); err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ os.Exit(1)
+ }
+
+ lateCharts := []fluxhandler.HelmChart{
+ {filepath.Join(helper.ClusterPath, "/kubernetes/core/metallb-config/app"), false, false},
+ }
+
+ log.Info().Msgf("Bootstrap: Loading VolumeSnapshotClasses")
+
+ for _, filePath := range VSCfilePaths {
+ log.Info().Msgf("Bootstrap: Loading VolumeSnapshotClass: %v", filePath)
+ if err := kubectlcmds.KubectlApply(ctx, filePath); err != nil {
+ log.Info().Msgf("Error applying manifest for %s: %v\n", filepath.Base(filePath), err)
+ os.Exit(1)
+ }
+ }
+
+ fluxhandler.InstallCharts(lateCharts, HelmRepos, true)
+
+ log.Info().Msg("Bootstrap: Installing included applications")
+ postCharts := []fluxhandler.HelmChart{
+ {filepath.Join(helper.ClusterPath, "/kubernetes/apps/kubernetes-dashboard/app"), false, true},
+ // TODO: Add Intel GPU CRD to truecharts and reference here
+ }
+
+ fluxhandler.InstallCharts(postCharts, HelmRepos, true)
+
+ log.Info().Msg("------")
+
+ fluxhandler.FluxBootstrap(ctx)
+
+ log.Info().Msg("Bootstrap: Completed Successfully!")
+}
diff --git a/clustertool/pkg/gencmd/execcmd.go b/clustertool/pkg/gencmd/execcmd.go
new file mode 100644
index 0000000000000..052ccd07189f2
--- /dev/null
+++ b/clustertool/pkg/gencmd/execcmd.go
@@ -0,0 +1,134 @@
+package gencmd
+
+import (
+ "os"
+ "strings"
+ "time"
+
+ "github.com/rs/zerolog/log"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+ "github.com/truecharts/private/clustertool/pkg/nodestatus"
+)
+
+func ExecCmd(cmd string) {
+ argslice := strings.Split(cmd, " ")
+ // log.Info().Msgf("Running: %s\n", argslice[0:])
+
+ // log.Info().Msg("test", strings.Join(argslice, " "))
+ out, err := helper.RunCommand(argslice, false)
+ if err != nil {
+ log.Info().Msgf("err: %v", err)
+ if strings.Contains(cmd, "bootstrap") {
+ log.Info().Msg("Bootstrap: Fail, retrying...")
+ time.Sleep(5 * time.Second)
+ out, err = helper.RunCommand(argslice, false)
+
+ if err != nil && strings.Contains(string(out), "bootstrap is not available yet") {
+ start := time.Now()
+ timeout := 2 * time.Minute
+
+ for {
+ log.Info().Msg("Bootstrap: Fail, retrying...")
+ time.Sleep(5 * time.Second)
+
+ out, err = helper.RunCommand(argslice, false)
+ if err != nil || !strings.Contains(string(out), "bootstrap is not available yet") {
+ break
+ }
+ if time.Since(start) >= timeout {
+ log.Info().Msg("Timeout reached: Node not ready for bootstrap within 2 minutes.")
+ break
+ }
+ }
+ }
+ }
+
+ }
+}
+
+func ExecCmds(taloscmds []string, healthcheck bool) error {
+ log.Info().Msg("Regenerating config prior to apply...")
+ GenConfig([]string{})
+ var todocmds []string
+ var healthcmd string
+ skipped := false
+ if healthcheck {
+ log.Info().Msg("Pre-Run Healthchecks...")
+
+ for _, command := range taloscmds {
+ node := helper.ExtractNode(command)
+ log.Info().Msgf("checking node availability: %v", node)
+ err := nodestatus.CheckHealth(node, "", false)
+ if err != nil {
+ log.Info().Msgf("node seems not to be runnign correctly and cannot be used %v", node)
+ log.Info().Msgf("node This will also make it impossible to poll total-cluster-health as well... %v", node)
+ if !helper.GetYesOrNo("Do you want to continue without this node? (yes/no) [y/n]: ") {
+ log.Info().Msg("Exiting...")
+ os.Exit(1)
+ } else {
+ skipped = true
+ }
+ }
+ todocmds = append(todocmds, command)
+ }
+ if skipped {
+ log.Info().Msg("skipping cluster health check due to unhealthy nodes being ignored...")
+ } else {
+ if helper.GetYesOrNo("Do you want to check the health of the cluster? (yes/no) [y/n]: ") {
+ log.Info().Msg("Checking if cluster is healthy...")
+ healthcmd := GenHealth(helper.TalEnv["VIP_IP"])
+ ExecCmd(healthcmd)
+ } else {
+ skipped = true
+ }
+ }
+ } else {
+ todocmds = taloscmds
+ }
+
+ log.Info().Msg("Executing Cmds...")
+ for _, command := range todocmds {
+ node := helper.ExtractNode(command)
+ log.Info().Msgf("Executing commands on node: %v", node)
+ argslice := strings.Split(string(command), " ")
+ // log.Info().Msg("test", strings.Join(argslice, " "))
+ out, err := helper.RunCommand(argslice, false)
+ if err != nil {
+ if strings.Contains(string(out), "certificate signed by unknown authority") {
+ argslice = append(argslice, "--insecure")
+ _, err2 := helper.RunCommand(argslice, false)
+ if err2 != nil {
+ log.Info().Msgf("err: %v", err2)
+ }
+ } else {
+ log.Info().Msgf("err: %v", err)
+ log.Info().Msgf("node has thrown an error... %v", node)
+ if !helper.GetYesOrNo("Are you sure you want to continue applying this to other nodes? (yes/no) [y/n]: ") {
+ log.Info().Msg("Exiting...")
+ os.Exit(1)
+ }
+
+ }
+
+ }
+ time.Sleep(15 * time.Second)
+
+ if healthcheck {
+ log.Info().Msgf("checking if node is back online: %v", node)
+ err := nodestatus.CheckHealth(node, "", false)
+ if err != nil {
+ log.Info().Msgf("node seems not to be running correctly... %v", node)
+ if !helper.GetYesOrNo("Are you sure you want to continue applying this to other nodes? (yes/no) [y/n]: ") {
+ log.Info().Msg("Exiting...")
+ os.Exit(1)
+ }
+ }
+ }
+ }
+
+ if healthcheck && !skipped && !strings.Contains(taloscmds[0], "upgrade") {
+ log.Info().Msg("Checking if cluster is healthy after commands...")
+ ExecCmd(healthcmd)
+ }
+ return nil
+}
diff --git a/clustertool/pkg/gencmd/genconfig.go b/clustertool/pkg/gencmd/genconfig.go
new file mode 100644
index 0000000000000..1b333c565c47a
--- /dev/null
+++ b/clustertool/pkg/gencmd/genconfig.go
@@ -0,0 +1,171 @@
+package gencmd
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "log/slog"
+ "os"
+ "path"
+
+ "github.com/rs/zerolog/log"
+
+ talhelperCfg "github.com/budimanjojo/talhelper/v3/pkg/config"
+ "github.com/budimanjojo/talhelper/v3/pkg/generate"
+ "github.com/budimanjojo/talhelper/v3/pkg/substitute"
+ "github.com/budimanjojo/talhelper/v3/pkg/talos"
+ "github.com/fatih/color"
+ sideroConfig "github.com/siderolabs/talos/pkg/machinery/config"
+ "github.com/siderolabs/talos/pkg/machinery/config/generate/secrets"
+ "github.com/truecharts/private/clustertool/pkg/fluxhandler"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+ "github.com/truecharts/private/clustertool/pkg/initfiles"
+ "gopkg.in/yaml.v3"
+)
+
+func GenConfig(args []string) error {
+ initfiles.GenSchema()
+ initfiles.GenTalEnvConfigMap()
+ initfiles.CheckEnvVariables()
+ initfiles.GenPatches()
+ fluxhandler.CreateSshPatch()
+ genTalSecret()
+ validateTalConfig(args)
+ talhelperGenConfig()
+ validateTalConfig(args)
+ initfiles.UpdateGitRepo()
+
+ if err := fluxhandler.ProcessDirectory(path.Join(helper.ClusterPath, "kubernetes")); err != nil {
+ log.Info().Msgf("Error: %v", err)
+ }
+ if err := fluxhandler.ProcessDirectory(path.Join(helper.ClusterPath, "kubernetes")); err != nil {
+ log.Info().Msgf("Error: %v", err)
+ } else {
+ log.Info().Msgf("Kustomizations processed successfully.")
+ }
+ helper.CreateEncrPreCommitHook()
+ log.Info().Msg("GenConfig: Completed Successfully!")
+ return nil
+}
+
+func genTalSecret() error {
+
+ log.Info().Msg("Generating TalSecret...")
+
+ if _, err := os.Stat(helper.TalSecretFile); err == nil {
+
+ } else if errors.Is(err, os.ErrNotExist) {
+ os.MkdirAll(helper.TalosGenerated, os.ModePerm)
+ outfile, err := os.Create(helper.TalSecretFile)
+ if err != nil {
+ panic(err)
+ }
+ defer outfile.Close()
+
+ var s *secrets.Bundle
+ version, _ := sideroConfig.ParseContractFromVersion(talhelperCfg.LatestTalosVersion)
+ s, err = talos.NewSecretBundle(secrets.NewClock(), *version)
+ if err != nil {
+ return err
+ }
+
+ buf := new(bytes.Buffer)
+ encoder := yaml.NewEncoder(buf)
+ encoder.SetIndent(2)
+
+ err = encoder.Encode(s)
+
+ if err != nil {
+ return err
+ }
+
+ _, err = outfile.Write(buf.Bytes())
+ if err != nil {
+ // Handle the error
+ panic(err)
+ }
+
+ return nil
+
+ } else {
+
+ }
+ return nil
+}
+
+func talhelperGenConfig() error {
+ genconfigTalosMode := "metal"
+ genconfigNoGitignore := false
+ genconfigDryRun := false
+ genconfigOfflineMode := false
+
+ cfg, err := talhelperCfg.LoadAndValidateFromFile(helper.TalConfigFile, []string{helper.ClusterEnvFile}, true)
+ if err != nil {
+ log.Fatal().Err(err).Msgf("failed to parse TalConfig or talenv file: %s", err)
+ }
+ log.Info().Msg("Start Generating Config File...")
+
+ err = generate.GenerateConfig(cfg, genconfigDryRun, helper.TalosGenerated, helper.TalSecretFile, genconfigTalosMode, genconfigOfflineMode)
+ if err != nil {
+ log.Fatal().Err(err).Msgf("failed to generate talos config: %s", err)
+ }
+
+ if !genconfigNoGitignore && !genconfigDryRun {
+ err = cfg.GenerateGitignore(helper.TalosGenerated)
+ if err != nil {
+ log.Fatal().Err(err).Msgf("failed to generate gitignore file: %s", err)
+ }
+ }
+ return nil
+}
+
+func validateTalConfig(argsInt []string) error {
+ cfg := helper.TalConfigFile
+ validateTHNoSubstitute := false
+ args := []string{}
+ if len(args) > 0 {
+ cfg = args[0]
+ }
+
+ log.Info().Msg("start loading and validating config file")
+ slog.Debug(fmt.Sprintf("reading %s", cfg))
+ cfgByte, err := os.ReadFile(cfg)
+ if err != nil {
+ log.Fatal().Err(err).Msgf("failed to read config file: %s", err)
+ }
+
+ if !validateTHNoSubstitute {
+ if err := substitute.LoadEnvFromFiles([]string{helper.ClusterEnvFile}); err != nil {
+ log.Fatal().Err(err).Msg("failed to load env file: %s")
+ }
+ cfgByte, err = substitute.SubstituteEnvFromByte(cfgByte)
+ if err != nil {
+ log.Fatal().Err(err).Msg("failed trying to substitute env: %s")
+ }
+ }
+
+ errs, warns, err := talhelperCfg.ValidateFromByte(cfgByte)
+ if err != nil {
+ log.Fatal().Err(err).Msgf("failed to validate talhelper config file: %s", err)
+ }
+
+ if len(errs) > 0 || len(warns) > 0 {
+ color.Red("There are issues with your talhelper config file:")
+ grouped := make(map[string][]string)
+ for _, v := range errs {
+ grouped[v.Field] = append(grouped[v.Field], v.Message.Error())
+ }
+ for _, v := range warns {
+ grouped[v.Field] = append(grouped[v.Field], v.Message)
+ }
+ for field, list := range grouped {
+ color.Yellow("field: %q\n", field)
+ for _, l := range list {
+ log.Info().Msgf(l + "\n")
+ }
+ }
+ } else {
+ log.Info().Msg("Your talhelper config file is looking great!")
+ }
+ return nil
+}
diff --git a/clustertool/pkg/gencmd/health.go b/clustertool/pkg/gencmd/health.go
new file mode 100644
index 0000000000000..bde930622c36e
--- /dev/null
+++ b/clustertool/pkg/gencmd/health.go
@@ -0,0 +1,12 @@
+package gencmd
+
+import (
+ "github.com/truecharts/private/clustertool/embed"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+func GenHealth(node string) string {
+ talosPath := embed.GetTalosExec()
+ strout := talosPath + " health --talosconfig " + helper.TalosConfigFile + " -n " + node
+ return strout
+}
diff --git a/clustertool/pkg/gencmd/kubeconfig.go b/clustertool/pkg/gencmd/kubeconfig.go
new file mode 100644
index 0000000000000..9d8b7b516f886
--- /dev/null
+++ b/clustertool/pkg/gencmd/kubeconfig.go
@@ -0,0 +1,12 @@
+package gencmd
+
+import (
+ "github.com/truecharts/private/clustertool/embed"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+func GenKubeConfig(node string) string {
+ talosPath := embed.GetTalosExec()
+ strout := talosPath + " kubeconfig --talosconfig " + helper.TalosConfigFile + " -n " + node + " --force"
+ return strout
+}
diff --git a/clustertool/pkg/gencmd/reset.go b/clustertool/pkg/gencmd/reset.go
new file mode 100644
index 0000000000000..32579d62c5fb0
--- /dev/null
+++ b/clustertool/pkg/gencmd/reset.go
@@ -0,0 +1,47 @@
+package gencmd
+
+import (
+ "io"
+ "os"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+
+ talhelperCfg "github.com/budimanjojo/talhelper/v3/pkg/config"
+ "github.com/budimanjojo/talhelper/v3/pkg/generate"
+ "github.com/truecharts/private/clustertool/embed"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+func GenReset(node string, extraFlags []string) []string {
+ cfg, err := talhelperCfg.LoadAndValidateFromFile(helper.TalConfigFile, []string{helper.ClusterEnvFile}, false)
+ if err != nil {
+ log.Fatal().Err(err).Msgf("failed to parse talconfig or talenv file: %s", err)
+ }
+
+ resetStdout := os.Stdout
+ r, w, _ := os.Pipe()
+ os.Stdout = w
+
+ err = generate.GenerateResetCommand(cfg, helper.TalosGenerated, node, extraFlags)
+
+ w.Close()
+ out, _ := io.ReadAll(r)
+ os.Stdout = resetStdout
+
+ sliceOut := strings.Split(string(out), ";\n")
+ talosPath := embed.GetTalosExec()
+ var slice []string
+ for _, str := range sliceOut {
+ if str != "" {
+ str = strings.ReplaceAll(str, "talosctl", talosPath)
+ slice = append(slice, str)
+ }
+
+ }
+
+ if err != nil {
+ log.Fatal().Err(err).Msgf("failed to generate talosctl reset command: %s", err)
+ }
+ return slice
+}
diff --git a/clustertool/pkg/gencmd/upgrade.go b/clustertool/pkg/gencmd/upgrade.go
new file mode 100644
index 0000000000000..8862721d74164
--- /dev/null
+++ b/clustertool/pkg/gencmd/upgrade.go
@@ -0,0 +1,53 @@
+package gencmd
+
+import (
+ "io"
+ "os"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+
+ talhelperCfg "github.com/budimanjojo/talhelper/v3/pkg/config"
+ "github.com/budimanjojo/talhelper/v3/pkg/generate"
+ "github.com/truecharts/private/clustertool/embed"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+func GenUpgrade(node string, extraFlags []string) []string {
+ cfg, err := talhelperCfg.LoadAndValidateFromFile(helper.TalConfigFile, []string{helper.ClusterEnvFile}, false)
+ if err != nil {
+ log.Fatal().Err(err).Msgf("failed to parse talconfig or talenv file: %s", err)
+ }
+
+ upgradeStdout := os.Stdout
+ r, w, _ := os.Pipe()
+ os.Stdout = w
+ extraFlags = append(extraFlags, "--preserve")
+ err = generate.GenerateUpgradeCommand(cfg, helper.TalosGenerated, node, extraFlags)
+
+ w.Close()
+ out, _ := io.ReadAll(r)
+ os.Stdout = upgradeStdout
+
+ sliceOut := strings.Split(string(out), ";\n")
+ talosPath := embed.GetTalosExec()
+ var slice []string
+ for _, str := range sliceOut {
+ if str != "" {
+ str = strings.ReplaceAll(str, "talosctl", talosPath)
+ slice = append(slice, str)
+ }
+
+ }
+
+ if err != nil {
+ log.Fatal().Err(err).Msgf("failed to generate talosctl upgrade command: %s", err)
+ }
+ return slice
+}
+
+func GenKubeUpgrade(node string) string {
+ talosPath := embed.GetTalosExec()
+ strout := talosPath + " upgrade-k8s --talosconfig " + helper.TalosConfigFile + " -n " + node
+ return strout
+}
diff --git a/clustertool/pkg/helper/copy.go b/clustertool/pkg/helper/copy.go
new file mode 100644
index 0000000000000..fa737075450dd
--- /dev/null
+++ b/clustertool/pkg/helper/copy.go
@@ -0,0 +1,120 @@
+package helper
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+)
+
+// copyDirInternal copies files and directories from src to dest, preserving the directory structure.
+// If replaceExisting is true, it will overwrite existing files in the destination.
+// The filter string specifies files to be included (can be a regex pattern).
+func copyDirInternal(src, dest string, replaceExisting bool, filter string) error {
+ var regexFilter *regexp.Regexp
+ var err error
+
+ if filter != "" {
+ // Compile filter string into regex pattern
+ regexFilter, err = regexp.Compile(filter)
+ if err != nil {
+ return err
+ }
+ }
+
+ err = filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ // Determine the new path relative to the source directory
+ relPath, err := filepath.Rel(src, path)
+ if err != nil {
+ return err
+ }
+
+ // Add debug output to verify the files being processed
+ // log.Info().Msgf("Processing: %s\n", relPath)
+
+ if regexFilter != nil && !regexFilter.MatchString(relPath) {
+ // Skip files that do not match the regex filter
+ if info.IsDir() {
+ // log.Info().Msgf("Skipping directory (filtered): %s\n", relPath)
+ return filepath.SkipDir // Skip entire directory if filtered out
+ }
+ // log.Info().Msgf("Skipping file (filtered): %s\n", relPath)
+ return nil // Skip the file itself if filtered out
+ }
+
+ // Replace DOTREPLACE in the destination path
+ destPath := filepath.Join(dest, relPath)
+ destPath = ReplaceDotInFilename(destPath)
+
+ if info.IsDir() {
+ // If it's a directory, create the directory in the destination
+ if err := os.MkdirAll(destPath, os.ModePerm); err != nil {
+ return err
+ }
+ } else {
+ // If it's a file, copy the file
+ if _, err := os.Stat(destPath); os.IsNotExist(err) || replaceExisting {
+ if err := CopyFile(path, destPath, replaceExisting); err != nil {
+ return err
+ }
+ } else {
+ //log.Info().Msgf("Skipping existing file: %s\n", destPath)
+ }
+ }
+ return nil
+ })
+ return err
+}
+
+// replaceDotInFilename replaces DOTREPLACE with a dot (.) in the given filename.
+func ReplaceDotInFilename(filename string) string {
+ return strings.ReplaceAll(filename, "DOTREPLACE", ".")
+}
+
+func CopyDir(src, dest string, replaceExisting bool) error {
+ if err := copyDirInternal(src, dest, replaceExisting, ""); err != nil {
+ return err
+ }
+ return nil
+}
+
+func CopyDirFiltered(src, dest string, replaceExisting bool, filter string) error {
+ if err := copyDirInternal(src, dest, replaceExisting, filter); err != nil {
+ return err
+ }
+ return nil
+}
+
+// CopyFile copies a file from source to destination. If replaceExisting is true, it will overwrite existing files in the destination.
+func CopyFile(source, destination string, replaceExisting bool) error {
+ if !replaceExisting {
+ if _, err := os.Stat(destination); err == nil {
+ log.Info().Msgf("Skipping existing file: %s\n", destination)
+ return nil
+ } else if !os.IsNotExist(err) {
+ return err
+ }
+ }
+
+ sourceFile, err := os.Open(source)
+ if err != nil {
+ return err
+ }
+ defer sourceFile.Close()
+
+ destinationFile, err := os.Create(destination)
+ if err != nil {
+ return err
+ }
+ defer destinationFile.Close()
+
+ _, err = io.Copy(destinationFile, sourceFile)
+ return err
+}
diff --git a/clustertool/pkg/helper/dns.go b/clustertool/pkg/helper/dns.go
new file mode 100644
index 0000000000000..81e04b9aa341e
--- /dev/null
+++ b/clustertool/pkg/helper/dns.go
@@ -0,0 +1,40 @@
+package helper
+
+import (
+ "net"
+ "os"
+
+ "github.com/rs/zerolog/log"
+)
+
+func checkDNSResolution(domain string) bool {
+ _, err := net.LookupHost(domain)
+ if err != nil {
+ return false
+ }
+ return true
+}
+
+func checkAllDomains(domains []string, verbose bool) {
+ for _, domain := range domains {
+ if !checkDNSResolution(domain) {
+ log.Info().Msgf("DNS for %s does not resolve.\n", domain)
+ os.Exit(1)
+ } else {
+ if verbose {
+ log.Info().Msgf("DNS for %s resolves.\n", domain)
+ }
+ }
+ }
+}
+
+func CheckReqDomains() {
+ domains := []string{
+ "tccr.io",
+ "ghcr.io",
+ "github.com",
+ "docker.com",
+ }
+
+ checkAllDomains(domains, false)
+}
diff --git a/clustertool/pkg/helper/envsubst.go b/clustertool/pkg/helper/envsubst.go
new file mode 100644
index 0000000000000..b4c0d34956dee
--- /dev/null
+++ b/clustertool/pkg/helper/envsubst.go
@@ -0,0 +1,155 @@
+package helper
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "log/slog"
+ "os"
+ "path/filepath"
+ "regexp"
+
+ "github.com/joho/godotenv"
+ "github.com/rs/zerolog/log"
+)
+
+// LoadEnvFromFile reads yaml data from the "output.yaml" file and sets
+// environment variables in the global output map. It skips if the file
+// doesn't exist. It returns an error, if any.
+func LoadEnvFromFile(file string, output map[string]string) error {
+ if _, err := os.Stat(file); err == nil {
+ slog.Debug(fmt.Sprintf("loading environment variables from %s", file))
+ content, err := os.ReadFile(file)
+ if err != nil {
+ return fmt.Errorf("reading file %s: %s", file, err)
+ }
+
+ // Strip comments from YAML content before processing
+ content = StripYamlComment(content)
+
+ // See: https://github.com/budimanjojo/talhelper/issues/220
+ content = StripYAMLDocDelimiter(content)
+ if err := LoadEnv(content, output); err != nil {
+ return fmt.Errorf("trying to load env from %s: %s", file, err)
+ }
+ } else if errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("file %s does not exist", file)
+ } else {
+ return fmt.Errorf("trying to stat %s: %s", file, err)
+ }
+ return nil
+}
+
+// LoadEnv reads yaml data and sets environment variables in the global output map.
+// It returns an error, if any.
+func LoadEnv(file []byte, output map[string]string) error {
+ mFile, err := godotenv.Unmarshal(string(file))
+ if err != nil {
+ return err
+ }
+
+ for k, v := range mFile {
+ slog.Debug(fmt.Sprintf("loaded environment variable: %s=%s", k, v))
+ output[k] = v
+ }
+ return nil
+}
+
+// stripYamlComment takes yaml bytes and returns them back with
+// comments stripped.
+func StripYamlComment(file []byte) []byte {
+ // FIXME use better logic than regex.
+ re := regexp.MustCompile(".?#.*\n")
+ stripped := re.ReplaceAllFunc(file, func(b []byte) []byte {
+ re := regexp.MustCompile("^['\"].+['\"]|^[a-zA-Z0-9]")
+ if re.Match(b) {
+ return b
+ } else {
+ return []byte("\n")
+ }
+ })
+
+ var final bytes.Buffer
+ for _, line := range bytes.Split(stripped, []byte("\n")) {
+ if len(bytes.TrimSpace(line)) > 0 {
+ final.WriteString(string(line) + "\n")
+ }
+ }
+
+ return final.Bytes()
+}
+
+// stripYAMLDocDelimiter replaces YAML document delimiter with an empty line.
+func StripYAMLDocDelimiter(src []byte) []byte {
+ re := regexp.MustCompile(`(?m)^---\n`)
+ return re.ReplaceAll(src, []byte("\n"))
+}
+
+// EnvSubst replaces occurrences of ${SOMETHING} with values from output
+// in the content of the specified file. Returns the modified content.
+func EnvSubst(filename string, envs map[string]string) (string, error) {
+ // Read the content of the file
+ content, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return "", fmt.Errorf("failed to read file %s: %v", filename, err)
+ }
+
+ // Regular expression to match ${SOMETHING}
+ re := regexp.MustCompile(`\$\{([^\}]+)\}`)
+
+ // Replace occurrences with values from output
+ modifiedContent := re.ReplaceAllStringFunc(string(content), func(match string) string {
+ // Extract SOMETHING from ${SOMETHING}
+ key := match[2 : len(match)-1]
+ // Check if SOMETHING exists in output
+ if value, ok := envs[key]; ok {
+ return value
+ }
+ // If SOMETHING doesn't exist in output, return the original match
+ return match
+ })
+
+ // Write the modified content back to the file
+ err = ioutil.WriteFile(filename, []byte(modifiedContent), 0644)
+ if err != nil {
+ return "", fmt.Errorf("failed to write file %s: %v", filename, err)
+ }
+
+ return modifiedContent, nil
+}
+
+// EnvSubstRecursive applies EnvSubst to all files matching the regex pattern
+// within the specified directory path (and its subdirectories).
+func EnvSubstRecursive(rootPath string, regexPattern string, envs map[string]string) error {
+ // Compile the regex pattern for filename matching
+ re, err := regexp.Compile(regexPattern)
+ if err != nil {
+ return fmt.Errorf("failed to compile regex pattern: %v", err)
+ }
+
+ // Walk through the directory structure
+ err = filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return fmt.Errorf("error accessing path %s: %v", path, err)
+ }
+
+ // Check if the file matches the regex pattern and is not a directory
+ if !info.IsDir() && re.MatchString(info.Name()) {
+ // Apply EnvSubst to the file
+ _, err := EnvSubst(path, envs)
+ if err != nil {
+ return fmt.Errorf("error processing file %s: %v", path, err)
+ }
+ log.Info().Msgf("Processed file: %s\n", path)
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return fmt.Errorf("error walking path %s: %v", rootPath, err)
+ }
+
+ return nil
+}
diff --git a/clustertool/pkg/helper/git.go b/clustertool/pkg/helper/git.go
new file mode 100644
index 0000000000000..f2453441fb5c2
--- /dev/null
+++ b/clustertool/pkg/helper/git.go
@@ -0,0 +1,344 @@
+package helper
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+)
+
+// getStagedFiles lists all files that are staged for commit
+func GetStagedFiles() ([]string, error) {
+ // Run git diff --cached --name-only to get staged files
+ cmd := exec.Command("git", "diff", "--cached", "--name-only")
+
+ var out bytes.Buffer
+ cmd.Stdout = &out
+
+ if err := cmd.Run(); err != nil {
+ return nil, fmt.Errorf("failed to run git command: %v", err)
+ }
+
+ // Split the output into lines (file names)
+ output := strings.TrimSpace(out.String())
+ if output == "" {
+ return nil, nil
+ }
+
+ files := strings.Split(output, "\n")
+ return files, nil
+}
+
+func StageFiles(files []string) error {
+ for _, file := range files {
+ // Stage the file using `git add `
+ cmd := exec.Command("git", "add", file)
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("failed to stage file %s: %v", file, err)
+ }
+ }
+ return nil
+}
+
+//////
+
+// StageFile stages the given file using `git add`
+func StageFile(filePath string) error {
+ cmd := exec.Command("git", "add", filePath)
+ var out bytes.Buffer
+ cmd.Stdout = &out
+ err := cmd.Run()
+ if err != nil {
+ return fmt.Errorf("error staging file: %v", err)
+ }
+ return nil
+}
+
+// GetGitStagedFiles returns a list of git-staged files, excluding ignored files.
+func GetGitStagedFiles() ([]string, error) {
+ // Get the list of staged files
+ cmd := exec.Command("git", "diff", "--cached", "--name-only")
+ var out bytes.Buffer
+ cmd.Stdout = &out
+ if err := cmd.Run(); err != nil {
+ return nil, err
+ }
+
+ // Split the output into a slice of file names
+ files := strings.Split(strings.TrimSpace(out.String()), "\n")
+
+ return files, nil
+}
+
+// IsFileIgnored checks if a file is ignored by Git.
+func IsFileIgnored(file string) (bool, error) {
+ // Check if the file is ignored as-is
+ ignored, err := checkIgnore(file)
+ if err != nil {
+ return false, err // Return error if checking fails
+ }
+ if ignored {
+ return true, nil // Return true if the file is ignored as-is
+ }
+
+ // Check if the file is ignored with "clustertool/" prefix
+ prefixedFile := "clustertool/" + file
+ ignored, err = checkIgnore(prefixedFile)
+ if err != nil {
+ return false, err // Return error if checking fails
+ }
+
+ return ignored, nil // Return the result of the prefixed check
+}
+
+// checkIgnore is a helper function that runs the git check-ignore command for a given file.
+func checkIgnore(file string) (bool, error) {
+ // Define the base folder to check
+ devTrigger := "DEVTRIGGER"
+
+ // Check if the directory exists
+ if _, err := os.Stat(devTrigger); !os.IsNotExist(err) {
+
+ // If the directory exists, skip checks for specified paths
+ // Check if the file path starts with the specified prefixes
+ if isPathIgnored(file, []string{
+ "repositories",
+ "clusters",
+ "clustertool/repositories",
+ "clustertool/clusters",
+ }) {
+ return true, nil // Skip ignoring check
+ }
+ }
+
+ // Run the git check-ignore command for the given file
+ cmd := exec.Command("git", "check-ignore", file)
+ if err := cmd.Run(); err != nil {
+ // If the error is an ExitError, check the exit code
+ if exitError, ok := err.(*exec.ExitError); ok {
+ if exitError.ExitCode() == 1 {
+ // The file is ignored (exit code 1 indicates that the file is ignored)
+ return true, nil
+ }
+ }
+ // If there's another error, return it
+ return false, err
+ }
+ // If the command succeeds (exit code 0), the file is not ignored
+ return false, nil
+}
+
+// isPathIgnored checks if the file path starts with any of the specified prefixes.
+func isPathIgnored(file string, prefixes []string) bool {
+ for _, prefix := range prefixes {
+ // Use filepath.HasPrefix to check if the file path starts with the prefix
+ if filepath.HasPrefix(file, prefix) {
+ return true
+ }
+ }
+ return false
+}
+
+// IsFileFullyStaged checks if a file is fully staged (no pending unstaged changes)
+// for both the unprefixed path and the path prefixed with /clustertool.
+// It ignores files that are listed in .gitignore.
+func IsFileFullyStaged(filePath string) (bool, error) {
+ // Get the Git root directory
+ cmd := exec.Command("git", "rev-parse", "--show-toplevel")
+ var out bytes.Buffer
+ cmd.Stdout = &out
+ err := cmd.Run()
+ if err != nil {
+ return false, err
+ }
+ gitRoot := strings.TrimSpace(out.String())
+
+ // Check if the clustertool directory exists in the Git root
+ clustertoolPath := filepath.Join(gitRoot, "clustertool")
+ _, err = exec.Command("test", "-d", clustertoolPath).Output()
+ clustertoolExists := (err == nil)
+
+ // Create a slice of file paths to check
+ filePaths := []string{filePath}
+ if clustertoolExists {
+ filePaths = append(filePaths, "clustertool/"+filePath)
+ }
+
+ // Check if the files are ignored
+ for _, path := range filePaths {
+ ignoredCmd := exec.Command("git", "check-ignore", path)
+ var ignoredOut bytes.Buffer
+ ignoredCmd.Stdout = &ignoredOut
+ err := ignoredCmd.Run()
+ if err == nil {
+ // If there's no error, the file is ignored
+ continue // Skip this file since it's ignored
+ }
+
+ // If the file is not ignored, check for unstaged changes
+ diffCmd := exec.Command("git", "diff", path) // Check for unstaged changes
+ var diffOut bytes.Buffer
+ diffCmd.Stdout = &diffOut
+ err = diffCmd.Run()
+ if err != nil {
+ return false, err
+ }
+
+ // If there's output from git diff, it means there are unstaged changes
+ if strings.TrimSpace(diffOut.String()) != "" {
+ return false, nil // Found unstaged changes
+ }
+ }
+
+ // If no unstaged changes were found for both paths and files were not ignored
+ return true, nil
+}
+
+// IsCurrentDirGitRepo checks if the current directory is a Git repository.
+func IsCurrentDirGitRepo() (bool, error) {
+ // Get the current working directory
+ dir, err := os.Getwd()
+ if err != nil {
+ return false, err
+ }
+
+ // Construct the path to the .git directory
+ gitDir := filepath.Join(dir, ".git")
+
+ // Check if the .git directory exists and is a directory
+ info, err := os.Stat(gitDir)
+ if os.IsNotExist(err) {
+ return false, nil
+ }
+ if err != nil {
+ return false, err
+ }
+ return info.IsDir(), nil
+}
+
+// CreateEncrPreCommitHook creates a pre-commit hook script in the .git/hooks directory
+func CreateEncrPreCommitHook() error {
+ isRepo, err := IsCurrentDirGitRepo()
+ if err != nil {
+ return fmt.Errorf("error checking if current directory is a Git repository: %v", err)
+ } else if isRepo {
+ log.Info().Msg("Bootstrap: The current directory is a valid GIT repository, creating precommit hook...")
+ } else {
+ log.Info().Msg("The current directory is not a valid GIT repository. Skipping precommit hook creation...")
+ return nil
+ }
+
+ // Get the current working directory
+ dir, err := os.Getwd()
+ if err != nil {
+ return fmt.Errorf("could not get current working directory: %v", err)
+ }
+
+ // Define the path to the .git/hooks directory
+ hooksDir := filepath.Join(dir, ".git", "hooks")
+ var hookPath string
+
+ if runtime.GOOS == "windows" {
+ hookPath = filepath.Join(hooksDir, "pre-commit.bat")
+ } else {
+ hookPath = filepath.Join(hooksDir, "pre-commit")
+ }
+
+ // Define the script path
+ filename := "precommit"
+ scriptPath := filepath.Join(CacheDir, filename)
+ var hookScript string
+
+ // Check if go.mod exists to decide on the script content
+ goModPath := filepath.Join(dir, "go.mod")
+ if _, err := os.Stat(goModPath); !os.IsNotExist(err) {
+ // If go.mod exists, use `go run . checkcrypt`
+ hookScript = fmt.Sprintf(`#!/bin/sh
+# Pre-commit hook script
+
+# Use go run . checkcrypt if go.mod exists
+echo "Running pre-commit encryption check..."
+# go run . checkcrypt
+if [ $? -ne 0 ]; then
+ echo "Pre-commit encryption check failed. Commit aborted."
+ exit 1
+fi
+`)
+ } else {
+ // Otherwise, use the file
+ switch runtime.GOOS {
+ case "windows":
+ // On Windows, the script must be a batch file or similar executable
+ scriptPath = filepath.ToSlash(scriptPath) // Ensure path format is correct for Windows
+ // Add .exe suffix for Windows
+ scriptPath = scriptPath + ".exe"
+ hookScript = fmt.Sprintf(`@echo off
+REM Pre-commit hook script
+
+REM Path to the script to run
+set scriptPath=%s
+
+REM Check if the script exists
+if exist "%%scriptPath%%" (
+ echo Running pre-commit script...
+ "%%scriptPath%%"
+ if errorlevel 1 (
+ echo Pre-commit script failed. Commit aborted.
+ exit /b 1
+ )
+) else (
+ echo Script %%scriptPath%% not found. Commit aborted.
+ exit /b 1
+)
+`, scriptPath)
+
+ default:
+ // For Unix-like systems: Linux, macOS, and FreeBSD
+ hookScript = fmt.Sprintf(`#!/bin/sh
+# Pre-commit hook script
+
+# Check if the script exists and is executable
+if [ -x "%s" ]; then
+ echo "Running pre-commit script..."
+ "%s"
+ if [ $? -ne 0 ]; then
+ echo "Pre-commit script failed. Commit aborted."
+ exit 1
+ fi
+else
+ echo "Script %s not found or not executable. Commit aborted."
+ exit 1
+fi
+`, scriptPath, scriptPath, scriptPath)
+ }
+ }
+
+ // Create or overwrite the pre-commit hook file
+ file, err := os.Create(hookPath)
+ if err != nil {
+ return fmt.Errorf("could not create pre-commit hook file: %v", err)
+ }
+ defer file.Close()
+
+ // Write the script content to the file
+ _, err = file.WriteString(hookScript)
+ if err != nil {
+ return fmt.Errorf("could not write to pre-commit hook file: %v", err)
+ }
+
+ // Make the hook script executable on Unix-like systems
+ if runtime.GOOS != "windows" {
+ err = os.Chmod(hookPath, 0755)
+ if err != nil {
+ return fmt.Errorf("could not make pre-commit hook executable: %v", err)
+ }
+ }
+
+ log.Info().Msg("Pre-commit hook created successfully.")
+ return nil
+}
diff --git a/clustertool/pkg/helper/helper.go b/clustertool/pkg/helper/helper.go
new file mode 100644
index 0000000000000..4e9711a21f1bd
--- /dev/null
+++ b/clustertool/pkg/helper/helper.go
@@ -0,0 +1,117 @@
+package helper
+
+import (
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "slices"
+ "sync"
+
+ "github.com/rs/zerolog/log"
+)
+
+var ExcludedDirs = []string{
+ "templates", ".github", "docs",
+ ".vscode", "tools", ".devcontainer",
+ "testdata",
+}
+
+// WalkMode specifies the mode for walking charts
+type WalkMode int
+
+const (
+ // SyncMode processes charts sequentially
+ SyncMode WalkMode = iota
+ // AsyncMode processes charts concurrently
+ AsyncMode
+)
+
+type ActionFunc func(string, string) error
+
+func getWalkDirFunc(action ActionFunc, bump string, mode WalkMode, wg *sync.WaitGroup) fs.WalkDirFunc {
+ return func(path string, info os.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if info.IsDir() && slices.Contains(ExcludedDirs, info.Name()) {
+ return filepath.SkipDir
+ }
+
+ // Check if the current file is Chart.yaml
+ if info.Name() == "Chart.yaml" {
+ switch mode {
+ case SyncMode:
+ // Process charts sequentially
+ if err := action(path, bump); err != nil {
+ log.Fatal().Err(err).Msgf("Error executing action on Chart.yaml at [%s]", path)
+ }
+ case AsyncMode:
+ // Process charts concurrently
+ wg.Add(1)
+ go func(path string) {
+ defer wg.Done()
+ if err := action(path, bump); err != nil {
+ log.Fatal().Err(err).Msgf("Error executing action on Chart.yaml at [%s]", path)
+ }
+ }(path)
+ default:
+ return fmt.Errorf("invalid mode: %d", mode)
+ }
+
+ // Stop processing the current directory after finding Chart.yaml
+ return filepath.SkipDir
+ }
+
+ return nil
+ }
+}
+
+// TODO: Replace with WalkCharts2, first we have to refactor the "action" function
+// Should be a valid fs.WalkDirFunc, if we need to add state to that function,
+// we can use a closure to pass the state.
+
+// UpdateChartFiles traverses Chart.yaml files based on the provided paths and executes the specified function.
+// If paths is empty, process all charts.
+// The mode parameter toggles between synchronous and asynchronous modes.
+func WalkCharts(paths []string, action func(string, string) error, bump string, mode WalkMode) error {
+ var wg sync.WaitGroup
+
+ if len(paths) == 0 {
+ // If paths is empty, default to processing all charts in the charts directory
+ paths = []string{"./charts"}
+ }
+
+ for _, rootDir := range paths {
+
+ err := filepath.WalkDir(rootDir, getWalkDirFunc(action, bump, mode, &wg))
+
+ if err != nil {
+ return fmt.Errorf("error walking directory %s: %s", rootDir, err)
+ }
+ }
+
+ // Wait for all goroutines to finish in asynchronous mode
+ if mode == AsyncMode {
+ wg.Wait()
+ }
+
+ return nil
+}
+
+func UniqueNonEmptyElementsOf(s []string) []string {
+ unique := make(map[string]bool, len(s))
+ us := make([]string, len(unique))
+ for _, elem := range s {
+ if len(elem) != 0 {
+ if !unique[elem] {
+ us = append(us, elem)
+ unique[elem] = true
+ }
+ }
+ }
+
+ return us
+
+}
diff --git a/clustertool/pkg/helper/marshaller.go b/clustertool/pkg/helper/marshaller.go
new file mode 100644
index 0000000000000..a8ba62fc78965
--- /dev/null
+++ b/clustertool/pkg/helper/marshaller.go
@@ -0,0 +1,14 @@
+package helper
+
+import (
+ "bytes"
+
+ "gopkg.in/yaml.v3"
+)
+
+func MarshalYaml(buf *bytes.Buffer, v interface{}) error {
+ enc := yaml.NewEncoder(buf)
+ defer enc.Close()
+ enc.SetIndent(2)
+ return enc.Encode(v)
+}
diff --git a/clustertool/pkg/helper/netvalidate.go b/clustertool/pkg/helper/netvalidate.go
new file mode 100644
index 0000000000000..3e481009f1abf
--- /dev/null
+++ b/clustertool/pkg/helper/netvalidate.go
@@ -0,0 +1,104 @@
+package helper
+
+import (
+ "net"
+ "os"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+)
+
+// CIDROverlap checks if two CIDR annotations overlap.
+func CIDROverlap(cidr1, cidr2 string) (bool, error) {
+ _, ipnet1, err1 := net.ParseCIDR(cidr1)
+ _, ipnet2, err2 := net.ParseCIDR(cidr2)
+
+ if err1 != nil || err2 != nil {
+ return false, err1
+ }
+
+ return ipnet1.Contains(ipnet2.IP) || ipnet2.Contains(ipnet1.IP), nil
+}
+
+// IPInCIDR checks if an IP fits into a CIDR.
+func IPInCIDR(ipStr, cidr string) (bool, error) {
+ ip := net.ParseIP(ipStr)
+ if ip == nil {
+ return false, nil
+ }
+
+ _, ipnet, err := net.ParseCIDR(cidr)
+ if err != nil {
+ return false, err
+ }
+
+ return ipnet.Contains(ip), nil
+}
+
+// IPInRange checks if an IP fits into an IP range (ip-ip).
+func IPInRange(ipStr, rangeStr string) (bool, error) {
+ ip := net.ParseIP(ipStr)
+ if ip == nil {
+ return false, nil
+ }
+
+ parts := strings.Split(rangeStr, "-")
+ if len(parts) != 2 {
+ return false, nil
+ }
+
+ startIP := net.ParseIP(parts[0])
+ endIP := net.ParseIP(parts[1])
+ if startIP == nil || endIP == nil {
+ return false, nil
+ }
+
+ return bytesCompare(ip, startIP) >= 0 && bytesCompare(ip, endIP) <= 0, nil
+}
+
+// bytesCompare compares two IP addresses.
+// Returns -1 if a < b, 0 if a == b, 1 if a > b.
+func bytesCompare(a, b net.IP) int {
+ for i := range a {
+ if a[i] < b[i] {
+ return -1
+ }
+ if a[i] > b[i] {
+ return 1
+ }
+ }
+ return 0
+}
+
+func ValidateIPorCIDRNotInCIDR(ipOrCIDR, cidr, ipOrCIDRName, cidrName string) {
+ inCIDR, err := IPInCIDR(ipOrCIDR, cidr)
+ if err != nil {
+ log.Info().Msgf("Error validating %s against %s: %v\n", ipOrCIDRName, cidrName, err)
+ os.Exit(1)
+ }
+ if inCIDR {
+ log.Info().Msgf("Cannot proceed, %s cannot be in %s\n", ipOrCIDRName, cidrName)
+ os.Exit(1)
+ }
+}
+
+func ValidateRangeNotInCIDR(rangeStr, cidr, rangeName, cidrName string) {
+ parts := strings.Split(rangeStr, "-")
+ if len(parts) != 2 {
+ log.Info().Msgf("Invalid range format for %s\n", rangeName)
+ os.Exit(1)
+ }
+
+ inCIDRStart, errStart := IPInCIDR(parts[0], cidr)
+ inCIDREnd, errEnd := IPInCIDR(parts[1], cidr)
+
+ if errStart != nil || errEnd != nil {
+ log.Info().Msgf("Error validating %s against %s: %v %v\n", rangeName, cidrName, errStart, errEnd)
+ os.Exit(1)
+ }
+
+ if inCIDRStart || inCIDREnd {
+ log.Info().Msgf("Cannot proceed, %s cannot be in %s\n", rangeName, cidrName)
+ os.Exit(1)
+ }
+}
diff --git a/clustertool/pkg/helper/prompts.go b/clustertool/pkg/helper/prompts.go
new file mode 100644
index 0000000000000..45bddc69d2009
--- /dev/null
+++ b/clustertool/pkg/helper/prompts.go
@@ -0,0 +1,35 @@
+package helper
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+)
+
+// getYesOrNo prompts the user with a question and returns true for yes and false for no
+func GetYesOrNo(prompt string) bool {
+ reader := bufio.NewReader(os.Stdin)
+ for {
+ fmt.Print(prompt)
+ input, err := reader.ReadString('\n')
+ if err != nil {
+ log.Info().Msgf("An error occurred: %v", err)
+ continue
+ }
+
+ input = strings.TrimSpace(input)
+ input = strings.ToLower(input)
+
+ switch input {
+ case "yes", "y":
+ return true
+ case "no", "n":
+ return false
+ default:
+ log.Info().Msg("Invalid input. Please enter yes/no or y/n.")
+ }
+ }
+}
diff --git a/clustertool/pkg/helper/replace.go b/clustertool/pkg/helper/replace.go
new file mode 100644
index 0000000000000..b267acc36e0dd
--- /dev/null
+++ b/clustertool/pkg/helper/replace.go
@@ -0,0 +1,96 @@
+package helper
+
+import (
+ "bufio"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "strings"
+)
+
+// ReplaceInFile reads the content of a file, performs regex replacement, and writes the modified content back to the file.
+func ReplaceInFile(filename string, pattern string, replacement string) error {
+ // Read the file content
+ content, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return fmt.Errorf("failed to read file: %w", err)
+ }
+
+ // Convert content to string
+ original := string(content)
+
+ // Replace all instances matching the regex pattern
+ replaced := strings.ReplaceAll(original, pattern, replacement)
+
+ // Write the modified content back to the file
+ err = ioutil.WriteFile(filename, []byte(replaced), 0644)
+ if err != nil {
+ return fmt.Errorf("failed to write file: %w", err)
+ }
+
+ return nil
+}
+
+// ReplaceContentBetweenLines replaces content between specified lines in the target file with content from the source file.
+func ReplaceContentBetweenLines(targetFilePath string, sourceFilePath string, from string, till string) error {
+ // Read the source file content
+ sourceContent, err := ioutil.ReadFile(sourceFilePath)
+ if err != nil {
+ return fmt.Errorf("failed to read source file: %v", err)
+ }
+
+ // Remove the markers from the source content
+ sourceContentStr := strings.ReplaceAll(string(sourceContent), from, "")
+ sourceContentStr = strings.ReplaceAll(sourceContentStr, till, "")
+
+ // Open the target file
+ targetFile, err := os.Open(targetFilePath)
+ if err != nil {
+ return fmt.Errorf("failed to open target file: %v", err)
+ }
+ defer targetFile.Close()
+
+ // Read the target file line by line
+ var result []string
+ scanner := bufio.NewScanner(targetFile)
+ inReplaceBlock := false
+
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ if strings.Contains(line, from) {
+ inReplaceBlock = true
+ result = append(result, line)
+
+ // Skip the lines until we find the "till" marker
+ for scanner.Scan() {
+ line = scanner.Text()
+ if strings.Contains(line, till) {
+ break
+ }
+ }
+
+ // Append the cleaned source content after skipping the block
+ result = append(result, sourceContentStr)
+ } else if !inReplaceBlock {
+ result = append(result, line)
+ }
+
+ if inReplaceBlock && strings.Contains(line, till) {
+ inReplaceBlock = false
+ result = append(result, line)
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return fmt.Errorf("error reading target file: %v", err)
+ }
+
+ // Write the result back to the target file
+ err = ioutil.WriteFile(targetFilePath, []byte(strings.Join(result, "\n")), 0644)
+ if err != nil {
+ return fmt.Errorf("failed to write to target file: %v", err)
+ }
+
+ return nil
+}
diff --git a/clustertool/pkg/helper/runcmd.go b/clustertool/pkg/helper/runcmd.go
new file mode 100644
index 0000000000000..da0816b89e2e5
--- /dev/null
+++ b/clustertool/pkg/helper/runcmd.go
@@ -0,0 +1,53 @@
+package helper
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "os/exec"
+ "strings"
+)
+
+type filteredWriter struct {
+ writer io.Writer
+ filters []string
+}
+
+func (fw *filteredWriter) Write(p []byte) (n int, err error) {
+ lines := strings.Split(string(p), "\n")
+ var filteredLines []string
+ for _, line := range lines {
+ shouldFilter := false
+ for _, filter := range fw.filters {
+ if strings.Contains(line, filter) {
+ shouldFilter = true
+ break
+ }
+ }
+ if !shouldFilter {
+ filteredLines = append(filteredLines, line)
+ }
+ }
+ return fw.writer.Write([]byte(strings.Join(filteredLines, "\n")))
+}
+
+func RunCommand(commandSlice []string, silent bool) (string, error) {
+ filters := []string{"certificate signed by unknown authority", "bootstrap is not available yet"}
+ cmd := exec.Command(commandSlice[0], commandSlice[1:]...)
+
+ var stdoutBuf, stderrBuf bytes.Buffer
+ if silent {
+ cmd.Stdout = &stdoutBuf
+ cmd.Stderr = &stderrBuf
+ } else {
+ cmd.Stdout = io.MultiWriter(&stdoutBuf, &filteredWriter{writer: os.Stdout, filters: filters})
+ cmd.Stderr = io.MultiWriter(&stderrBuf, &filteredWriter{writer: os.Stderr, filters: filters})
+ }
+
+ err := cmd.Run()
+ if err != nil {
+ return stdoutBuf.String() + stderrBuf.String(), err
+ }
+
+ return stdoutBuf.String() + stderrBuf.String(), nil
+}
diff --git a/clustertool/pkg/helper/talhelperextract.go b/clustertool/pkg/helper/talhelperextract.go
new file mode 100644
index 0000000000000..3378485b49be1
--- /dev/null
+++ b/clustertool/pkg/helper/talhelperextract.go
@@ -0,0 +1,73 @@
+package helper
+
+import (
+ "path"
+ "strings"
+
+ "gopkg.in/yaml.v3"
+)
+
+// Node represents a node structure in the YAML file.
+type Node struct {
+ Hostname string `yaml:"hostname"`
+ IPAddress string `yaml:"ipAddress"`
+}
+
+// Config represents the top-level structure of the YAML file.
+type Config struct {
+ Nodes []Node `yaml:"nodes"`
+}
+
+func ExtractNode(cmd string) string {
+ sliceOut := strings.Split(string(cmd), " ")
+ nodeout := ""
+
+ for _, str := range sliceOut {
+ if strings.Contains(str, "--nodes=") {
+ nodeout = strings.ReplaceAll(str, "--nodes=", "")
+ }
+
+ }
+ return string(nodeout)
+}
+func ExtractSchematic(cmd string) string {
+ sliceOut := strings.Split(string(cmd), " ")
+ schematic := ""
+
+ for _, str := range sliceOut {
+ if strings.Contains(str, "--image=") {
+ cleanstring := strings.ReplaceAll(str, "--image=factory.talos.dev/installer/", "")
+ schemslice := strings.Split(string(cleanstring), ":")
+ schematic = schemslice[0]
+ }
+ }
+ return string(schematic)
+}
+
+// CreateIPHostnameMap reads the YAML configuration file and returns a map of ipAddress to hostname.
+func CreateIPHostnameMap() (map[string]string, error) {
+ // Read the YAML file.
+ data, err := EnvSubst(path.Join(ClusterPath, "/talos/talconfig.yaml"), TalEnv)
+
+ if err != nil {
+ return nil, err
+ }
+
+ // Convert the string data to bytes.
+ dataBytes := []byte(data)
+
+ // Unmarshal the YAML file into a Config struct.
+ var config Config
+ err = yaml.Unmarshal(dataBytes, &config)
+ if err != nil {
+ return nil, err
+ }
+
+ // Create the map of ipAddress to hostname.
+ ipHostnameMap := make(map[string]string)
+ for _, node := range config.Nodes {
+ ipHostnameMap[node.IPAddress] = node.Hostname
+ }
+
+ return ipHostnameMap, nil
+}
diff --git a/clustertool/pkg/helper/time.go b/clustertool/pkg/helper/time.go
new file mode 100644
index 0000000000000..e80a283e6d159
--- /dev/null
+++ b/clustertool/pkg/helper/time.go
@@ -0,0 +1,40 @@
+package helper
+
+import (
+ "os"
+ "time"
+
+ "github.com/rs/zerolog/log"
+
+ "github.com/beevik/ntp"
+)
+
+// checkSystemTime compares the system time with an NTP server time and returns whether it's correct within the given threshold
+func CheckSystemTime() bool {
+ log.Info().Msg("Checking if System Time is correct...")
+ threshold := 5 * time.Second
+ // Get the current system time
+ systemTime := time.Now()
+
+ // Get the time from an NTP server
+ ntpTime, err := ntp.Time("pool.ntp.org")
+ if err != nil {
+ log.Info().Msgf("Failed to get NTP time: %v", err)
+ return true
+ }
+
+ // Calculate the difference between system time and NTP time
+ timeDifference := systemTime.Sub(ntpTime)
+
+ // Check if the time difference is within the acceptable threshold
+ if timeDifference > -threshold && timeDifference < threshold {
+ log.Info().Msg("System Time is correct...")
+ } else {
+ log.Info().Msg("ERROR: System Time incorrect, please correct your systemtime:")
+ log.Info().Msgf("System time: %v", systemTime)
+ log.Info().Msgf("NTP time: %v", ntpTime)
+ log.Info().Msgf("Aborting command!", )
+ os.Exit(1)
+ }
+ return timeDifference > -threshold && timeDifference < threshold
+}
diff --git a/clustertool/pkg/helper/var2file.go b/clustertool/pkg/helper/var2file.go
new file mode 100644
index 0000000000000..94d6abe205dce
--- /dev/null
+++ b/clustertool/pkg/helper/var2file.go
@@ -0,0 +1,25 @@
+package helper
+
+import (
+ "errors"
+ "os"
+
+ "github.com/rs/zerolog/log"
+)
+
+func VarToFile(filename string, content string) error {
+ if _, err := os.Stat(filename); err == nil {
+
+ } else if errors.Is(err, os.ErrNotExist) {
+ // Write the content to the file
+ err := os.WriteFile(filename, []byte(content), 0644)
+ if err != nil {
+ log.Info().Msgf("Error writing to file: %v", err)
+ return err
+ }
+ } else {
+
+ }
+ return nil
+
+}
diff --git a/clustertool/pkg/helper/vars.go b/clustertool/pkg/helper/vars.go
new file mode 100644
index 0000000000000..afe4ac304fbc1
--- /dev/null
+++ b/clustertool/pkg/helper/vars.go
@@ -0,0 +1,34 @@
+package helper
+
+import (
+ "os"
+ "path/filepath"
+)
+
+var (
+ HelmCache = filepath.Join(CacheDir, "tgz_cache")
+ UserCacheDir, _ = os.UserCacheDir()
+ TalEnv = make(map[string]string)
+ ClusterName = "main"
+ KubeCache = filepath.Join(CacheDir, "kubernetes")
+ BaseCache = filepath.Join(CacheDir, "base")
+ RootCache = filepath.Join(CacheDir, "root")
+ PatchCache = filepath.Join(CacheDir, "patches")
+ CacheDir = filepath.Join(UserCacheDir, "clustertool")
+ ClusterPath = filepath.Join("./clusters", ClusterName)
+ ClusterEnvFile = filepath.Join(ClusterPath, "/clusterenv.yaml")
+ TalConfigFile = filepath.Join(ClusterPath, "/talos", "talconfig.yaml")
+ TalosPath = filepath.Join(ClusterPath, "/talos")
+ TalosGenerated = filepath.Join(TalosPath, "/generated")
+ TalosConfigFile = filepath.Join(TalosGenerated, "talosconfig")
+ TalSecretFile = filepath.Join(TalosGenerated, "talsecret.yaml")
+ AllIPs = []string{}
+ ControlPlaneIPs = []string{}
+ WorkerIPs = []string{}
+ KubeFilterStr = []string{
+ ".*would violate PodSecurity.*",
+ }
+
+ IndexCache = "./index_cache"
+ GpgDir = ".cr-gpg" // Adjust the path based on your project structure
+)
diff --git a/clustertool/pkg/helper/walker.go b/clustertool/pkg/helper/walker.go
new file mode 100644
index 0000000000000..85f4a8c1e557b
--- /dev/null
+++ b/clustertool/pkg/helper/walker.go
@@ -0,0 +1,39 @@
+package helper
+
+import (
+ "io/fs"
+ "path/filepath"
+ "sync"
+
+ "github.com/rs/zerolog/log"
+)
+
+func WalkCharts2(paths []string, fn fs.WalkDirFunc, mode WalkMode) error {
+ if len(paths) == 0 {
+ // If paths is empty, default to processing all charts in the charts directory
+ paths = []string{"./charts"}
+ }
+ var wg sync.WaitGroup
+
+ for _, dir := range paths {
+ wg.Add(1)
+ go func(dir string) {
+ defer wg.Done()
+ if err := filepath.WalkDir(dir, fn); err != nil {
+ log.Info().Msgf("Error walking directory %s: %s\n", dir, err)
+ }
+ }(dir)
+
+ if mode == SyncMode {
+ // Wait for THIS goroutine to finish
+ wg.Wait()
+ }
+ }
+
+ if mode == AsyncMode {
+ // Wait for all goroutines to finish
+ wg.Wait()
+ }
+
+ return nil
+}
diff --git a/clustertool/pkg/info/info.go b/clustertool/pkg/info/info.go
new file mode 100644
index 0000000000000..30a3865838077
--- /dev/null
+++ b/clustertool/pkg/info/info.go
@@ -0,0 +1,61 @@
+package info
+
+import (
+ "runtime/debug"
+ "time"
+
+ "github.com/rs/zerolog/log"
+)
+
+type Data struct {
+ GoVersion string
+ GoArch string
+ GoOS string
+ GoC bool
+ GitCommit string
+ GitDate time.Time
+ GitDirty bool
+}
+
+func NewInfo() *Data {
+ info, _ := debug.ReadBuildInfo()
+ data := &Data{
+ GoVersion: info.GoVersion,
+ }
+
+ // Available info: https://github.com/golang/go/blob/master/src/runtime/debug/mod.go#L73
+ for _, kv := range info.Settings {
+ switch kv.Key {
+ case "GOARCH":
+ data.GoArch = kv.Value
+ case "GOOS":
+ data.GoOS = kv.Value
+ case "CGO_ENABLED":
+ data.GoC = kv.Value == "1"
+ case "vcs.revision":
+ data.GitCommit = kv.Value
+ case "vcs.time":
+ data.GitDate, _ = time.Parse(time.RFC3339, kv.Value)
+ case "vcs.modified":
+ data.GitDirty = kv.Value == "true"
+ }
+ }
+
+ return data
+}
+
+func (d *Data) Print() {
+ log.Info().Msgf(`
+clustertool is a tool for managing TrueCharts charts.
+
+Go
+ Version: %s
+ OS: %s
+ Arch: %s
+ CGO: %t
+Git
+ Commit: %s
+ Date: %s
+ Dirty: %t
+`, d.GoVersion, d.GoOS, d.GoArch, d.GoC, d.GitCommit, d.GitDate, d.GitDirty)
+}
diff --git a/clustertool/pkg/initfiles/clusterenv.go b/clustertool/pkg/initfiles/clusterenv.go
new file mode 100644
index 0000000000000..fb47a1044dcd9
--- /dev/null
+++ b/clustertool/pkg/initfiles/clusterenv.go
@@ -0,0 +1,246 @@
+package initfiles
+
+import (
+ "fmt"
+ "net"
+ "os"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+func LoadTalEnv() error {
+ // Check if talenv.yaml file exists
+ if _, err := os.Stat(helper.ClusterPath + "/clusterenv.yaml"); err == nil {
+ // Load environment variables from clusterenv.yaml
+ err := helper.LoadEnvFromFile(helper.ClusterPath+"/clusterenv.yaml", helper.TalEnv)
+ if err != nil {
+ log.Info().Msgf("Error loading environment from clusterenv.yaml: %v\n", err)
+ os.Exit(1)
+ }
+ } else if os.IsNotExist(err) {
+ log.Info().Msg("clusterenv.yaml file not found, skipping environment variable loading.")
+ } else {
+ log.Info().Msgf("Error checking clusterenv.yaml file: %v\n", err)
+ os.Exit(1)
+ }
+ clusterName()
+ PostProcessTalEnv()
+ clusterEnvtoEnv()
+ log.Info().Msgf("ClusterEnv loaded successfully\n", )
+ return nil
+}
+
+func clusterName() {
+ helper.TalEnv["CLUSTERNAME"] = helper.ClusterName
+}
+
+func clusterEnvtoEnv() {
+ // Split IP/NETMASK and normalize IPs
+ for key, value := range helper.TalEnv {
+ os.Setenv(key, value)
+ }
+}
+func PostProcessTalEnv() {
+ // Split IP/NETMASK and normalize IPs
+ for key, value := range helper.TalEnv {
+ ip, netmask, err := splitIPandNetmask(value)
+ if err == nil {
+ // Update TalEnv with IP and NETMASK entries
+ helper.TalEnv[key+"_IP"] = ip
+ helper.TalEnv[key+"_NETMASK"] = netmask
+ helper.TalEnv[key+"_CIDR"] = ip + "/" + netmask
+ }
+ }
+
+ // Validate and normalize specific IP variables
+ ValidateAndNormalizeIPsInTalEnv()
+
+ // Validate and normalize IP/NETMASK variables
+ ValidateAndNormalizeIPNetmaskVarsInTalEnv()
+}
+
+func splitIPandNetmask(ipWithMask string) (string, string, error) {
+ // Check if IP/NETMASK format
+ parts := strings.Split(ipWithMask, "/")
+ if len(parts) == 2 {
+ ip := parts[0]
+ netmask := parts[1]
+ // Validate netmask format (you might want to add more rigorous validation)
+ if _, _, err := net.ParseCIDR(ipWithMask); err != nil {
+ return "", "", fmt.Errorf("invalid IP/NETMASK format: %s", ipWithMask)
+ }
+ return ip, netmask, nil
+ }
+
+ // Assume NETMASK 24 if only IP provided
+ ip := ipWithMask
+ netmask := "24"
+ // Validate IP format (you might want to add more rigorous validation)
+ if net.ParseIP(ip) == nil {
+ return "", "", fmt.Errorf("invalid IP format: %s", ipWithMask)
+ }
+ return ip, netmask, nil
+}
+
+func ValidateAndNormalizeIPsInTalEnv() {
+ ipVariables := []string{"Master1IP"}
+
+ for _, key := range ipVariables {
+ value, exists := helper.TalEnv[key]
+ if !exists {
+ continue // Skip if the variable doesn't exist in TalEnv
+ }
+
+ ip, err := normalizeIP(value)
+ if err != nil {
+ log.Info().Msgf("Error processing %s: %v\n", key, err)
+ continue
+ }
+
+ // Update TalEnv with normalized IP value
+ helper.TalEnv[key] = ip
+ }
+}
+
+func normalizeIP(ipWithMask string) (string, error) {
+ // Check if IP/NETMASK format
+ parts := strings.Split(ipWithMask, "/")
+ if len(parts) == 2 {
+ ip := parts[0]
+ netmask := parts[1]
+ // Validate netmask format (you might want to add more rigorous validation)
+ if _, _, err := net.ParseCIDR(ipWithMask); err != nil {
+ return "", fmt.Errorf("invalid IP/NETMASK format: %s", ipWithMask)
+ }
+ return ip + "/" + netmask, nil
+ }
+
+ // Assume NETMASK 24 if only IP provided
+ ip := ipWithMask
+ // Validate IP format (you might want to add more rigorous validation)
+ if net.ParseIP(ip) == nil {
+ return "", fmt.Errorf("invalid IP format: %s", ipWithMask)
+ }
+ return ip + "/24", nil // Default to /24 subnet mask
+}
+
+func ValidateAndNormalizeIPNetmaskVarsInTalEnv() {
+ netmaskVariables := []string{"PODNET", "SVCNET"}
+
+ for _, key := range netmaskVariables {
+ value, exists := helper.TalEnv[key]
+ if !exists {
+ continue // Skip if the variable doesn't exist in TalEnv
+ }
+
+ ipNetmask, err := normalizeIPNetmask(value)
+ if err != nil {
+ log.Info().Msgf("Error processing %s: %v\n", key, err)
+ continue
+ }
+
+ // Update TalEnv with normalized IP/NETMASK value
+ helper.TalEnv[key] = ipNetmask
+ }
+}
+
+func normalizeIPNetmask(ipNetmask string) (string, error) {
+ // Validate IP/NETMASK format
+ if _, _, err := net.ParseCIDR(ipNetmask); err != nil {
+ return "", fmt.Errorf("invalid IP/NETMASK format: %s", ipNetmask)
+ }
+ return ipNetmask, nil
+}
+
+func CheckEnvVariables() {
+ LoadTalEnv()
+ requiredKeys := []string{
+ "VIP",
+ "MASTER1IP_IP",
+ "MASTER1IP_NETMASK",
+ "GATEWAY",
+ "METALLB_RANGE",
+ "DASHBOARD_IP_IP",
+ "PODNET",
+ "SVCNET",
+ }
+ for _, key := range requiredKeys {
+ if helper.TalEnv[key] == "" {
+ log.Info().Msgf("%s cannot be empty\n", key)
+ os.Exit(1)
+ }
+ }
+
+ // Validate VIP and MASTER1IP format and check subnet compatibility
+ vip := helper.TalEnv["VIP_IP"]
+ master1ip := helper.TalEnv["MASTER1IP_IP"]
+ master1ipCidr := helper.TalEnv["MASTER1IP_CIDR"]
+ gateway := helper.TalEnv["GATEWAY"]
+
+ // Check if MASTER1IP matches GATEWAY or VIP
+ if master1ip == gateway || master1ip == vip {
+ log.Info().Msg("Cannot proceed, MASTER1IP cannot match GATEWAY or VIP")
+ os.Exit(1)
+ }
+
+ // Check if VIP matches any Node IPs
+ if vip == master1ip {
+ log.Info().Msg("Cannot proceed, VIP cannot match any Node IPs")
+ os.Exit(1)
+ }
+
+ // Check ranges against METALLB_RANGE
+ inRange, err := helper.IPInRange(vip, helper.TalEnv["METALLB_RANGE"])
+ if err != nil {
+ log.Info().Msgf("Error checking VIP against METALLB_RANGE: %v\n", err)
+ os.Exit(1)
+ }
+ if inRange {
+ log.Info().Msg("Cannot proceed, VIP cannot be in the METALLB_RANGE")
+ os.Exit(1)
+ }
+
+ inRange, err = helper.IPInRange(master1ip, helper.TalEnv["METALLB_RANGE"])
+ if err != nil {
+ log.Info().Msgf("Error checking MASTER1IP against METALLB_RANGE: %v\n", err)
+ os.Exit(1)
+ }
+ if inRange {
+ log.Info().Msg("Cannot proceed, MASTER1IP cannot be in the METALLB_RANGE")
+ os.Exit(1)
+ }
+
+ inRange, err = helper.IPInRange(gateway, helper.TalEnv["METALLB_RANGE"])
+ if err != nil {
+ log.Info().Msgf("Error checking GATEWAY against METALLB_RANGE: %v\n", err)
+ os.Exit(1)
+ }
+ if inRange {
+ log.Info().Msg("Cannot proceed, GATEWAY cannot be in the METALLB_RANGE")
+ os.Exit(1)
+ }
+
+ // Check DASHBOARD_IP against METALLB_RANGE
+ inRange, err = helper.IPInRange(helper.TalEnv["DASHBOARD_IP"], helper.TalEnv["METALLB_RANGE"])
+ if err != nil {
+ log.Info().Msgf("Error checking DASHBOARD_IP against METALLB_RANGE: %v\n", err)
+ os.Exit(1)
+ }
+ if !inRange {
+ log.Info().Msg("Cannot proceed, DASHBOARD_IP must be in the METALLB_RANGE")
+ os.Exit(1)
+ }
+
+ // Validate other CIDR/IP checks with new netmask support
+ helper.ValidateIPorCIDRNotInCIDR(vip+"/32", helper.TalEnv["PODNET"], "VIP", "PODNET")
+ helper.ValidateIPorCIDRNotInCIDR(master1ipCidr, helper.TalEnv["PODNET"], "MASTER1IP", "PODNET")
+ helper.ValidateIPorCIDRNotInCIDR(gateway+"/32", helper.TalEnv["PODNET"], "GATEWAY", "PODNET")
+ helper.ValidateRangeNotInCIDR(helper.TalEnv["METALLB_RANGE"], helper.TalEnv["PODNET"], "METALLB_RANGE", "PODNET")
+
+ helper.ValidateIPorCIDRNotInCIDR(vip+"/32", helper.TalEnv["SVCNET"], "VIP", "SVCNET")
+ helper.ValidateIPorCIDRNotInCIDR(master1ipCidr, helper.TalEnv["SVCNET"], "MASTER1IP", "SVCNET")
+ helper.ValidateIPorCIDRNotInCIDR(gateway+"/32", helper.TalEnv["SVCNET"], "GATEWAY", "SVCNET")
+ helper.ValidateRangeNotInCIDR(helper.TalEnv["METALLB_RANGE"], helper.TalEnv["SVCNET"], "METALLB_RANGE", "SVCNET")
+}
diff --git a/clustertool/pkg/initfiles/initfiles.go b/clustertool/pkg/initfiles/initfiles.go
new file mode 100644
index 0000000000000..76ba35ed127f8
--- /dev/null
+++ b/clustertool/pkg/initfiles/initfiles.go
@@ -0,0 +1,424 @@
+package initfiles
+
+import (
+ "bufio"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/rs/zerolog/log"
+
+ age "filippo.io/age"
+ talhelperCfg "github.com/budimanjojo/talhelper/v3/pkg/config"
+ "github.com/invopop/jsonschema"
+ "github.com/truecharts/private/clustertool/pkg/fluxhandler"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+func InitFiles() error {
+ ageGen()
+ genRootFiles()
+ genBaseFiles()
+ UpdateRootFiles()
+ UpdateBaseFiles()
+ GenSchema()
+ GenPatches()
+ genKubernetes()
+ GenTalEnvConfigMap()
+ UpdateGitRepo()
+ fluxhandler.CreateGitSecret(helper.TalEnv["GITHUB_REPOSITORY"])
+ fluxhandler.CreateSshPatch()
+ if err := fluxhandler.ProcessDirectory(path.Join(helper.ClusterPath, "kubernetes")); err != nil {
+ log.Error().Msgf("Error: %v", err)
+ }
+ if err := fluxhandler.ProcessDirectory(path.Join(helper.ClusterPath, "kubernetes")); err != nil {
+ log.Error().Msgf("Error: %v", err)
+ } else {
+ log.Info().Msg("Kustomizations processed successfully.")
+ }
+
+ helper.CreateEncrPreCommitHook()
+ log.Info().Msg("Init: Completed Successfully!")
+ return nil
+}
+
+func genKubernetes() error {
+
+ err := helper.CopyDir(helper.KubeCache, helper.ClusterPath+"/kubernetes", false)
+ if err != nil {
+ log.Info().Msgf("Error: %v", err)
+ } else {
+ log.Info().Msgf("Kubernetes files copied successfully.")
+ }
+
+ helper.ReplaceInFile(path.Join(helper.ClusterPath, "/kubernetes/flux-entry.yaml"), "REPLACEWITHCLUSTERNAME", helper.ClusterName)
+ if err != nil {
+ log.Fatal().Err(err).Msgf("Error: %s", err)
+ }
+
+ return nil
+}
+
+func GenTalEnvConfigMap() error {
+
+ log.Info().Msg("Creating TalEnv configmap reference 'clustersettings'.")
+ // Read the content of the talenv.yaml file
+ talenvContent, err := os.ReadFile(helper.ClusterEnvFile)
+ if err != nil {
+ return err
+ }
+
+ // Convert the file content to a string and split it into lines
+ talenvLines := strings.Split(string(talenvContent), "\n")
+
+ // Add indentation to each line
+ for i, line := range talenvLines {
+ talenvLines[i] = " " + line
+ }
+ indentClusterName := " CLUSTERNAME: " + helper.ClusterName
+ talenvLines = append(talenvLines, indentClusterName)
+
+ // Join the indented lines back into a single string
+ indentedTalenvContent := strings.Join(talenvLines, "\n")
+
+ clusterSettings := filepath.Join("flux-system", "flux", "clustersettings.secret.yaml")
+ clusterSettingsDest := filepath.Join(helper.ClusterPath+"/kubernetes", clusterSettings)
+ clusterSettingsSrc := filepath.Join(helper.KubeCache, clusterSettings)
+ os.MkdirAll(filepath.Join(helper.ClusterPath, "/kubernetes", "flux-system", "flux"), os.ModePerm)
+ err = helper.CopyFile(clusterSettingsSrc, clusterSettingsDest, true)
+ log.Info().Msgf("test %v", clusterSettingsDest)
+ helper.ReplaceInFile(clusterSettingsDest, "REPLACEWITHENV", indentedTalenvContent)
+ if err != nil {
+ log.Fatal().Err(err).Msg("Error: %s")
+ }
+ log.Info().Msg("Configmap reference Created.")
+ return nil
+}
+
+func UpdateGitRepo() {
+ if helper.TalEnv["GITHUB_REPOSITORY"] != "" {
+ repoPath := filepath.Join("repositories", "git", "this-repo.yaml")
+ gitrepo := FormatGitURL(helper.TalEnv["GITHUB_REPOSITORY"])
+ helper.ReplaceInFile(repoPath, "ssh://REPLACEWITHGITREPO", gitrepo)
+ }
+}
+
+// FormatGitURL formats the input Git URL according to the specified rules.
+func FormatGitURL(input string) string {
+ // If the input starts with "https://", remove it
+ if strings.HasPrefix(input, "https://") {
+ input = strings.TrimPrefix(input, "https://")
+ }
+
+ // If the input does not start with "ssh://", add "ssh://"
+ if !strings.HasPrefix(input, "ssh://") {
+ input = "ssh://" + input
+ }
+
+ // Replace "github.com/" with "git@github.com:" if present
+ input = strings.Replace(input, "github.com/", "git@github.com:", 1)
+
+ // Compile a regex to match and replace the URL pattern
+ re := regexp.MustCompile(`^ssh://git@github.com:(\w+)/(\w+).git$`)
+ matches := re.FindStringSubmatch(input)
+
+ if len(matches) == 3 {
+ // Reformat the URL
+ return fmt.Sprintf("ssh://git@github.com/%s/%s.git", matches[1], matches[2])
+ }
+
+ // Return the input if it doesn't match the expected pattern
+ return input
+}
+
+func genBaseFiles() error {
+ err := helper.CopyDir(helper.BaseCache, helper.ClusterPath+"", false)
+ if err != nil {
+ log.Info().Msgf("Error: %v", err)
+ } else {
+ log.Info().Msg("Base files copied successfully.")
+ }
+
+ log.Info().Msg("basefiles successfully altered.")
+ return nil
+}
+
+func UpdateBaseFiles() error {
+ // Read filenames in source directory
+ sourceFiles, err := readFilenamesInDir(helper.BaseCache)
+ if err != nil {
+ log.Info().Msgf("Error reading source directory: %v\n", err)
+ return err
+ }
+
+ // Process each file in the target directory
+ for _, filename := range sourceFiles {
+ sourceFilePath := filepath.Join(helper.BaseCache, filename)
+ targetFilePath := filepath.Join(helper.ClusterPath+"", helper.ReplaceDotInFilename(filename))
+ helper.ReplaceContentBetweenLines(targetFilePath, sourceFilePath, "## Do not edit between this and DO NOT REMOVE", "## DO NOT REMOVE: Personal setting go under this line")
+ }
+ log.Info().Msg("basefiles successfully updated.")
+
+ CheckEnvVariables()
+
+ return nil
+
+}
+
+func genRootFiles() error {
+
+ err := helper.CopyDir(helper.RootCache, "./", false)
+ if err != nil {
+ log.Info().Msgf("Error: %v", err)
+ } else {
+ log.Info().Msg("Root files copied successfully.")
+ }
+
+ agePubKey, err := GetPubKey()
+ if err != nil {
+ log.Fatal().Err(err).Msg("error: %v")
+ }
+ log.Info().Msgf("Public Key: %v", agePubKey)
+ helper.ReplaceInFile(".sops.yaml", "REPLACEME", agePubKey)
+ if err != nil {
+ log.Fatal().Err(err).Msg("Error: %s")
+ }
+
+ log.Info().Msg("basefiles successfully altered.")
+ return nil
+}
+
+func UpdateRootFiles() error {
+ // Read filenames in source directory
+ sourceFiles, err := readFilenamesInDir(helper.RootCache)
+ if err != nil {
+ log.Info().Msgf("Error reading source directory: %v\n", err)
+ return err
+ }
+
+ // Process each file in the target directory
+ for _, filename := range sourceFiles {
+ sourceFilePath := filepath.Join(helper.BaseCache, filename)
+ targetFilePath := filepath.Join("./", helper.ReplaceDotInFilename(filename))
+ helper.ReplaceContentBetweenLines(targetFilePath, sourceFilePath, "## Do not edit between this and DO NOT REMOVE", "## DO NOT REMOVE: Personal setting go under this line")
+ }
+ log.Info().Msg("rootfiles successfully updated.")
+
+ agePubKey, err := GetPubKey()
+ if err != nil {
+ log.Fatal().Err(err).Msg("error: %v")
+ }
+
+ helper.ReplaceInFile(".sops.yaml", "REPLACEME", agePubKey)
+ if err != nil {
+ log.Fatal().Err(err).Msg("Error: %s")
+ }
+
+ CheckEnvVariables()
+
+ return nil
+
+}
+
+// Function to read all filenames in a directory
+func readFilenamesInDir(dir string) ([]string, error) {
+ files, err := ioutil.ReadDir(dir)
+ if err != nil {
+ return nil, err
+ }
+
+ var filenames []string
+ for _, file := range files {
+ if !file.IsDir() {
+ filenames = append(filenames, file.Name())
+ }
+ }
+ return filenames, nil
+}
+
+func ResetBootstrapValues() error {
+ LoadTalEnv()
+ err := helper.CopyDirFiltered(helper.KubeCache, helper.ClusterPath+"/kubernetes", true, `^bootstrap-values\.yaml.ct$`)
+ if err != nil {
+ log.Info().Msg("Error:")
+ }
+
+ err2 := helper.EnvSubstRecursive(helper.ClusterPath+"/kubernetes", `^bootstrap-values\.yaml.ct$`, helper.TalEnv)
+ if err2 != nil {
+ log.Info().Msg("Error:")
+ }
+
+ log.Info().Msg("Bootstrap-Values.yaml Files reset successfully.")
+ return nil
+}
+
+func GenPatches() error {
+
+ err := helper.CopyDir(helper.PatchCache, path.Join(helper.ClusterPath, "/talos/patches"), true)
+ if err != nil {
+ log.Info().Msg("Error:")
+ } else {
+ log.Info().Msg("Patch files copied successfully.")
+ }
+
+ ageSecKey, err := GetSecKey()
+ helper.ReplaceInFile(filepath.Join(helper.ClusterPath+"/talos/patches", "sopssecret.yaml"), "REPLACEWITHSOPS", ageSecKey)
+ if err != nil {
+ log.Fatal().Err(err).Msg("Error: %s")
+ }
+
+ // Read the content of the talenv.yaml file
+ talenvContent, err := os.ReadFile(helper.ClusterPath + "/clusterenv.yaml")
+ if err != nil {
+ return err
+ }
+
+ // Convert the file content to a string and split it into lines
+ talenvLines := strings.Split(string(talenvContent), "\n")
+
+ // Add indentation to each line
+ for i, line := range talenvLines {
+ talenvLines[i] = " " + line
+ }
+
+ // Join the indented lines back into a single string
+ indentedTalenvContent := strings.Join(talenvLines, "\n")
+
+ helper.ReplaceInFile(filepath.Join(helper.ClusterPath, "/talos/patches", "sopssecret.yaml"), "REPLACEWITHTALENV", indentedTalenvContent)
+ // log.Info().Msg("test", filepath.Join(helper.ClusterPath, "/talos/patches", "sopssecret.yaml"))
+ if err != nil {
+ log.Fatal().Err(err).Msg("Error: %s")
+ }
+
+ return nil
+}
+
+func ageGen() error {
+ outFlag := "age.agekey"
+
+ if _, err := os.Stat(outFlag); err == nil {
+
+ } else if errors.Is(err, os.ErrNotExist) {
+ out := os.Stdout
+ f, err := os.OpenFile(outFlag, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
+ if err != nil {
+ log.Fatal().Err(err).Msg("failed to open output file %q: %v")
+ }
+ defer func() {
+ if err := f.Close(); err != nil {
+ log.Fatal().Err(err).Msg("failed to close output file %q: %v")
+ }
+ }()
+ out = f
+ if fi, err := out.Stat(); err == nil && fi.Mode().IsRegular() && fi.Mode().Perm()&0004 != 0 {
+ log.Info().Msgf("writing secret key to a world-readable file\n")
+ }
+
+ k, err := age.GenerateX25519Identity()
+ if err != nil {
+ log.Fatal().Err(err).Msg("internal error: %v")
+ }
+
+ fmt.Fprintf(out, "# created: %s\n", time.Now().Format(time.RFC3339))
+ fmt.Fprintf(out, "# public key: %s\n", k.Recipient())
+ fmt.Fprintf(out, "%s\n", k)
+
+ } else {
+
+ }
+
+ return nil
+}
+
+func GetPubKey() (string, error) {
+ // Open the file
+ filename := "age.agekey"
+ file, err := os.Open(filename)
+ if err != nil {
+ return "", fmt.Errorf("failed to open file: %v", err)
+ }
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+ var publicKey string
+
+ // Read the file line by line
+ for scanner.Scan() {
+ line := scanner.Text()
+ // Find the line with the public key
+ if strings.HasPrefix(line, "# public key:") {
+ parts := strings.Split(line, ": ")
+ if len(parts) == 2 {
+ publicKey = parts[1]
+ }
+ break
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return "", fmt.Errorf("failed to scan file: %v", err)
+ }
+
+ if publicKey == "" {
+ return "", fmt.Errorf("public key not found")
+ }
+
+ return publicKey, nil
+}
+
+// getSecretKeyFromFile reads the specified file and returns the secret key found within it.
+func GetSecKey() (string, error) {
+ // Open the file
+ filename := "age.agekey"
+ file, err := os.Open(filename)
+ if err != nil {
+ return "", fmt.Errorf("failed to open file: %v", err)
+ }
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+ var secretKey string
+
+ // Read the file line by line
+ for scanner.Scan() {
+ line := scanner.Text()
+ // Find the line that contains the secret key prefix
+ if strings.HasPrefix(line, "AGE-SECRET-KEY-") {
+ secretKey = line
+ break
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return "", fmt.Errorf("failed to scan file: %v", err)
+ }
+
+ if secretKey == "" {
+ return "", fmt.Errorf("secret key not found")
+ }
+
+ return secretKey, nil
+}
+
+func GenSchema() error {
+ cfg := talhelperCfg.TalhelperConfig{}
+ r := new(jsonschema.Reflector)
+ r.FieldNameTag = "yaml"
+ r.RequiredFromJSONSchemaTags = true
+ os.MkdirAll(helper.ClusterPath+"/talos", os.ModePerm)
+ var genschemaFile = path.Join(helper.ClusterPath, "/talos/talconfig.json")
+
+ schema := r.Reflect(&cfg)
+ data, _ := json.MarshalIndent(schema, "", " ")
+ if err := os.WriteFile(genschemaFile, data, os.FileMode(0o644)); err != nil {
+ log.Fatal().Err(err).Msg("failed to write file to %s: %v")
+ }
+ return nil
+}
diff --git a/clustertool/pkg/kubectlcmds/apply.go b/clustertool/pkg/kubectlcmds/apply.go
new file mode 100644
index 0000000000000..1267e92dc6b6d
--- /dev/null
+++ b/clustertool/pkg/kubectlcmds/apply.go
@@ -0,0 +1,223 @@
+package kubectlcmds
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+
+ "github.com/go-logr/logr"
+ "github.com/go-logr/zapr"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/client-go/tools/clientcmd"
+ "k8s.io/client-go/util/homedir"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/kustomize/api/krusty"
+ "sigs.k8s.io/kustomize/kyaml/filesys"
+ "sigs.k8s.io/kustomize/kyaml/kio"
+ "sigs.k8s.io/yaml"
+)
+
+// getKubeClient initializes and returns a controller-runtime client.Client
+func getKubeClient() (client.Client, error) {
+ // Load kubeconfig from the default location
+ kubeconfig := filepath.Join(homedir.HomeDir(), ".kube", "config")
+ config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load kubeconfig: %v", err)
+ }
+
+ // Create a controller-runtime client
+ c, err := client.New(config, client.Options{})
+ if err != nil {
+ return nil, fmt.Errorf("failed to create Kubernetes client: %v", err)
+ }
+
+ return c, nil
+}
+
+// setupLogger initializes a logger that writes to a buffer and returns both
+func setupLogger() (logr.Logger, *bytes.Buffer, error) {
+ // Create a buffer to capture logs
+ var buf bytes.Buffer
+
+ // Create a WriteSyncer to write to the buffer
+ writeSyncer := zapcore.AddSync(&buf)
+
+ // Configure zap to use console encoder for readability
+ encoderCfg := zap.NewProductionEncoderConfig()
+ encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder
+ encoder := zapcore.NewConsoleEncoder(encoderCfg)
+
+ // Set log level to Info
+ level := zapcore.InfoLevel
+
+ // Create zap core
+ core := zapcore.NewCore(encoder, writeSyncer, level)
+
+ // Create zap logger
+ zapLogger := zap.New(core)
+
+ // Wrap zap logger with zapr to get a logr.Logger interface
+ log := zapr.NewLogger(zapLogger)
+
+ return log, &buf, nil
+}
+
+// applyYAML applies the given YAML data to the Kubernetes cluster using the provided client and logger
+func applyYAML(k8sClient client.Client, yamlData []byte, log logr.Logger) error {
+ // Parse the YAML into KIO nodes
+ reader := kio.ByteReader{
+ Reader: bytes.NewReader(yamlData),
+ }
+ nodes, err := reader.Read()
+ if err != nil {
+ return fmt.Errorf("failed to parse YAML: %v", err)
+ }
+
+ // Apply each node to the cluster
+ for _, node := range nodes {
+ obj := &unstructured.Unstructured{}
+ if err := yaml.Unmarshal([]byte(node.MustString()), obj); err != nil {
+ return fmt.Errorf("failed to unmarshal node: %v", err)
+ }
+ if err := k8sClient.Patch(context.TODO(), obj, client.Apply, client.FieldOwner("kustomize-controller")); err != nil {
+ return fmt.Errorf("failed to apply object: %v", err)
+ }
+ log.Info("Successfully applied object", "object", obj.GetName(), "kind", obj.GetKind(), "namespace", obj.GetNamespace())
+ }
+
+ return nil
+}
+
+// filterLogOutput filters the log data by removing strings that match any of the provided regex patterns
+func filterLogOutput(logData string) (string, error) {
+ filteredLog := logData
+ for _, pattern := range helper.KubeFilterStr {
+ re, err := regexp.Compile(pattern)
+ if err != nil {
+ return "", fmt.Errorf("invalid regex pattern '%s': %v", pattern, err)
+ }
+ filteredLog = re.ReplaceAllString(filteredLog, "")
+ }
+ return filteredLog, nil
+}
+
+// KubectlApply applies a YAML file to the Kubernetes cluster and filters the logs
+func KubectlApply(ctx context.Context, filePath string) error {
+ // Check if the file exists
+ if _, err := os.Stat(filePath); os.IsNotExist(err) {
+ return fmt.Errorf("file does not exist: %s", filePath)
+ }
+
+ // Read the YAML file
+ yamlData, err := ioutil.ReadFile(filePath)
+ if err != nil {
+ return fmt.Errorf("failed to read YAML file: %v", err)
+ }
+
+ // Initialize logger and buffer
+ log, buf, err := setupLogger()
+ if err != nil {
+ return fmt.Errorf("failed to set up logger: %v", err)
+ }
+
+ // Initialize Kubernetes client
+ k8sClient, err := getKubeClient()
+ if err != nil {
+ return err
+ }
+
+ // Apply the YAML to the cluster
+ if err := applyYAML(k8sClient, yamlData, log); err != nil {
+ return fmt.Errorf("failed to apply YAML: %v", err)
+ }
+
+ // Get log output from buffer
+ logOutput := buf.String()
+
+ // Filter the logs
+ filteredLog, err := filterLogOutput(logOutput)
+ if err != nil {
+ return fmt.Errorf("failed to filter logs: %v", err)
+ }
+
+ // Output filtered logs
+ fmt.Println(filteredLog)
+
+ return nil
+}
+
+// KubectlApplyKustomize applies a kustomize directory or file to the Kubernetes cluster and filters the logs
+func KubectlApplyKustomize(ctx context.Context, filePath string) error {
+ // Check if the path exists
+ if _, err := os.Stat(filePath); os.IsNotExist(err) {
+ return fmt.Errorf("path does not exist: %s", filePath)
+ }
+
+ // Determine if the path is a directory or a file
+ fileInfo, err := os.Stat(filePath)
+ if err != nil {
+ return fmt.Errorf("failed to stat path: %v", err)
+ }
+
+ var kustomizePath string
+ if fileInfo.IsDir() {
+ // If it's a directory, use it as the kustomize path
+ kustomizePath = filePath
+ } else {
+ // If it's a file, use its directory as the kustomize path
+ kustomizePath = filepath.Dir(filePath)
+ }
+
+ // Process kustomize to get the YAML output
+ fSys := filesys.MakeFsOnDisk()
+ k := krusty.MakeKustomizer(krusty.MakeDefaultOptions())
+ resMap, err := k.Run(fSys, kustomizePath)
+ if err != nil {
+ return fmt.Errorf("failed to run kustomize: %v", err)
+ }
+
+ // Convert ResMap to YAML
+ output, err := resMap.AsYaml()
+ if err != nil {
+ return fmt.Errorf("failed to convert ResMap to YAML: %v", err)
+ }
+
+ // Initialize logger and buffer
+ log, buf, err := setupLogger()
+ if err != nil {
+ return fmt.Errorf("failed to set up logger: %v", err)
+ }
+
+ // Initialize Kubernetes client
+ k8sClient, err := getKubeClient()
+ if err != nil {
+ return err
+ }
+
+ // Apply the YAML to the cluster
+ if err := applyYAML(k8sClient, output, log); err != nil {
+ return fmt.Errorf("failed to apply YAML: %v", err)
+ }
+
+ // Get log output from buffer
+ logOutput := buf.String()
+
+ // Filter the logs
+ filteredLog, err := filterLogOutput(logOutput)
+ if err != nil {
+ return fmt.Errorf("failed to filter logs: %v", err)
+ }
+
+ // Output filtered logs
+ fmt.Println(filteredLog)
+
+ return nil
+}
diff --git a/clustertool/pkg/kubectlcmds/approvecerts.go b/clustertool/pkg/kubectlcmds/approvecerts.go
new file mode 100644
index 0000000000000..1c770449298b5
--- /dev/null
+++ b/clustertool/pkg/kubectlcmds/approvecerts.go
@@ -0,0 +1,85 @@
+package kubectlcmds
+
+import (
+ "context"
+ "time"
+
+ "github.com/rs/zerolog/log"
+ certificatesv1 "k8s.io/api/certificates/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd"
+)
+
+// getClientset creates a Kubernetes clientset from the in-cluster config or kubeconfig file
+func GetClientset() (*kubernetes.Clientset, error) {
+ // use the current context in kubeconfig
+ config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
+ if err != nil {
+ config, err = rest.InClusterConfig()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // create the clientset
+ clientset, err := kubernetes.NewForConfig(config)
+ if err != nil {
+ return nil, err
+ }
+
+ return clientset, nil
+}
+
+// Example function to approve pending CSRs
+func ApprovePendingCertificates(clientset *kubernetes.Clientset, stopCh <-chan struct{}) {
+ log.Info().Msg("Waiting to approve certificates...")
+
+ for {
+ select {
+ case <-stopCh:
+ log.Info().Msg("Stopping certificate approval...")
+ return
+ default:
+ // Get the list of pending CSRs
+ csrList, err := clientset.CertificatesV1().CertificateSigningRequests().List(context.TODO(), metav1.ListOptions{})
+ if err != nil {
+ log.Info().Msgf("Error getting CSRs: %v", err)
+ time.Sleep(5 * time.Second)
+ continue
+ }
+
+ // Approve pending CSRs
+ for _, csr := range csrList.Items {
+ if csr.Status.Conditions == nil || len(csr.Status.Conditions) == 0 {
+ // Create a copy of the CSR object
+ csrCopy := csr.DeepCopy()
+
+ // Prepare approval conditions
+ conditions := []certificatesv1.CertificateSigningRequestCondition{
+ {
+ Type: certificatesv1.CertificateApproved,
+ Reason: "AutoApproved",
+ Message: "This CSR was approved automatically by controller.",
+ LastUpdateTime: metav1.Now(),
+ Status: "True",
+ },
+ }
+ csrCopy.Status.Conditions = conditions
+
+ // Update approval for the CSR using the copied object
+ _, err := clientset.CertificatesV1().CertificateSigningRequests().UpdateApproval(context.TODO(), csr.Name, csrCopy, metav1.UpdateOptions{})
+ if err != nil {
+ log.Info().Msgf("Error approving CSR %s: %v\n", csr.Name, err)
+ } else {
+ log.Info().Msgf("Approved CSR", csr.Name)
+ }
+ }
+ }
+
+ // Sleep for 5 seconds before checking again
+ time.Sleep(5 * time.Second)
+ }
+ }
+}
diff --git a/clustertool/pkg/kubectlcmds/checkstatus.go b/clustertool/pkg/kubectlcmds/checkstatus.go
new file mode 100644
index 0000000000000..007d59034398a
--- /dev/null
+++ b/clustertool/pkg/kubectlcmds/checkstatus.go
@@ -0,0 +1,77 @@
+package kubectlcmds
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/rs/zerolog/log"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/tools/clientcmd"
+)
+
+func CheckStatus(requiredPods []string, excludePod []string, timeout time.Duration) error {
+ // Load kubeconfig from the default location
+ kubeconfig := clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename()
+ config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
+ if err != nil {
+ return fmt.Errorf("error loading kubeconfig: %w", err)
+ }
+
+ // Create clientset
+ clientset, err := kubernetes.NewForConfig(config)
+ if err != nil {
+ return fmt.Errorf("error creating clientset: %w", err)
+ }
+
+ // Maximum duration to wait (15 minutes)
+ maxDuration := timeout * time.Minute
+ endTime := time.Now().Add(maxDuration)
+
+ for time.Now().Before(endTime) {
+ // Get pods in all namespaces
+ pods, err := clientset.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
+ if err != nil {
+ // return fmt.Errorf("error listing pods: %w", err)
+ }
+
+ // Check if the required pods are both present and running
+ requiredPodsMap := make(map[string]bool)
+ for _, pod := range requiredPods {
+ requiredPodsMap[pod] = false
+ }
+
+ for _, pod := range pods.Items {
+ for _, requiredPod := range requiredPods {
+ for _, excludePod := range excludePod {
+ if strings.Contains(pod.Name, excludePod) {
+ requiredPodsMap[requiredPod] = true
+ }
+ }
+ if strings.Contains(pod.Name, requiredPod) && pod.Status.Phase == "Running" {
+ requiredPodsMap[requiredPod] = true
+ }
+ }
+ }
+
+ allRunning := true
+ for _, isRunning := range requiredPodsMap {
+ if !isRunning {
+ allRunning = false
+ break
+ }
+ }
+
+ if allRunning {
+ log.Info().Msg("All required pods are running")
+ return nil
+ }
+
+ // Wait for 5 seconds before checking again
+ time.Sleep(5 * time.Second)
+ }
+
+ return fmt.Errorf("timeout: not all required pods are running after 15 minutes")
+}
diff --git a/clustertool/pkg/nodestatus/health.go b/clustertool/pkg/nodestatus/health.go
new file mode 100644
index 0000000000000..3a6ebf8c810c9
--- /dev/null
+++ b/clustertool/pkg/nodestatus/health.go
@@ -0,0 +1,98 @@
+package nodestatus
+
+import (
+ "errors"
+ "strings"
+ "time"
+
+ "github.com/rs/zerolog/log"
+)
+
+func CheckHealth(node string, status string, silent bool) error {
+ out, err := CheckStatus(node)
+ if err != nil {
+ errstring := "healthcheck failed. status: " + string(out) + " error: " + err.Error()
+ if !silent {
+ log.Info().Msgf("Healthcheck: check on node : failed %v", node)
+ log.Info().Msgf("failed with error: %s", errstring)
+ }
+ return errors.New(errstring)
+ }
+ out = strings.TrimSpace(out)
+ if !silent {
+ log.Info().Msgf("Healthcheck: node currently reporting status: %v %v", node, out)
+ }
+ if status != "" && strings.Contains(string(out), status) {
+ if !silent {
+ response := "Healthcheck: detected node " + node + "in mode " + status + " , continuing..."
+ log.Info().Msg(response)
+ }
+ } else if status == "" && strings.Contains(string(out), "maintenance") {
+ response := "Healthcheck: WARN detected node " + node + "in mode " + "maintenance" + ".\nLikely a new node, so trying commands anyway. Continuing..."
+ log.Info().Msg(response)
+ } else if status == "" && strings.Contains(string(out), "running") {
+ _, err = CheckReadyStatus(node)
+ if err != nil {
+ errstring := "healthcheck failed. status: " + string(out) + " error: " + err.Error()
+ return errors.New(errstring)
+ }
+ } else {
+ if !silent {
+ log.Info().Msgf("Healthcheck: check on node : failed %v", node)
+ }
+ return errors.New("healthcheck failed")
+ }
+ return nil
+}
+
+func WaitForHealth(node string, status []string) (string, error) {
+ statusmsg := ""
+ if len(status) > 0 {
+ for _, check := range status {
+ statusmsg = statusmsg + ", " + check
+ }
+ } else {
+ statusmsg = "running"
+ status = []string{""}
+ }
+
+ // Corrected log with format specifiers
+ log.Info().Msgf("Healthcheck: Waiting for Node %s to reach status: %s", node, statusmsg)
+
+ // Duration constants
+ checkInterval := 10 * time.Second
+ maxDuration := 15 * time.Minute
+
+ // Create a ticker to run CheckHealth every 10 seconds
+ ticker := time.NewTicker(checkInterval)
+ defer ticker.Stop()
+
+ // Create a timer to stop the process after 15 minutes
+ timer := time.NewTimer(maxDuration)
+ defer timer.Stop()
+
+ // Initial health check before starting the ticker
+ for _, check := range status {
+ err := CheckHealth(node, check, true)
+ if err == nil {
+ return check, nil
+ }
+ }
+
+ // Loop to run CheckHealth every 10 seconds for a maximum of 15 minutes
+ for {
+ select {
+ case <-ticker.C:
+ for _, check := range status {
+ err := CheckHealth(node, check, true)
+ if err == nil {
+ return check, nil
+ }
+ }
+
+ case <-timer.C:
+ log.Info().Msg("Max duration reached. Stopping health checks.")
+ return "ERROR", errors.New("timeout waiting for Node to boot")
+ }
+ }
+}
diff --git a/clustertool/pkg/nodestatus/status.go b/clustertool/pkg/nodestatus/status.go
new file mode 100644
index 0000000000000..7f348fc20259c
--- /dev/null
+++ b/clustertool/pkg/nodestatus/status.go
@@ -0,0 +1,75 @@
+package nodestatus
+
+import (
+ "errors"
+ "path"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+ "github.com/truecharts/private/clustertool/embed"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+func baseStatusCMD(node string) []string {
+ argsslice := [...]string{embed.GetTalosExec(), "--talosconfig=" + path.Join(helper.ClusterPath, "/talos/generated/talosconfig"), "-n", node, "-e", node, "get", "machinestatus"}
+ return argsslice[:]
+}
+
+func CheckNeedBootstrap(node string) (bool, error) {
+ argsslice := append(baseStatusCMD(node), "-o", "jsonpath={.spec.stage}")
+ out, err := helper.RunCommand(argsslice, true)
+ if err != nil {
+ if strings.Contains(string(out), "certificate signed by unknown authority") {
+ argsslice := append(baseStatusCMD(node), "-o", "jsonpath={.spec.stage}", "--insecure")
+ out2, err2 := helper.RunCommand(argsslice, true)
+ if err2 != nil {
+ errstring := "status: " + string(out) + " error: " + err2.Error()
+ return false, errors.New(errstring)
+ }
+ if string(out2) != "" && strings.Contains(string(out2), "maintenance") {
+ return true, nil
+ }
+ } else {
+ errstring := "status: " + string(out) + " error: " + err.Error()
+ return false, errors.New(errstring)
+ }
+ }
+ errstring := "status: " + string(out) + " error: " + err.Error()
+ return false, errors.New(errstring)
+}
+
+func CheckStatus(node string) (string, error) {
+ argsslice := append(baseStatusCMD(node), "-o", "jsonpath={.spec.stage}")
+ out, err := helper.RunCommand(argsslice, true)
+ if err != nil {
+ if strings.Contains(string(out), "certificate signed by unknown authority") {
+ argsslice := append(baseStatusCMD(node), "-o", "jsonpath={.spec.stage}", "--insecure")
+ out2, err2 := helper.RunCommand(argsslice, true)
+ if err2 != nil {
+ errstring := "status: " + string(out) + " error: " + err2.Error()
+ return "ERROR", errors.New(errstring)
+ }
+ return string(out2), nil
+ } else {
+ errstring := "status: " + string(out) + " error: " + err.Error()
+ return "ERROR", errors.New(errstring)
+ }
+ }
+ return string(out), nil
+}
+
+func CheckReadyStatus(node string) (string, error) {
+ argsslice := append(baseStatusCMD(node), "-o", "jsonpath={.spec.status.ready}")
+ out, err := helper.RunCommand(argsslice, true)
+
+ if err != nil {
+ errstring := "status: " + string(out) + " error: " + err.Error()
+ return "ERROR", errors.New(errstring)
+ }
+ if strings.Contains(string(out), "true") {
+ log.Info().Msg("node ready...")
+ } else {
+ println("Node not Ready...")
+ }
+ return string(out), nil
+}
diff --git a/clustertool/pkg/scale/exportapps.go b/clustertool/pkg/scale/exportapps.go
new file mode 100644
index 0000000000000..c78326749b152
--- /dev/null
+++ b/clustertool/pkg/scale/exportapps.go
@@ -0,0 +1,59 @@
+package scale
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+)
+
+func ExportApps() {
+ // Execute the command and capture its output
+ cmd := exec.Command("midclt", "call", "chart.release.query")
+ var out bytes.Buffer
+ cmd.Stdout = &out
+ if err := cmd.Run(); err != nil {
+ fmt.Fprintf(os.Stderr, "Error executing command: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Parse the JSON output
+ var releases []map[string]interface{}
+ if err := json.Unmarshal(out.Bytes(), &releases); err != nil {
+ fmt.Fprintf(os.Stderr, "Error parsing JSON: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Ensure the directory exists
+ outputDir := "./truenas_exports"
+ if err := os.MkdirAll(outputDir, 0755); err != nil {
+ fmt.Fprintf(os.Stderr, "Error creating directory: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Save each release to a separate file
+ for _, release := range releases {
+ // Extract the release name
+ name, ok := release["name"].(string)
+ if !ok {
+ fmt.Fprintf(os.Stderr, "Error: release does not have a name field or it is not a string\n")
+ continue
+ }
+
+ // Marshal the release data to JSON
+ data, err := json.MarshalIndent(release, "", " ")
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error marshaling JSON: %v\n", err)
+ continue
+ }
+
+ // Create the filename using the release name
+ filename := filepath.Join(outputDir, fmt.Sprintf("%s.json", name))
+ if err := ioutil.WriteFile(filename, data, 0644); err != nil {
+ fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
+ }
+ }
+}
diff --git a/clustertool/pkg/scale/scale2flux.go b/clustertool/pkg/scale/scale2flux.go
new file mode 100644
index 0000000000000..dca6c9c5c6d9b
--- /dev/null
+++ b/clustertool/pkg/scale/scale2flux.go
@@ -0,0 +1,412 @@
+package scale
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+ "github.com/truecharts/private/clustertool/pkg/fluxhandler"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+ "gopkg.in/yaml.v3"
+)
+
+type RadarrConfig struct {
+ Name string `json:"name"`
+ Namespace string `json:"namespace"`
+ Info map[string]interface{} `json:"info"`
+ Config map[string]interface{} `json:"config"`
+ ChartMetadata map[string]interface{} `json:"chart_metadata"`
+}
+
+// removeKeysStartingWithIX recursively removes keys starting with "ix" or "IX" from a map
+func removeKeysStartingWithIX(data map[string]interface{}) {
+ for key := range data {
+ lowerKey := strings.ToLower(key)
+ if strings.HasPrefix(lowerKey, "ix") {
+ delete(data, key)
+ } else {
+ // If the value is a nested map, recursively remove keys
+ if nestedMap, ok := data[key].(map[string]interface{}); ok {
+ removeKeysStartingWithIX(nestedMap)
+ }
+ }
+ }
+}
+
+// mergeLists merges "somethingList" with "something" based on the "name" key
+func mergeLists(config map[string]interface{}) {
+ for key, value := range config {
+ if strings.HasSuffix(key, "List") {
+ baseKey := strings.TrimSuffix(key, "List")
+ if baseMap, ok := config[baseKey].(map[string]interface{}); ok {
+ if list, ok := value.([]interface{}); ok {
+ for _, item := range list {
+ if itemMap, ok := item.(map[string]interface{}); ok {
+ if name, exists := itemMap["name"].(string); exists {
+ baseMap[name] = itemMap
+ }
+ }
+ }
+ }
+ } else {
+ newMap := make(map[string]interface{})
+ if list, ok := value.([]interface{}); ok {
+ for _, item := range list {
+ if itemMap, ok := item.(map[string]interface{}); ok {
+ if name, exists := itemMap["name"].(string); exists {
+ newMap[name] = itemMap
+ }
+ }
+ }
+ config[baseKey] = newMap
+ }
+ }
+ delete(config, key)
+ } else {
+ // If the value is a nested map, recursively merge lists
+ if nestedMap, ok := value.(map[string]interface{}); ok {
+ mergeLists(nestedMap)
+ }
+ }
+ }
+}
+
+// removeSpecificKeys removes specific keys from the root-level of config/values
+func removeSpecificKeys(config map[string]interface{}) {
+ specificKeys := []string{
+ "donateNag",
+ "docs",
+ "image",
+ "expertPodOpts",
+ "advanced",
+ "portal",
+ "requests",
+ "global",
+ }
+
+ for _, key := range specificKeys {
+ deleteKeyRecursive(config, key)
+ }
+}
+
+// deleteKeyRecursive recursively deletes a key from config and its nested maps
+func deleteKeyRecursive(config interface{}, key string) {
+ switch c := config.(type) {
+ case map[string]interface{}:
+ if _, ok := c[key]; ok {
+ delete(c, key)
+ } else {
+ for _, v := range c {
+ deleteKeyRecursive(v, key)
+ }
+ }
+ case []interface{}:
+ for _, v := range c {
+ deleteKeyRecursive(v, key)
+ }
+ }
+}
+
+// removeEmptyLists recursively removes empty lists from the config
+func removeEmptyLists(config map[string]interface{}) {
+ for key, value := range config {
+ if list, ok := value.([]interface{}); ok && len(list) == 0 {
+ delete(config, key)
+ } else if nestedMap, ok := value.(map[string]interface{}); ok {
+ removeEmptyLists(nestedMap)
+ }
+ }
+}
+
+// removeEmptyDicts recursively removes empty dictionaries from the config
+func removeEmptyDicts(config map[string]interface{}) {
+ for key, value := range config {
+ if subMap, ok := value.(map[string]interface{}); ok {
+ if len(subMap) == 0 {
+ delete(config, key)
+ } else {
+ removeEmptyDicts(subMap)
+ }
+ }
+ }
+}
+
+// removeIXPrefix removes the "ix-" prefix from the namespace
+func removeIXPrefix(namespace string) string {
+ if strings.HasPrefix(namespace, "ix-") {
+ return namespace[3:]
+ }
+ return namespace
+}
+
+// removePortalOpenDisabled removes portal.open.enabled: false if nothing else is defined under portal.open
+func removePortalOpenDisabled(config map[string]interface{}) {
+ removeKeyIfValueRecursive(config, "portal.open.enabled", false)
+}
+
+// removeAdvancedSettings removes instances of advanced: false or advanced: true
+func removeAdvancedSettings(config map[string]interface{}) {
+ removeKeyIfValueRecursive(config, "advanced", false)
+ removeKeyIfValueRecursive(config, "advanced", true)
+}
+
+// removeKeyIfValueRecursive recursively deletes a key in config based on a dot-separated key path and value
+func removeKeyIfValueRecursive(config interface{}, keyPath string, value interface{}) {
+ switch c := config.(type) {
+ case map[string]interface{}:
+ keys := strings.Split(keyPath, ".")
+ if len(keys) == 1 {
+ if c[keys[0]] == value {
+ delete(c, keys[0])
+ }
+ } else {
+ if v, ok := c[keys[0]]; ok {
+ removeKeyIfValueRecursive(v, strings.Join(keys[1:], "."), value)
+ }
+ }
+ case []interface{}:
+ for _, v := range c {
+ removeKeyIfValueRecursive(v, keyPath, value)
+ }
+ }
+}
+
+// removeSpecificKeyValues removes specific key-value pairs from the config
+func removeSpecificKeyValues(config map[string]interface{}) {
+ specificKeyValues := map[string]interface{}{
+ "amd.com/gpu": float64(0), // Use float64(0) to match JSON unmarshal output
+ "cpu": "4000m",
+ "gpu.intel.com/i915": float64(0), // Use float64(0) to match JSON unmarshal output
+ "memory": "8Gi",
+ "nvidia.com/gpu": float64(0), // Use float64(0) to match JSON unmarshal output
+ "PUID": float64(568), // Use float64(568) to match JSON unmarshal output
+ "UMASK": "\"0022\"",
+ "fsGroup": float64(568), // Use float64(568) to match JSON unmarshal output
+ "fsGroupChangePolicy": "Always",
+ "readOnly": false,
+ "size": "256Gi",
+ "mode": "disabled",
+ "type": "pvc",
+ "runAsNonRoot": false,
+ }
+
+ removeSpecificKeyValuesRecursive(config, specificKeyValues)
+}
+
+// removeSpecificKeyValuesRecursive recursively deletes specific key-value pairs from the config
+func removeSpecificKeyValuesRecursive(config interface{}, specificKeyValues map[string]interface{}) {
+ switch c := config.(type) {
+ case map[string]interface{}:
+ for key, value := range c {
+ if expectedValue, ok := specificKeyValues[key]; ok {
+ switch expectedValue := expectedValue.(type) {
+ case float64:
+ if actualValue, ok := value.(float64); ok && actualValue == expectedValue {
+ delete(c, key)
+ }
+ case string:
+ if actualValue, ok := value.(string); ok && actualValue == expectedValue {
+ delete(c, key)
+ }
+ // Add more cases for other types if needed
+ }
+ } else {
+ removeSpecificKeyValuesRecursive(value, specificKeyValues)
+ }
+ }
+ case []interface{}:
+ for _, v := range c {
+ removeSpecificKeyValuesRecursive(v, specificKeyValues)
+ }
+ }
+}
+
+// deleteNestedKeyRecursive recursively deletes a nested key in config based on a dot-separated key path
+func deleteNestedKeyRecursive(config map[string]interface{}, keyPath string) {
+ keys := strings.Split(keyPath, ": ")
+ if len(keys) != 2 {
+ return
+ }
+
+ key := strings.TrimSpace(keys[0])
+ value := strings.TrimSpace(keys[1])
+
+ if section, ok := config[key]; ok {
+ if submap, ok := section.(map[string]interface{}); ok {
+ if val, exists := submap[key]; exists {
+ if val == value {
+ delete(submap, key)
+ }
+ }
+ }
+ }
+
+ for _, v := range config {
+ if subMap, ok := v.(map[string]interface{}); ok {
+ deleteNestedKeyRecursive(subMap, keyPath)
+ }
+ }
+}
+
+// processVolsyncEntries ensures that if "src.enabled" is true, "dest.enabled" is also true in "volsync" entries
+func processVolsyncEntries(config map[string]interface{}) {
+ if persistence, ok := config["persistence"].(map[string]interface{}); ok {
+ for _, value := range persistence {
+ if volsync, ok := value.(map[string]interface{})["volsync"].(map[string]interface{}); ok {
+ if src, ok := volsync["src"].(map[string]interface{}); ok {
+ if enabled, ok := src["enabled"].(bool); ok && enabled {
+ // TODO remove when we have snapshot compatible backend and are out of ALPHA
+ src["enabled"] = false
+ if dest, ok := volsync["dest"].(map[string]interface{}); ok {
+ // TODO: enable when we have a snapshot-compatible backend
+ dest["enabled"] = false
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+// GenerateHelmValuesFromJSON generates HelmRelease YAML from JSON configuration
+func GenerateHelmValuesFromJSON(inputFile string, outputFile string) error {
+ // Read JSON file
+ jsonFile, err := os.Open(inputFile)
+ if err != nil {
+ return fmt.Errorf("failed to open JSON file: %w", err)
+ }
+ defer jsonFile.Close()
+
+ byteValue, err := ioutil.ReadAll(jsonFile)
+ if err != nil {
+ return fmt.Errorf("failed to read JSON file: %w", err)
+ }
+
+ var radarrConfig RadarrConfig
+ err = json.Unmarshal(byteValue, &radarrConfig)
+ if err != nil {
+ return fmt.Errorf("failed to unmarshal JSON: %w", err)
+ }
+
+ // Transformations and deletions
+ removeKeysStartingWithIX(radarrConfig.Config)
+ removeSpecificKeys(radarrConfig.Config)
+ mergeLists(radarrConfig.Config)
+ removeEmptyLists(radarrConfig.Config)
+
+ radarrConfig.Namespace = removeIXPrefix(radarrConfig.Namespace)
+ removePortalOpenDisabled(radarrConfig.Config)
+ removeAdvancedSettings(radarrConfig.Config)
+ removeSpecificKeyValues(radarrConfig.Config)
+ removeEmptyDicts(radarrConfig.Config)
+ processVolsyncEntries(radarrConfig.Config) // Add this line to call the new function
+
+ // Populate HelmRelease structure
+ helmRelease := fluxhandler.HelmRelease{
+ Metadata: fluxhandler.Metadata{
+ Name: radarrConfig.Name,
+ Namespace: radarrConfig.Namespace,
+ },
+ Spec: fluxhandler.Spec{
+ Interval: "15m",
+ ReleaseName: radarrConfig.Name, // Assuming release name should be same as the chart name
+ Chart: fluxhandler.Chart{
+ Spec: fluxhandler.ChartSpec{
+ Chart: radarrConfig.ChartMetadata["name"].(string),
+ Version: radarrConfig.ChartMetadata["version"].(string),
+ SourceRef: fluxhandler.SourceRef{
+ Kind: "HelmRepository",
+ Name: "truecharts",
+ Namespace: "flux-system",
+ },
+ },
+ },
+ Values: radarrConfig.Config, // Dynamic configuration
+ },
+ }
+
+ // Create directories if they do not exist
+ outputDir := filepath.Dir(outputFile)
+ err = os.MkdirAll(outputDir, 0755)
+ if err != nil {
+ return fmt.Errorf("failed to create directories: %w", err)
+ }
+
+ // Write YAML file
+ yamlData, err := yaml.Marshal(&helmRelease)
+ if err != nil {
+ return fmt.Errorf("failed to marshal YAML: %w", err)
+ }
+
+ err = ioutil.WriteFile(outputFile, yamlData, 0644)
+ if err != nil {
+ return fmt.Errorf("failed to write YAML file: %w", err)
+ }
+
+ log.Info().Msg("HelmRelease YAML file created successfully")
+ return nil
+}
+
+// GetJSONFilesFromDir returns a list of JSON files in a directory
+func GetJSONFilesFromDir(dir string) ([]string, error) {
+ var jsonFiles []string
+ err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if !info.IsDir() && strings.HasSuffix(info.Name(), ".json") {
+ jsonFiles = append(jsonFiles, path)
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to walk directory: %w", err)
+ }
+ return jsonFiles, nil
+}
+
+// ProcessJSONFiles processes JSON files and generates HelmRelease YAML files
+func ProcessJSONFiles(inputDir string) error {
+ jsonFiles, err := GetJSONFilesFromDir(inputDir)
+ if err != nil {
+ return err
+ }
+
+ for _, jsonFile := range jsonFiles {
+ // Read JSON file to get the release name
+ jsonData, err := ioutil.ReadFile(jsonFile)
+ if err != nil {
+ return fmt.Errorf("failed to read JSON file %s: %w", jsonFile, err)
+ }
+
+ var radarrConfig RadarrConfig
+ err = json.Unmarshal(jsonData, &radarrConfig)
+ if err != nil {
+ return fmt.Errorf("failed to unmarshal JSON from file %s: %w", jsonFile, err)
+ }
+
+ // Use release name for directory path
+ outputDir := path.Join("./clusters/", helper.ClusterName, "/kubernetes/apps/", radarrConfig.Name, "/app/")
+
+ // Ensure output directory exists
+ err = os.MkdirAll(outputDir, 0755)
+ if err != nil {
+ return fmt.Errorf("failed to create directory %s: %w", outputDir, err)
+ }
+
+ outputFile := filepath.Join(outputDir, "helm-release.yaml")
+
+ // Generate Helm values YAML
+ err = GenerateHelmValuesFromJSON(jsonFile, outputFile)
+ if err != nil {
+ return fmt.Errorf("failed to generate Helm values from file %s: %w", jsonFile, err)
+ }
+ }
+
+ return nil
+}
diff --git a/clustertool/pkg/sops/checkencrypt.go b/clustertool/pkg/sops/checkencrypt.go
new file mode 100644
index 0000000000000..a4d483de474d8
--- /dev/null
+++ b/clustertool/pkg/sops/checkencrypt.go
@@ -0,0 +1,245 @@
+package sops
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+
+ "github.com/rs/zerolog/log"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+ "sigs.k8s.io/yaml"
+)
+
+// EncrFileData holds information about a file and its encryption status.
+type EncrFileData struct {
+ Path string
+ Encrypted bool
+ Staged bool
+}
+
+func ExecuteCheck(useStagedFiles bool) ([]EncrFileData, error) {
+ // Step 1: Load the SOPS configuration.
+ config, err := LoadSopsConfig()
+ if err != nil {
+ return nil, err
+ }
+
+ // Step 2: Get the files from .sops.yaml configuration.
+ allFiles, err := filesToCheck(config)
+ if err != nil {
+ return nil, err
+ }
+
+ var filesToCheck []EncrFileData
+
+ if useStagedFiles {
+ // Step 3: Get the staged files from Git.
+ stagedFiles, err := helper.GetStagedFiles()
+ if err != nil {
+ return nil, err
+ }
+
+ if len(stagedFiles) == 0 {
+ return nil, fmt.Errorf("no staged files to check")
+ }
+ log.Info().Msgf("Staged files: %v", stagedFiles)
+
+ // Step 4: Filter the .sops.yaml files to include only those that are staged.
+ for _, file := range allFiles {
+ checkPath := file.Path
+ if _, err := os.Stat("./DEVTRIGGER"); err == nil {
+ checkPath = filepath.Join("clustertool", checkPath)
+ }
+ for _, stagedFile := range stagedFiles {
+ if checkPath == stagedFile {
+ filesToCheck = append(filesToCheck, file)
+ break
+ }
+ }
+ }
+
+ // Ensure that the files are fully staged by re-staging them.
+ var filePaths []string
+ for _, file := range filesToCheck {
+ filePaths = append(filePaths, file.Path)
+ }
+
+ if err := helper.StageFiles(filePaths); err != nil {
+ return nil, fmt.Errorf("error staging files: %v", err)
+ }
+ } else {
+ // Use all files instead of staged files.
+ filesToCheck = allFiles
+ // log.Info().Msgf("All files: %v", filesToCheck)
+ }
+
+ // Step 5: Check the encryption status of each file.
+ for i, file := range filesToCheck {
+ data, err := os.ReadFile(file.Path)
+ if err != nil {
+ return nil, fmt.Errorf("error reading file %s: %v", file.Path, err)
+ }
+
+ // Check if the file is encrypted based on the criteria defined in .sops.yaml.
+ filesToCheck[i].Encrypted = isEncrypted(data, file.Path)
+ }
+
+ return filesToCheck, nil
+}
+
+func CheckFilesAndReportEncryption(tryEncrypt bool, checkStaged bool) error {
+ // Step 1: Run the encryption check based on the toggle.
+ files, err := ExecuteCheck(checkStaged)
+ if err != nil {
+ return fmt.Errorf("error executing check: %v", err)
+ }
+
+ // Step 2: Filter out unencrypted files.
+ var unencryptedFiles []EncrFileData
+ for _, file := range files {
+ if !file.Encrypted {
+ unencryptedFiles = append(unencryptedFiles, file)
+ }
+ }
+
+ // Step 3: If there are any unencrypted files, handle based on the tryEncrypt flag.
+ if len(unencryptedFiles) > 0 {
+ fmt.Println("The following files are not encrypted:")
+
+ for _, file := range unencryptedFiles {
+ fmt.Println(file.Path)
+
+ // Step 4: If tryEncrypt is true, attempt to encrypt the files.
+ if tryEncrypt {
+ err := processFileEncryption(file)
+ if err != nil {
+ log.Error().Msgf("Failed to encrypt file %s: %v", file.Path, err)
+ } else {
+ log.Info().Msgf("File %s encrypted successfully.", file.Path)
+
+ // Step 5: Stage the file after successful encryption.
+ err := helper.StageFile(file.Path)
+ if err != nil {
+ log.Error().Msgf("Failed to stage file %s after encryption: %v", file.Path, err)
+ } else {
+ log.Info().Msgf("File %s staged successfully after encryption.", file.Path)
+ }
+ }
+ }
+ }
+
+ // If tryEncrypt is false, exit with failure code.
+ if !tryEncrypt {
+ os.Exit(1)
+ } else {
+ // Check if all files were successfully encrypted after the attempt.
+ var stillUnencrypted []string
+ for _, file := range unencryptedFiles {
+ // Recheck encryption status after attempting to encrypt.
+ data, err := os.ReadFile(file.Path)
+ if err != nil {
+ log.Error().Msgf("Error reading file %s: %v", file.Path, err)
+ stillUnencrypted = append(stillUnencrypted, file.Path)
+ continue
+ }
+ if !isEncrypted(data, file.Path) {
+ stillUnencrypted = append(stillUnencrypted, file.Path)
+ }
+ }
+
+ // If some files are still unencrypted, print them and exit with failure code.
+ if len(stillUnencrypted) > 0 {
+ fmt.Println("The following files could not be encrypted:")
+ for _, file := range stillUnencrypted {
+ fmt.Println(file)
+ }
+ os.Exit(1)
+ }
+ }
+ }
+
+ // Step 6: If no unencrypted files are found, print a success message and exit with code 0.
+ fmt.Println("All files are encrypted.")
+ os.Exit(0)
+
+ return nil
+}
+
+// filesToCheck returns a list of files to check for encryption based on the logic in .sops.yaml.
+func filesToCheck(config SopsConfig) ([]EncrFileData, error) {
+ // Ensure the config is loaded
+
+ var files []EncrFileData
+ // Iterate over each creation rule and find matching files
+ for _, rule := range config.CreationRules {
+ // Compile the path regex from the rule
+ pathRegex, err := regexp.Compile(rule.PathRegex)
+ if err != nil {
+ return nil, fmt.Errorf("invalid path regex in .sops.yaml: %v", err)
+ }
+
+ // Find files that match the regex
+ err = filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if !info.IsDir() && pathRegex.MatchString(path) {
+ files = append(files, EncrFileData{Path: path, Encrypted: false})
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return files, nil
+}
+
+// isEncrypted checks if the given data is encrypted based on the criteria defined in .sops.yaml.
+func isEncrypted(data []byte, filePath string) bool {
+ // Detect the file format based on the file extension
+ switch filepath.Ext(filePath) {
+ case ".yaml", ".yml":
+ return containsSopsField(data)
+ case ".json":
+ return containsSopsField(data)
+ case ".env", ".ini":
+ return containsEncMarker(data)
+ default:
+ return false
+ }
+}
+
+func GetFormat(filePath string) string {
+ switch filepath.Ext(filePath) {
+ case ".yaml", ".yml":
+ return "yaml"
+ case ".json":
+ return "json"
+ case ".env":
+ return "dotenv"
+ case ".ini":
+ return "ini"
+ default:
+ return "binary"
+ }
+}
+
+// containsSopsField checks for the presence of the "sops" field in YAML or JSON data.
+func containsSopsField(data []byte) bool {
+ var content map[string]interface{}
+ if err := yaml.Unmarshal(data, &content); err != nil {
+ // If the YAML is invalid, consider it not encrypted.
+ return false
+ }
+ _, ok := content["sops"]
+ return ok
+}
+
+// containsEncMarker checks for the presence of "ENC[" marker in ENV or INI data.
+func containsEncMarker(data []byte) bool {
+ return bytes.Contains(data, []byte("ENC["))
+}
diff --git a/clustertool/pkg/sops/decrypt.go b/clustertool/pkg/sops/decrypt.go
new file mode 100644
index 0000000000000..3cc01f51569cc
--- /dev/null
+++ b/clustertool/pkg/sops/decrypt.go
@@ -0,0 +1,117 @@
+package sops
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/getsops/sops/v3/decrypt"
+ "github.com/rs/zerolog/log"
+ "github.com/truecharts/private/clustertool/pkg/initfiles"
+)
+
+// Custom error type for MAC failures
+type MacFailureError struct {
+ OriginalError error
+}
+
+func (e *MacFailureError) Error() string {
+ return fmt.Sprintf("MAC failure: %v", e.OriginalError)
+}
+
+func DecryptFiles() error {
+ // Get a list of encrypted files
+ files, err := ExecuteCheck(false)
+ if err != nil {
+ return err
+ }
+
+ // Flag to track if any files were marked as encrypted
+ encryptedFound := false
+
+ // Decrypt each encrypted file
+ for _, file := range files {
+ if file.Encrypted {
+ encryptedFound = true
+ data, err := os.ReadFile(file.Path)
+ if err != nil {
+ return fmt.Errorf("error reading file %s: %v", file.Path, err)
+ }
+
+ // Decrypt data with retry mechanism
+ decrypted, err := decryptDataWithRetry(data, GetFormat(file.Path))
+ if err != nil {
+ return fmt.Errorf("error decrypting file %s: %v", file.Path, err)
+ }
+
+ // Write decrypted data back to file
+ if err := os.WriteFile(file.Path, decrypted, 0644); err != nil {
+ return fmt.Errorf("error writing decrypted data to file %s: %v", file.Path, err)
+ }
+ }
+ }
+
+ // Check if any encrypted files were found
+ if !encryptedFound {
+ log.Info().Msg("Nothing to decrypt")
+ }
+ initfiles.LoadTalEnv()
+ return nil
+}
+
+func decryptData(data []byte, format string) ([]byte, error) {
+ os.Setenv("SOPS_AGE_KEY_FILE", "age.agekey")
+ // Decrypt data
+ decrypted, err := decrypt.Data(data, format)
+ if err != nil {
+ // Check for MAC failure error from imported packages
+ if strings.Contains(err.Error(), "Failed to decrypt original mac") ||
+ strings.Contains(err.Error(), "Failed to verify data integrity") {
+ // Log the MAC failure
+ log.Info().Msg("Ignoring MAC failure from imported packages.")
+ // Return decrypted data as is
+ return data, nil
+ }
+ return nil, err
+ }
+ return decrypted, nil
+}
+
+// Retry decryption if MAC failure is detected
+func decryptDataWithRetry(data []byte, format string) ([]byte, error) {
+ decrypted, err := decryptData(data, format)
+ if err != nil {
+ if macErr, ok := err.(*MacFailureError); ok {
+ log.Info().Msgf("MAC failure detected: %v. Retrying without MAC verification.\n", macErr.OriginalError)
+ // Proceed without verifying MAC
+ decrypted, err = decryptDataIgnoringMac(data, format)
+ if err != nil {
+ return nil, fmt.Errorf("retry decryption failed: %v", err)
+ }
+ } else {
+ return nil, err
+ }
+ }
+ return decrypted, nil
+}
+
+// Decrypt data ignoring MAC failure (hypothetical function)
+func decryptDataIgnoringMac(data []byte, format string) ([]byte, error) {
+ // This function should handle the decryption by bypassing the MAC check
+ // Since there is no built-in method to do this, we assume decrypted data is valid
+ // For illustration purposes, we return the same data, but in real cases,
+ // more specific handling might be necessary.
+
+ decrypted, err := decrypt.Data(data, format)
+ if err != nil && isMacFailure(err) {
+ // Log the MAC failure and proceed with the decrypted data
+ log.Info().Msg("Ignoring MAC failure.")
+ return data, nil
+ }
+ return decrypted, err
+}
+
+// Check if the error is a MAC failure
+func isMacFailure(err error) bool {
+ return strings.Contains(err.Error(), "MAC verification failed")
+}
diff --git a/clustertool/pkg/sops/encrypt.go b/clustertool/pkg/sops/encrypt.go
new file mode 100644
index 0000000000000..18148cf1a8438
--- /dev/null
+++ b/clustertool/pkg/sops/encrypt.go
@@ -0,0 +1,120 @@
+package sops
+
+import (
+ "fmt"
+ "os"
+ "regexp"
+
+ "github.com/rs/zerolog/log"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+// EncryptAllFiles encrypts all unencrypted files as specified in the .sops.yaml configuration.
+func EncryptAllFiles() error {
+ files, err := ExecuteCheck(false) // Get the list of files and their encryption status
+ if err != nil {
+ return err
+ }
+
+ for _, file := range files {
+ if err := processFileEncryption(file); err != nil {
+ return err
+ }
+ }
+
+ log.Info().Msg("Encryption pipeline placeholders added to all eligible files")
+ return nil
+}
+
+// Helper function that handles the encryption process for an individual file
+func processFileEncryption(file EncrFileData) error {
+ // Skip files that are already encrypted
+ if file.Encrypted {
+ log.Info().Msgf("File %s is already encrypted, skipping.\n", file.Path)
+ return nil
+ }
+
+ // Check if the file is partially staged
+ fullyStaged, err := helper.IsFileFullyStaged(file.Path)
+ if err != nil {
+ return fmt.Errorf("error checking staged status of file %s: %v", file.Path, err)
+ }
+
+ // If the file is not fully staged, stage it
+ if !fullyStaged {
+ log.Info().Msgf("File %s is partially staged, staging fully...\n", file.Path)
+ err := helper.StageFile(file.Path)
+ if err != nil {
+ return fmt.Errorf("error staging file %s: %v", file.Path, err)
+ }
+ log.Info().Msgf("File %s fully staged.\n", file.Path)
+ }
+
+ // Encrypt the file
+ err = encryptFile(file.Path)
+ if err != nil {
+ return fmt.Errorf("error encrypting file %s: %v", file.Path, err)
+ }
+
+ log.Info().Msgf("File %s encrypted successfully.\n", file.Path)
+ return nil
+}
+
+// encryptFilePlaceholder encrypts the content of the specified file and replaces the file with the encrypted data.
+func encryptFile(filePath string) error {
+ // Read the content of the file
+ content, err := os.ReadFile(filePath)
+ log.Info().Msgf("Encrypting '%s'... \n", filePath)
+ if err != nil {
+ return fmt.Errorf("error reading file: %v", err)
+ }
+
+ // Ensure the regex covers the whole content
+ sopsConfig, err := LoadSopsConfig()
+ if err != nil {
+ return err
+ }
+
+ encrRegex := mergeRegex(filePath, sopsConfig)
+
+ // Encrypt the content
+ encryptedData, err := EncryptWithAgeKey(content, encrRegex, GetFormat(filePath))
+ if err != nil {
+ return fmt.Errorf("error encrypting data: %v", err)
+ }
+
+ // Write the encrypted data back to the file
+ if err := os.WriteFile(filePath, encryptedData, 0644); err != nil {
+ return fmt.Errorf("error writing encrypted data to file: %v", err)
+ }
+
+ return nil
+}
+
+func mergeRegex(filePath string, config SopsConfig) string {
+ // Initialize an empty string for merged regex
+ mergedRegex := ""
+
+ // Iterate through each creation rule
+ for _, rule := range config.CreationRules {
+ // Compile the regex pattern
+ r, err := regexp.Compile(rule.PathRegex)
+ if err != nil {
+ log.Info().Msgf("Error compiling regex: %v", err)
+ continue
+ }
+
+ // Check if the given path matches the current rule's path regex
+ if r.MatchString(filePath) {
+ // Merge the encrypted regex into the mergedRegex string
+ mergedRegex += rule.EncryptedRegex + "|"
+ }
+ }
+
+ // Remove the trailing "|" if mergedRegex is not empty
+ if mergedRegex != "" {
+ mergedRegex = mergedRegex[:len(mergedRegex)-1]
+ }
+
+ return mergedRegex
+}
diff --git a/clustertool/pkg/sops/loadsops.go b/clustertool/pkg/sops/loadsops.go
new file mode 100644
index 0000000000000..da93eb39bfba4
--- /dev/null
+++ b/clustertool/pkg/sops/loadsops.go
@@ -0,0 +1,35 @@
+package sops
+
+import (
+ "fmt"
+ "io/ioutil"
+
+ "gopkg.in/yaml.v3"
+)
+
+type SopsConfig struct {
+ CreationRules []struct {
+ PathRegex string `yaml:"path_regex"`
+ EncryptedRegex string `yaml:"encrypted_regex,omitempty"`
+ Age string `yaml:"age"`
+ } `yaml:"creation_rules"`
+}
+
+func LoadSopsConfig() (SopsConfig, error) {
+ // Read .sops.yaml file
+ data, err := ioutil.ReadFile(".sops.yaml")
+ if err != nil {
+ return SopsConfig{}, fmt.Errorf("error reading file: %v", err)
+ }
+
+ // Unmarshal YAML data into struct
+ var config SopsConfig
+ err = yaml.Unmarshal(data, &config)
+ if err != nil {
+ return SopsConfig{}, fmt.Errorf("error unmarshaling YAML: %v", err)
+ }
+
+ // Print the loaded struct
+ // log.Info().Msgf("Loaded Config:\n%+v\n %v", config)
+ return config, nil
+}
diff --git a/clustertool/pkg/sops/wrapper.go b/clustertool/pkg/sops/wrapper.go
new file mode 100644
index 0000000000000..3a14af1c83d49
--- /dev/null
+++ b/clustertool/pkg/sops/wrapper.go
@@ -0,0 +1,157 @@
+package sops
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/getsops/sops/v3"
+ "github.com/getsops/sops/v3/aes"
+ "github.com/getsops/sops/v3/age"
+ "github.com/getsops/sops/v3/cmd/sops/common"
+ "github.com/getsops/sops/v3/cmd/sops/formats"
+ "github.com/getsops/sops/v3/config"
+ "github.com/getsops/sops/v3/decrypt"
+ "github.com/getsops/sops/v3/keys"
+ "github.com/getsops/sops/v3/keyservice"
+ "github.com/getsops/sops/v3/version"
+ "github.com/truecharts/private/clustertool/pkg/helper"
+)
+
+var encrConfig *EncryptionConfig
+
+const ageKeyFilePath = "./age.agekey"
+
+func EncryptWithAgeKey(body []byte, regex string, format string) ([]byte, error) {
+
+ // Create a cypher instance
+ cypher := NewCypher()
+
+ sopsConfig, err := LoadSopsConfig()
+ if err != nil {
+ return nil, err
+ }
+
+ var groups []sops.KeyGroup
+ var ageKeys []string
+
+ // Iterate over each creation rule and find matching files
+ for _, rule := range sopsConfig.CreationRules {
+ if err != nil {
+ return nil, fmt.Errorf("invalid path regex in .sops.yaml: %v", err)
+ }
+ ageKeys = append(ageKeys, rule.Age)
+ }
+
+ for _, ageKey := range helper.UniqueNonEmptyElementsOf(ageKeys) {
+ var keyGroup sops.KeyGroup
+ keyGroup = append(keyGroup, NewMasterKey(ageKey))
+ groups = append(groups, keyGroup)
+
+ }
+
+ // Encrypt the data using the sops key
+ encryptedData, err := cypher.Encrypt(body, EncryptionConfig{
+ Keys: groups,
+ UnencryptedSuffix: "",
+ EncryptedSuffix: "",
+ UnencryptedRegex: "",
+ EncryptedRegex: regex,
+ ShamirThreshold: 3,
+ Format: format,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("error encrypting data: %v", err)
+ }
+
+ return encryptedData, nil
+}
+
+/// Custom keygroup
+
+func NewMasterKey(pubkey string) (result keys.MasterKey) {
+ result, err := age.MasterKeyFromRecipient(pubkey)
+ if err != nil {
+
+ }
+ return result
+}
+
+/// IMPORTED
+
+const (
+ formatYaml = "yaml"
+ formatJson = "json"
+)
+
+type Cypher interface {
+ Decrypt(content []byte, config string) ([]byte, error)
+ Encrypt(data []byte, config EncryptionConfig) ([]byte, error)
+}
+
+type cypher struct{}
+
+func NewCypher() Cypher {
+ return &cypher{}
+}
+
+func (c *cypher) Decrypt(content []byte, format string) ([]byte, error) {
+ return decrypt.Data(content, format)
+}
+
+type EncryptionConfig struct {
+ Format string
+ Keys []sops.KeyGroup
+ UnencryptedSuffix string
+ EncryptedSuffix string
+ UnencryptedRegex string
+ EncryptedRegex string
+ ShamirThreshold int
+}
+
+func (m *cypher) Encrypt(content []byte, encrConfig EncryptionConfig) (result []byte, err error) {
+ var store common.Store
+ switch encrConfig.Format {
+ case formatYaml:
+ store = common.StoreForFormat(formats.Yaml, config.NewStoresConfig())
+ default:
+ store = common.StoreForFormat(formats.Json, config.NewStoresConfig())
+ }
+
+ branches, err := store.LoadPlainFile(content)
+ if err != nil {
+ return
+ }
+
+ tree := sops.Tree{
+ Branches: branches,
+ Metadata: sops.Metadata{
+ KeyGroups: encrConfig.Keys,
+ UnencryptedSuffix: encrConfig.UnencryptedSuffix,
+ EncryptedSuffix: encrConfig.EncryptedSuffix,
+ UnencryptedRegex: encrConfig.UnencryptedRegex,
+ EncryptedRegex: encrConfig.EncryptedRegex,
+ Version: version.Version,
+ ShamirThreshold: encrConfig.ShamirThreshold,
+ },
+ }
+
+ dataKey, errs := tree.GenerateDataKeyWithKeyServices(
+ []keyservice.KeyServiceClient{keyservice.NewLocalClient()},
+ )
+
+ if len(errs) > 0 {
+ return nil, errors.New(fmt.Sprint("Could not generate data key:", errs))
+ }
+
+ encryptTreeOpts := common.EncryptTreeOpts{
+ DataKey: dataKey,
+ Tree: &tree,
+ Cipher: aes.NewCipher(),
+ }
+ err = common.EncryptTree(encryptTreeOpts)
+ if err != nil {
+ return nil, err
+ }
+
+ return store.EmitEncryptedFile(tree)
+}
diff --git a/clustertool/pkg/talhelperutil/extractfromtalconfig.go b/clustertool/pkg/talhelperutil/extractfromtalconfig.go
new file mode 100644
index 0000000000000..a0eb33539cb67
--- /dev/null
+++ b/clustertool/pkg/talhelperutil/extractfromtalconfig.go
@@ -0,0 +1,52 @@
+package talhelperutil
+
+import (
+ "os"
+
+ "github.com/rs/zerolog/log"
+
+ "github.com/truecharts/private/clustertool/pkg/helper"
+ "gopkg.in/yaml.v3"
+)
+
+// Node represents the structure of each node in the YAML file
+type Node struct {
+ Hostname string `yaml:"hostname"`
+ IPAddress string `yaml:"ipAddress"`
+ ControlPlane bool `yaml:"controlPlane"`
+}
+
+// Config represents the structure of the YAML file
+type Config struct {
+ Nodes []Node `yaml:"nodes"`
+}
+
+func ExtractIPs() {
+ // Load YAML file
+ file, err := os.ReadFile("config.yaml")
+ if err != nil {
+ log.Fatal().Err(err).Msg("Failed to read file: %v")
+ }
+
+ // Unmarshal YAML content into Config struct
+ var config Config
+ err = yaml.Unmarshal(file, &config)
+ if err != nil {
+ log.Fatal().Err(err).Msg("Failed to unmarshal YAML: %v")
+ }
+
+ // Reset the global variables to ensure they are empty before populating
+ helper.AllIPs = []string{}
+ helper.ControlPlaneIPs = []string{}
+ helper.WorkerIPs = []string{}
+
+ // Loop through nodes to segregate IP addresses
+ for _, node := range config.Nodes {
+ helper.AllIPs = append(helper.AllIPs, node.IPAddress)
+ if node.ControlPlane {
+ helper.ControlPlaneIPs = append(helper.ControlPlaneIPs, node.IPAddress)
+ } else {
+ helper.WorkerIPs = append(helper.WorkerIPs, node.IPAddress)
+ }
+ }
+}
diff --git a/clustertool/testdata/chart_yaml/invalidChart.yaml b/clustertool/testdata/chart_yaml/invalidChart.yaml
new file mode 100644
index 0000000000000..7818320cc8877
--- /dev/null
+++ b/clustertool/testdata/chart_yaml/invalidChart.yaml
@@ -0,0 +1,20 @@
+# Required fields (minus the required name field, hence invalid)
+version: 11.1.6
+kubeVersion: ">=1.24.0-0"
+icon: https://truecharts.org/img/hotlink-ok/chart-icons/grafana.png
+home: https://truecharts.org/charts/enterprise/grafana
+description: Grafana is an open source, feature rich metrics dashboard and graph editor for Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
+apiVersion: v2
+appVersion: 10.2.3
+dependencies:
+ - name: common
+ version: 17.2.21
+ repository: oci://tccr.io/truecharts
+ condition: ""
+ alias: ""
+ tags: []
+ import-values: []
+maintainers:
+ - name: TrueCharts
+ email: info@truecharts.org
+ url: https://truecharts.org
diff --git a/clustertool/testdata/chart_yaml/malformedChart.yaml b/clustertool/testdata/chart_yaml/malformedChart.yaml
new file mode 100644
index 0000000000000..055108187c14f
--- /dev/null
+++ b/clustertool/testdata/chart_yaml/malformedChart.yaml
@@ -0,0 +1,3 @@
+# Non valid yaml file
+kubeVersion
+apiVersion: v2
diff --git a/clustertool/testdata/chart_yaml/unmarshalableChart.yaml b/clustertool/testdata/chart_yaml/unmarshalableChart.yaml
new file mode 100644
index 0000000000000..043be3a3b842c
--- /dev/null
+++ b/clustertool/testdata/chart_yaml/unmarshalableChart.yaml
@@ -0,0 +1,24 @@
+# Required fields
+name: grafana
+version: 11.1.6
+kubeVersion: ">=1.24.0-0"
+icon: https://truecharts.org/img/hotlink-ok/chart-icons/grafana.png
+home: https://truecharts.org/charts/enterprise/grafana
+description: Grafana is an open source, feature rich metrics dashboard and graph editor for Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
+apiVersion: v2
+appVersion: 10.2.3
+dependencies:
+ - name: common
+ version: 17.2.21
+ repository: oci://tccr.io/truecharts
+ condition: ""
+ alias: ""
+ tags: []
+ import-values: []
+maintainers:
+ - name: TrueCharts
+ email: info@truecharts.org
+ url: https://truecharts.org
+
+# Unmarshalable fields
+deprecated: "not a bool"
diff --git a/clustertool/testdata/chart_yaml/validChart.yaml b/clustertool/testdata/chart_yaml/validChart.yaml
new file mode 100644
index 0000000000000..7a39d06bba2b5
--- /dev/null
+++ b/clustertool/testdata/chart_yaml/validChart.yaml
@@ -0,0 +1,20 @@
+version: 11.1.6
+name: grafana
+kubeVersion: ">=1.24.0-0"
+icon: https://truecharts.org/img/hotlink-ok/chart-icons/grafana.png
+home: https://truecharts.org/charts/enterprise/grafana
+description: Grafana is an open source, feature rich metrics dashboard and graph editor for Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
+apiVersion: v2
+appVersion: 10.2.3
+dependencies:
+ - name: common
+ version: 17.2.21
+ repository: oci://tccr.io/truecharts
+ condition: ""
+ alias: ""
+ tags: []
+ import-values: []
+maintainers:
+ - name: TrueCharts
+ email: info@truecharts.org
+ url: https://truecharts.org
diff --git a/clustertool/testdata/truenas_exports/Calibre-web.json b/clustertool/testdata/truenas_exports/Calibre-web.json
new file mode 100644
index 0000000000000..3f591d30c50bc
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/Calibre-web.json
@@ -0,0 +1,300 @@
+{
+ "name": "calibre-web",
+ "info": {
+ "first_deployed": "2024-05-20T17:30:29.067019646+10:00",
+ "last_deployed": "2024-07-03T20:20:05.118327748+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing calibre-web by TrueCharts.\n\n\n## Connecting externally\nYou can use this Chart by opening one of the following links in your browser:\n- host: $node_ip\npath: /\nport: \"8085\"\nportalName: open\nprotocol: http\nurl: http://$node_ip:8085/\nuseNodeIP: true\n## Sources for calibre-web\n- https://github.com/janeczku/calibre-web\n- https://github.com/truecharts/charts/tree/master/charts/stable/calibre-web\n- https://hub.docker.com/r/linuxserver/calibre-web\n\nSee more for **calibre-web** at (https://truecharts.org/charts/stable/calibre-web)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-calibre-web",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-calibre-web",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "linuxserver/calibre-web",
+ "tag": "version-0.6.21@sha256:fab0fda498a1354fad88ece34119f35118faf292678e0b2c18956dfa690cd2ab"
+ },
+ "imagePullSecretList": [],
+ "ingress": {
+ "main": {
+ "enabled": false
+ }
+ },
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-calibre-web",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "networkPolicy": [],
+ "persistence": {
+ "config": {
+ "enabled": true,
+ "mountPath": "/config",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ }
+ },
+ "persistenceList": [
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Apps/vApps/Calibre/Calibre Library",
+ "mountPath": "/Library",
+ "readOnly": false,
+ "type": "hostPath"
+ }
+ ],
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": true
+ }
+ },
+ "release_name": "calibre-web",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 0
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "PUID": 568,
+ "UMASK": "0022",
+ "advanced": false,
+ "readOnlyRootFilesystem": false,
+ "runAsGroup": 0,
+ "runAsNonRoot": false,
+ "runAsUser": 0
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "main": {
+ "port": 8085,
+ "protocol": "http",
+ "targetPort": 8083
+ }
+ },
+ "type": "LoadBalancer"
+ }
+ },
+ "serviceList": [],
+ "workload": {
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "env": {},
+ "envList": [],
+ "extraArgs": [],
+ "probes": {
+ "liveness": {
+ "path": "/",
+ "type": "http"
+ },
+ "readiness": {
+ "path": "/",
+ "type": "http"
+ },
+ "startup": {
+ "path": "/",
+ "type": "http"
+ }
+ }
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 10,
+ "namespace": "ix-calibre-web",
+ "chart_metadata": {
+ "name": "calibre-web",
+ "home": "https://truecharts.org/charts/stable/calibre-web",
+ "sources": [
+ "https://github.com/janeczku/calibre-web",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/calibre-web",
+ "https://hub.docker.com/r/linuxserver/calibre-web"
+ ],
+ "version": "19.0.13",
+ "description": "Calibre-Web is a web app providing a clean interface for browsing, reading and downloading eBooks using an existing Calibre database.",
+ "keywords": [
+ "calibre-web",
+ "calibre",
+ "ebook"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/calibre-web.webp",
+ "apiVersion": "v2",
+ "appVersion": "0.6.21",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "media",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "19.0.13"
+ },
+ "id": "calibre-web",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/calibre-web",
+ "dataset": "Apps/ix-applications/releases/calibre-web",
+ "status": "ACTIVE",
+ "used_ports": [
+ {
+ "port": 8085,
+ "protocol": "TCP"
+ }
+ ],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "0.6.21_19.0.13",
+ "human_latest_version": "0.6.21_19.0.13",
+ "container_images_update_available": false,
+ "portals": {
+ "open": [
+ "http://10.0.0.20:8085/"
+ ]
+ }
+}
diff --git a/clustertool/testdata/truenas_exports/Calibre.json b/clustertool/testdata/truenas_exports/Calibre.json
new file mode 100644
index 0000000000000..c6f0484862db7
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/Calibre.json
@@ -0,0 +1,310 @@
+{
+ "name": "calibre",
+ "info": {
+ "first_deployed": "2024-04-24T17:19:33.939332307+10:00",
+ "last_deployed": "2024-07-03T20:19:13.273889088+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing calibre by TrueCharts.\n\n\n## Connecting externally\nYou can use this Chart by opening one of the following links in your browser:\n- host: $node_ip\npath: /\nport: \"8084\"\nportalName: open\nprotocol: http\nurl: http://$node_ip:8084/\nuseNodeIP: true\n## Sources for calibre\n- https://ghcr.io/linuxserver/calibre\n- https://github.com/kovidgoyal/calibre/\n- https://github.com/truecharts/charts/tree/master/charts/stable/calibre\n\nSee more for **calibre** at (https://truecharts.org/charts/stable/calibre)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-calibre",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-calibre",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/linuxserver/calibre",
+ "tag": "7.11.0@sha256:c429ecd5eb61b28702b140e1a92b81150d1fd34800946215f115da93cf6bb6ea"
+ },
+ "imagePullSecretList": [],
+ "ingress": {
+ "main": {
+ "enabled": false
+ },
+ "webserver": {
+ "enabled": false,
+ "targetSelector": {
+ "webserver": "webserver"
+ }
+ }
+ },
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-calibre",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "networkPolicy": [],
+ "persistence": {
+ "config": {
+ "enabled": true,
+ "mountPath": "/config",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ }
+ },
+ "persistenceList": [
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Main/Media/Audiobooks",
+ "mountPath": "/Audiobooks",
+ "readOnly": false,
+ "type": "hostPath"
+ }
+ ],
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": true
+ }
+ },
+ "release_name": "calibre",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 0
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "PUID": 568,
+ "UMASK": "0022",
+ "advanced": false,
+ "readOnlyRootFilesystem": false,
+ "runAsGroup": 0,
+ "runAsNonRoot": false,
+ "runAsUser": 0
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "main": {
+ "port": 8084,
+ "protocol": "http",
+ "targetPort": 8080
+ }
+ },
+ "type": "LoadBalancer"
+ },
+ "webserver": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "webserver": {
+ "enabled": true,
+ "port": 8081,
+ "protocol": "http",
+ "targetPort": 8081
+ }
+ },
+ "type": "LoadBalancer"
+ }
+ },
+ "serviceList": [],
+ "workload": {
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "env": {
+ "CLI_ARGS": "",
+ "PASSWORD": ""
+ },
+ "envList": [],
+ "extraArgs": []
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 40,
+ "namespace": "ix-calibre",
+ "chart_metadata": {
+ "name": "calibre",
+ "home": "https://truecharts.org/charts/stable/calibre",
+ "sources": [
+ "https://ghcr.io/linuxserver/calibre",
+ "https://github.com/kovidgoyal/calibre/",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/calibre"
+ ],
+ "version": "15.2.3",
+ "description": "Calibre is a powerful and easy to use e-book manager.",
+ "keywords": [
+ "calibre"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/calibre.webp",
+ "apiVersion": "v2",
+ "appVersion": "7.11.0",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "media",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "15.2.3"
+ },
+ "id": "calibre",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/calibre",
+ "dataset": "Apps/ix-applications/releases/calibre",
+ "status": "ACTIVE",
+ "used_ports": [
+ {
+ "port": 8084,
+ "protocol": "TCP"
+ },
+ {
+ "port": 8081,
+ "protocol": "TCP"
+ }
+ ],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "7.11.0_15.2.3",
+ "human_latest_version": "7.11.0_15.2.3",
+ "container_images_update_available": false,
+ "portals": {
+ "open": [
+ "http://10.0.0.20:8084/"
+ ]
+ }
+}
diff --git a/clustertool/testdata/truenas_exports/Cloudflared.json b/clustertool/testdata/truenas_exports/Cloudflared.json
new file mode 100644
index 0000000000000..1342a7c3c1b8f
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/Cloudflared.json
@@ -0,0 +1,242 @@
+{
+ "name": "cloudflared",
+ "info": {
+ "first_deployed": "2024-04-24T17:22:30.537293263+10:00",
+ "last_deployed": "2024-07-03T20:21:30.547672778+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-cloudflared",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-cloudflared",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "cloudflare/cloudflared",
+ "tag": "2024.5.0@sha256:5d5f70a59d5e124d4a1a747769e0d27431861877860ca31deaad41b09726ca71"
+ },
+ "imagePullSecretList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-cloudflared",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "networkPolicy": [],
+ "persistenceList": [],
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": false
+ }
+ },
+ "release_name": "cloudflared",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 0
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "PUID": 568,
+ "UMASK": "0022",
+ "advanced": false,
+ "runAsGroup": 0,
+ "runAsUser": 0
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "enabled": false,
+ "ports": {
+ "main": {
+ "enabled": false
+ }
+ }
+ }
+ },
+ "serviceList": [],
+ "workload": {
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "args": [
+ "tunnel",
+ "--no-autoupdate",
+ "run"
+ ],
+ "env": {
+ "TUNNEL_TOKEN": "REDACTED"
+ },
+ "envList": [],
+ "extraArgs": [],
+ "probes": {
+ "liveness": {
+ "enabled": false
+ },
+ "readiness": {
+ "enabled": false
+ },
+ "startup": {
+ "enabled": false
+ }
+ }
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 14,
+ "namespace": "ix-cloudflared",
+ "chart_metadata": {
+ "name": "cloudflared",
+ "home": "https://truecharts.org/charts/stable/cloudflared",
+ "sources": [
+ "https://github.com/truecharts/charts/tree/master/charts/stable/cloudflared",
+ "https://hub.docker.com/r/cloudflare/cloudflared"
+ ],
+ "version": "11.1.4",
+ "description": "Client for Cloudflare Tunnel, a daemon that exposes private services through the Cloudflare edge.",
+ "keywords": [
+ "cloudflared",
+ "networking"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/cloudflared.webp",
+ "apiVersion": "v2",
+ "appVersion": "2024.5.0",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "network",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "11.1.4"
+ },
+ "id": "cloudflared",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/cloudflared",
+ "dataset": "Apps/ix-applications/releases/cloudflared",
+ "status": "ACTIVE",
+ "used_ports": [],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "2024.5.0_11.1.4",
+ "human_latest_version": "2024.5.0_11.1.4",
+ "container_images_update_available": false,
+ "portals": {}
+}
diff --git a/clustertool/testdata/truenas_exports/Fileflows.json b/clustertool/testdata/truenas_exports/Fileflows.json
new file mode 100644
index 0000000000000..f267a3f1c0085
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/Fileflows.json
@@ -0,0 +1,349 @@
+{
+ "name": "fileflows",
+ "info": {
+ "first_deployed": "2024-04-24T17:25:05.171195503+10:00",
+ "last_deployed": "2024-07-03T20:22:26.731724743+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing fileflows by TrueCharts.\n\n\n## Connecting externally\nYou can use this Chart by opening one of the following links in your browser:\n- host: $node_ip\npath: /\nport: \"10242\"\nportalName: open\nprotocol: http\nurl: http://$node_ip:10242/\nuseNodeIP: true\n## Sources for fileflows\n- https://github.com/revenz/FileFlows\n- https://github.com/truecharts/charts/tree/master/charts/stable/fileflows\n- https://hub.docker.com/r/revenz/fileflows\n\nSee more for **fileflows** at (https://truecharts.org/charts/stable/fileflows)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-fileflows",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-fileflows",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "revenz/fileflows",
+ "tag": "24.05@sha256:a7af698a4816833261c0f92583d4eb48efc70705b8456402364b44048d4541fb"
+ },
+ "imagePullSecretList": [],
+ "ingress": {
+ "main": {
+ "enabled": false
+ }
+ },
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-fileflows",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "networkPolicy": [],
+ "persistence": {
+ "data": {
+ "enabled": true,
+ "mountPath": "/app/Data",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ },
+ "logs": {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Apps/vApps/FileFlows",
+ "mountPath": "/app/Logs",
+ "readOnly": false,
+ "type": "hostPath"
+ },
+ "media": {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Apps/vApps/FileFlows",
+ "mountPath": "/media",
+ "readOnly": false,
+ "type": "hostPath"
+ },
+ "temp": {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Apps/vApps/FileFlows",
+ "mountPath": "/temp",
+ "readOnly": false,
+ "type": "hostPath"
+ }
+ },
+ "persistenceList": [
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Main/Media/Movie",
+ "mountPath": "/Media/Movie",
+ "readOnly": false,
+ "type": "hostPath"
+ },
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Main/Media/Shows",
+ "mountPath": "/Media/Shows",
+ "readOnly": false,
+ "type": "hostPath"
+ },
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Main/Media/Test",
+ "mountPath": "/Media/Test",
+ "readOnly": false,
+ "type": "hostPath"
+ }
+ ],
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": true
+ }
+ },
+ "release_name": "fileflows",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 1
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "PUID": 0,
+ "UMASK": "0022",
+ "advanced": false,
+ "allowPrivilegeEscalation": true,
+ "privileged": true,
+ "readOnlyRootFilesystem": false,
+ "runAsGroup": 0,
+ "runAsNonRoot": false,
+ "runAsUser": 0
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "main": {
+ "port": 10242,
+ "targetPort": 5000
+ }
+ },
+ "type": "LoadBalancer"
+ }
+ },
+ "serviceList": [],
+ "workload": {
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "env": {},
+ "envList": [],
+ "extraArgs": [],
+ "probes": {
+ "liveness": {
+ "path": "/",
+ "type": "http"
+ },
+ "readiness": {
+ "path": "/",
+ "type": "http"
+ },
+ "startup": {
+ "path": "/",
+ "type": "http"
+ }
+ }
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 22,
+ "namespace": "ix-fileflows",
+ "chart_metadata": {
+ "name": "fileflows",
+ "home": "https://truecharts.org/charts/stable/fileflows",
+ "sources": [
+ "https://github.com/revenz/FileFlows",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/fileflows",
+ "https://hub.docker.com/r/revenz/fileflows"
+ ],
+ "version": "11.1.7",
+ "description": "An application that lets you automatically process files through a simple rule flow.",
+ "keywords": [
+ "fileflows"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/fileflows.webp",
+ "apiVersion": "v2",
+ "appVersion": "24.05.0",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "media",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "11.1.7"
+ },
+ "id": "fileflows",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/fileflows",
+ "dataset": "Apps/ix-applications/releases/fileflows",
+ "status": "ACTIVE",
+ "used_ports": [
+ {
+ "port": 10242,
+ "protocol": "TCP"
+ }
+ ],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "24.05.0_11.1.7",
+ "human_latest_version": "24.05.0_11.1.7",
+ "container_images_update_available": false,
+ "portals": {
+ "open": [
+ "http://10.0.0.20:10242/"
+ ]
+ }
+}
diff --git a/clustertool/testdata/truenas_exports/flaresolverr.json b/clustertool/testdata/truenas_exports/flaresolverr.json
new file mode 100644
index 0000000000000..6d52416dcdbf3
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/flaresolverr.json
@@ -0,0 +1,277 @@
+{
+ "name": "flaresolverr",
+ "info": {
+ "first_deployed": "2024-04-24T17:26:30.530984507+10:00",
+ "last_deployed": "2024-07-03T20:23:17.037008712+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing flaresolverr by TrueCharts.\n\n\n## Sources for flaresolverr\n- https://ghcr.io/flaresolverr/flaresolverr\n- https://github.com/FlareSolverr/FlareSolverr\n- https://github.com/truecharts/charts/tree/master/charts/stable/flaresolverr\n\nSee more for **flaresolverr** at (https://truecharts.org/charts/stable/flaresolverr)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-flaresolverr",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-flaresolverr",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/flaresolverr/flaresolverr",
+ "tag": "v3.3.19@sha256:0bdf9ed48f3c54c998bc160be46244ce3a88a7783b6cfd31eec9c1667786152f"
+ },
+ "imagePullSecretList": [],
+ "ingress": {
+ "main": {
+ "enabled": false
+ }
+ },
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-flaresolverr",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "networkPolicy": [],
+ "persistence": {
+ "config": {
+ "enabled": true,
+ "mountPath": "/config",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ }
+ },
+ "persistenceList": [],
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": false
+ }
+ },
+ "release_name": "flaresolverr",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 0
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "PUID": 568,
+ "UMASK": "0022",
+ "advanced": false,
+ "readOnlyRootFilesystem": false,
+ "runAsGroup": 0,
+ "runAsNonRoot": false,
+ "runAsUser": 0
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "main": {
+ "port": 8191
+ }
+ },
+ "type": "LoadBalancer"
+ }
+ },
+ "serviceList": [],
+ "workload": {
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "env": {
+ "BROWSER_TIMEOUT": 40000,
+ "CAPTCHA_SOLVER": "none",
+ "HEADLESS": true,
+ "HOST": "0.0.0.0",
+ "LOG_HTML": false,
+ "LOG_LEVEL": "info",
+ "PORT": "{{ .Values.service.main.ports.main.port }}",
+ "TEST_URL": "https://www.google.com.au"
+ },
+ "envList": [],
+ "extraArgs": []
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 14,
+ "namespace": "ix-flaresolverr",
+ "chart_metadata": {
+ "name": "flaresolverr",
+ "home": "https://truecharts.org/charts/stable/flaresolverr",
+ "sources": [
+ "https://ghcr.io/flaresolverr/flaresolverr",
+ "https://github.com/FlareSolverr/FlareSolverr",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/flaresolverr"
+ ],
+ "version": "14.0.10",
+ "description": "FlareSolverr is a proxy server to bypass Cloudflare protection",
+ "keywords": [
+ "flaresolverr",
+ "proxy"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/flaresolverr.webp",
+ "apiVersion": "v2",
+ "appVersion": "3.3.19",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "media",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "14.0.10"
+ },
+ "id": "flaresolverr",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/flaresolverr",
+ "dataset": "Apps/ix-applications/releases/flaresolverr",
+ "status": "ACTIVE",
+ "used_ports": [
+ {
+ "port": 8191,
+ "protocol": "TCP"
+ }
+ ],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "3.3.19_14.0.10",
+ "human_latest_version": "3.3.19_14.0.10",
+ "container_images_update_available": false,
+ "portals": {}
+}
diff --git a/clustertool/testdata/truenas_exports/homepage.json b/clustertool/testdata/truenas_exports/homepage.json
new file mode 100644
index 0000000000000..7319ca9e11cf9
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/homepage.json
@@ -0,0 +1,524 @@
+{
+ "name": "homepage",
+ "info": {
+ "first_deployed": "2024-04-24T17:28:58.760945606+10:00",
+ "last_deployed": "2024-07-03T20:24:03.895313943+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing homepage by TrueCharts.\n\n\n## Connecting externally\nYou can use this Chart by opening one of the following links in your browser:\n- host: $node_ip\npath: /\nport: \"10352\"\nportalName: open\nprotocol: http\nurl: http://$node_ip:10352/\nuseNodeIP: true\n## Sources for homepage\n- https://ghcr.io/gethomepage/homepage\n- https://github.com/benphelps/homepage\n- https://github.com/truecharts/charts/tree/master/charts/stable/homepage\n\nSee more for **homepage** at (https://truecharts.org/charts/stable/homepage)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "configmap": {
+ "config": {
+ "data": {
+ "bookmarks.yaml": "---\n# For configuration options and examples, please see:\n# https://gethomepage.dev/latest/configs/bookmarks\n\n- Developer:\n - Github:\n - abbr: GH\n href: https://github.com/\n\n- Social:\n - Reddit:\n - abbr: RE\n href: https://reddit.com/\n\n- Entertainment:\n - YouTube:\n - abbr: YT\n href: https://youtube.com/\n\n- TrueCharts:\n - TrueCharts:\n - abbr: TC\n icon: https://truecharts.org/svg/favicon.svg\n href: https://truecharts.org\n description: \"TrueCharts Website\"\n - Github:\n - abbr: GH\n icon: https://github.com/fluidicon.png\n href: https://github.com/truecharts\n description: \"TrueCharts GitHub\"\n - Open Collective:\n - abbr: TC\n icon: https://opencollective.com/favicon.ico\n href: https://opencollective.com/truecharts\n description: \"TrueCharts Open Collective\"\n - Discord:\n - abbr: DC\n icon: https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://discord.com&size=32\n href: https://truecharts.org/s/discord\n description: \"TrueCharts Discord\"\n",
+ "custom.css": "",
+ "custom.js": "",
+ "kubernetes.yaml": "mode: cluster\n",
+ "services.yaml": "---\n# For configuration options and examples, please see:\n# https://gethomepage.dev/latest/configs/services\n\n- Arr:\n - My First Service:\n href: http://localhost/\n description: Homepage is awesome\n\n- Media:\n - My Second Service:\n href: http://localhost/\n description: Homepage is the best\n\n- Infra:\n - My Third Service:\n href: http://localhost/\n description: Homepage is 😎\n",
+ "settings.yaml": "---\n# For configuration options and examples, please see:\n# https://gethomepage.dev/latest/configs/settings\n\nproviders:\n openweathermap: openweathermapapikey\n weatherapi: weatherapiapikey\n",
+ "widgets.yaml": "---\n# For configuration options and examples, please see:\n# https://gethomepage.dev/latest/configs/widgets\n\n- resources:\n cpu: true\n memory: true\n disk: /\n\n- search:\n provider: duckduckgo\n target: _blank\n"
+ },
+ "enabled": true
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-homepage",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "forceConfigFromValues": false,
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-homepage",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/gethomepage/homepage",
+ "tag": "v0.8.13@sha256:43a3ee88abe3b37c64bc52ea93da01c3dcb4a332a953bcd7f438c8d7328d3947"
+ },
+ "imagePullSecretList": [],
+ "ingress": {
+ "main": {
+ "enabled": false
+ }
+ },
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-homepage",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "networkPolicy": [],
+ "persistence": {
+ "bookmarks-config": {
+ "enabled": "{{ not .Values.forceConfigFromValues }}",
+ "mountPath": "/dummy-config/bookmarks.yaml",
+ "objectName": "config",
+ "readOnly": true,
+ "subPath": "bookmarks.yaml",
+ "targetSelector": {
+ "main": {
+ "init-config": {}
+ }
+ },
+ "type": "configmap"
+ },
+ "config": {
+ "enabled": true,
+ "mountPath": "/app/config",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "targetSelector": {
+ "main": {
+ "init-config": {},
+ "main": {}
+ }
+ },
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ },
+ "custom-css-config": {
+ "enabled": "{{ not .Values.forceConfigFromValues }}",
+ "mountPath": "/dummy-config/custom.css",
+ "objectName": "config",
+ "readOnly": true,
+ "subPath": "custom.css",
+ "targetSelector": {
+ "main": {
+ "init-config": {}
+ }
+ },
+ "type": "configmap"
+ },
+ "custom-js-config": {
+ "enabled": "{{ not .Values.forceConfigFromValues }}",
+ "mountPath": "/dummy-config/custom.js",
+ "objectName": "config",
+ "readOnly": true,
+ "subPath": "custom.js",
+ "targetSelector": {
+ "main": {
+ "init-config": {}
+ }
+ },
+ "type": "configmap"
+ },
+ "force-bookmarks-config": {
+ "enabled": "{{ .Values.forceConfigFromValues }}",
+ "mountPath": "/app/config/bookmarks.yaml",
+ "objectName": "config",
+ "readOnly": true,
+ "subPath": "bookmarks.yaml",
+ "type": "configmap"
+ },
+ "force-custom-css-config": {
+ "enabled": "{{ .Values.forceConfigFromValues }}",
+ "mountPath": "/app/config/custom.css",
+ "objectName": "config",
+ "readOnly": true,
+ "subPath": "custom.css",
+ "type": "configmap"
+ },
+ "force-custom-js-config": {
+ "enabled": "{{ .Values.forceConfigFromValues }}",
+ "mountPath": "/app/config/custom.js",
+ "objectName": "config",
+ "readOnly": true,
+ "subPath": "custom.js",
+ "type": "configmap"
+ },
+ "force-services-config": {
+ "enabled": "{{ .Values.forceConfigFromValues }}",
+ "mountPath": "/app/config/services.yaml",
+ "objectName": "config",
+ "readOnly": true,
+ "subPath": "services.yaml",
+ "type": "configmap"
+ },
+ "force-settings-config": {
+ "enabled": "{{ .Values.forceConfigFromValues }}",
+ "mountPath": "/app/config/settings.yaml",
+ "objectName": "config",
+ "readOnly": true,
+ "subPath": "settings.yaml",
+ "type": "configmap"
+ },
+ "force-widgets-config": {
+ "enabled": "{{ .Values.forceConfigFromValues }}",
+ "mountPath": "/app/config/widgets.yaml",
+ "objectName": "config",
+ "readOnly": true,
+ "subPath": "widgets.yaml",
+ "type": "configmap"
+ },
+ "kubernetes-config": {
+ "enabled": true,
+ "mountPath": "/app/config/kubernetes.yaml",
+ "objectName": "config",
+ "readOnly": true,
+ "subPath": "kubernetes.yaml",
+ "type": "configmap"
+ },
+ "services-config": {
+ "enabled": "{{ not .Values.forceConfigFromValues }}",
+ "mountPath": "/dummy-config/services.yaml",
+ "objectName": "config",
+ "readOnly": true,
+ "subPath": "services.yaml",
+ "targetSelector": {
+ "main": {
+ "init-config": {}
+ }
+ },
+ "type": "configmap"
+ },
+ "settings-config": {
+ "enabled": "{{ not .Values.forceConfigFromValues }}",
+ "mountPath": "/dummy-config/settings.yaml",
+ "objectName": "config",
+ "readOnly": true,
+ "subPath": "settings.yaml",
+ "targetSelector": {
+ "main": {
+ "init-config": {}
+ }
+ },
+ "type": "configmap"
+ },
+ "widgets-config": {
+ "enabled": "{{ not .Values.forceConfigFromValues }}",
+ "mountPath": "/dummy-config/widgets.yaml",
+ "objectName": "config",
+ "readOnly": true,
+ "subPath": "widgets.yaml",
+ "targetSelector": {
+ "main": {
+ "init-config": {}
+ }
+ },
+ "type": "configmap"
+ }
+ },
+ "persistenceList": [],
+ "podOptions": {
+ "dnsConfig": {
+ "options": [
+ {
+ "name": "ndots",
+ "value": "3"
+ }
+ ]
+ },
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": true
+ }
+ },
+ "rbac": {
+ "main": {
+ "clusterWide": true,
+ "enabled": true,
+ "primary": true,
+ "rules": [
+ {
+ "apiGroups": [
+ ""
+ ],
+ "resources": [
+ "namespaces",
+ "pods",
+ "nodes"
+ ],
+ "verbs": [
+ "get",
+ "list"
+ ]
+ },
+ {
+ "apiGroups": [
+ "extensions",
+ "networking.k8s.io"
+ ],
+ "resources": [
+ "ingresses"
+ ],
+ "verbs": [
+ "get",
+ "list"
+ ]
+ },
+ {
+ "apiGroups": [
+ "traefik.containo.us",
+ "traefik.io"
+ ],
+ "resources": [
+ "ingressroutes"
+ ],
+ "verbs": [
+ "get",
+ "list"
+ ]
+ },
+ {
+ "apiGroups": [
+ "metrics.k8s.io"
+ ],
+ "resources": [
+ "nodes",
+ "pods"
+ ],
+ "verbs": [
+ "get",
+ "list"
+ ]
+ },
+ {
+ "apiGroups": [
+ "apiextensions.k8s.io"
+ ],
+ "resources": [
+ "customresourcedefinitions/status"
+ ],
+ "verbs": [
+ "get"
+ ]
+ }
+ ]
+ }
+ },
+ "release_name": "homepage",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 0
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "UMASK": "0022",
+ "advanced": false,
+ "runAsGroup": 568,
+ "runAsUser": 568
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "main": {
+ "port": 10352,
+ "protocol": "http",
+ "targetPort": 3000
+ }
+ },
+ "type": "LoadBalancer"
+ }
+ },
+ "serviceAccount": {
+ "main": {
+ "enabled": true,
+ "primary": true
+ }
+ },
+ "serviceList": [],
+ "workload": {
+ "main": {
+ "podSpec": {
+ "automountServiceAccountToken": true,
+ "containers": {
+ "main": {
+ "advanced": false,
+ "envList": [],
+ "extraArgs": []
+ }
+ },
+ "initContainers": {
+ "init-config": {
+ "command": [
+ "/bin/sh",
+ "-c",
+ "mkdir -p /app/config\nif [ ! -f /app/config/bookmarks.yaml ]; then\n echo \"Bookmarks file not found, copying dummy...\"\n cp /dummy-config/bookmarks.yaml /app/config/bookmarks.yaml\n echo \"Config file copied, you can now edit it at /app/config/bookmarks.yaml\"\nfi\nif [ ! -f /app/config/services.yaml ]; then\n echo \"services file not found, copying dummy...\"\n cp /dummy-config/services.yaml /app/config/services.yaml\n echo \"Config file copied, you can now edit it at /app/config/services.yaml\"\nfi\nif [ ! -f /app/config/settings.yaml ]; then\n echo \"settings file not found, copying dummy...\"\n cp /dummy-config/settings.yaml /app/config/settings.yaml\n echo \"Config file copied, you can now edit it at /app/config/settings.yaml\"\nfi\nif [ ! -f /app/config/widgets.yaml ]; then\n echo \"widgets file not found, copying dummy...\"\n cp /dummy-config/widgets.yaml /app/config/widgets.yaml\n echo \"Config file copied, you can now edit it at /app/config/widgets.yaml\"\nfi\nif [ ! -f /app/config/custom.css ]; then\n echo \"custom.css file not found, copying dummy...\"\n cp /dummy-config/custom.css /app/config/custom.css\n echo \"Config file copied, you can now edit it at /app/config/custom.css\"\nfi\nif [ ! -f /app/config/custom.js ]; then\n echo \"custom.js file not found, copying dummy...\"\n cp /dummy-config/custom.js /app/config/custom.js\n echo \"Config file copied, you can now edit it at /app/config/custom.js\"\nfi\n"
+ ],
+ "enabled": true,
+ "imageSelector": "alpineImage",
+ "type": "init"
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 16,
+ "namespace": "ix-homepage",
+ "chart_metadata": {
+ "name": "homepage",
+ "home": "https://truecharts.org/charts/stable/homepage",
+ "sources": [
+ "https://ghcr.io/gethomepage/homepage",
+ "https://github.com/benphelps/homepage",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/homepage"
+ ],
+ "version": "8.0.9",
+ "description": "A highly customizable homepage",
+ "keywords": [
+ "homepage"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/homepage.webp",
+ "apiVersion": "v2",
+ "appVersion": "0.8.13",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "dashboard",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "8.0.9"
+ },
+ "id": "homepage",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/homepage",
+ "dataset": "Apps/ix-applications/releases/homepage",
+ "status": "ACTIVE",
+ "used_ports": [
+ {
+ "port": 10352,
+ "protocol": "TCP"
+ }
+ ],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "0.8.13_8.0.9",
+ "human_latest_version": "0.8.13_8.0.9",
+ "container_images_update_available": false,
+ "portals": {
+ "open": [
+ "http://10.0.0.20:10352/"
+ ]
+ }
+}
diff --git a/clustertool/testdata/truenas_exports/kometa.json b/clustertool/testdata/truenas_exports/kometa.json
new file mode 100644
index 0000000000000..3fe95ff140f1f
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/kometa.json
@@ -0,0 +1,306 @@
+{
+ "name": "kometa",
+ "info": {
+ "first_deployed": "2024-06-11T01:43:24.095293414+10:00",
+ "last_deployed": "2024-07-03T20:25:43.695927391+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing kometa by TrueCharts.\n\n\n## Sources for kometa\n- https://github.com/kometa-team/kometa\n- https://github.com/truecharts/charts/tree/master/charts/stable/kometa\n- https://hub.docker.com/r/kometateam/kometa\n\nSee more for **kometa** at (https://truecharts.org/charts/stable/kometa)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-kometa",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-kometa",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "kometateam/kometa",
+ "tag": "v2.0.1@sha256:c1f9d1ef49f976377a4bbbe9018bfed9a7057ccf427d10759a8ad975d4505300"
+ },
+ "imagePullSecretList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-kometa",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "kometa": {
+ "no_countdown": true,
+ "plex_token": "fFXAMH1HkGYLhz9E5Xfi",
+ "plex_url": "plexmediaserver.ix-plexmediaserver.svc.cluster.local",
+ "run": false,
+ "time": [
+ "00:00"
+ ],
+ "times": [
+ "05:00"
+ ]
+ },
+ "networkPolicy": [],
+ "persistence": {
+ "config": {
+ "enabled": true,
+ "mountPath": "/config",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "targetSelectAll": true,
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ }
+ },
+ "persistenceList": [
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Apps/vApps/PlexMetaManager/assets",
+ "mountPath": "/assets",
+ "readOnly": false,
+ "type": "hostPath"
+ }
+ ],
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": false
+ }
+ },
+ "release_name": "kometa",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 0
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "PUID": 568,
+ "UMASK": "0022",
+ "advanced": false,
+ "readOnlyRootFilesystem": false,
+ "runAsGroup": 0,
+ "runAsNonRoot": false,
+ "runAsUser": 0
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "enabled": false,
+ "ports": {
+ "main": {
+ "enabled": false
+ }
+ }
+ }
+ },
+ "serviceList": [],
+ "workload": {
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "envFrom": [
+ {
+ "secretRef": {
+ "name": "secret"
+ }
+ }
+ ],
+ "envList": [],
+ "extraArgs": [],
+ "probes": {
+ "liveness": {
+ "enabled": false
+ },
+ "readiness": {
+ "enabled": false
+ },
+ "startup": {
+ "enabled": false
+ }
+ }
+ }
+ },
+ "initContainers": {
+ "create-init-config-file": {
+ "args": [
+ "echo \"Creating config.yml file...\"\nif [ -f /config/config.yml ]; then\n echo \"Config file exists! Skipping...\"\nelse\n echo \"Config file is missing, getting a new one!\"\n curl -fLvo /config/config.yml https://raw.githubusercontent.com/kometa-team/kometa/master/config/config.yml.template || (echo \"Downloading config file, FAILED...\" && exit 1)\nfi\n"
+ ],
+ "command": [
+ "/bin/sh",
+ "-c"
+ ],
+ "enabled": true,
+ "imageSelector": "image",
+ "type": "init"
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 9,
+ "namespace": "ix-kometa",
+ "chart_metadata": {
+ "name": "kometa",
+ "home": "https://truecharts.org/charts/stable/kometa",
+ "sources": [
+ "https://github.com/kometa-team/kometa",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/kometa",
+ "https://hub.docker.com/r/kometateam/kometa"
+ ],
+ "version": "2.0.6",
+ "description": "Python script to update metadata and automatically build collections.",
+ "keywords": [
+ "kometa"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/kometa.webp",
+ "apiVersion": "v2",
+ "appVersion": "2.0.1",
+ "annotations": {
+ "truecharts.org/category": "media",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "2.0.6"
+ },
+ "id": "kometa",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/kometa",
+ "dataset": "Apps/ix-applications/releases/kometa",
+ "status": "ACTIVE",
+ "used_ports": [],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "2.0.1_2.0.6",
+ "human_latest_version": "2.0.1_2.0.6",
+ "container_images_update_available": false,
+ "portals": {}
+}
diff --git a/clustertool/testdata/truenas_exports/openebs.json b/clustertool/testdata/truenas_exports/openebs.json
new file mode 100644
index 0000000000000..1263823bcac29
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/openebs.json
@@ -0,0 +1,280 @@
+{
+ "name": "openebs",
+ "info": {
+ "first_deployed": "2024-04-24T14:51:21.911963321+10:00",
+ "last_deployed": "2024-07-03T20:00:43.101610328+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing openebs by TrueCharts.\n\n\n## Sources for openebs\n- https://github.com/cert-manager\n- https://cert-manager.io/\n- https://github.com/truecharts/charts/tree/master/charts/system/openebs\n- https://github.com/truecharts/containers/tree/master/apps/scratch\n\nSee more for **openebs** at (https://truecharts.org/charts/system/openebs)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-openebs",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "tccr.io/tccr/scratch",
+ "tag": "latest"
+ },
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-openebs",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "localpv-provisioner": {
+ "analytics": {
+ "enabled": true,
+ "pingInterval": "24h"
+ },
+ "enabled": false,
+ "fullnameOverride": "",
+ "helperPod": {
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "registry": "",
+ "repository": "openebs/linux-utils",
+ "tag": "4.0.0"
+ }
+ },
+ "hostpathClass": {
+ "basePath": "",
+ "enabled": false,
+ "ext4Quota": {
+ "enabled": false,
+ "hardLimitGrace": "0%",
+ "softLimitGrace": "0%"
+ },
+ "isDefaultClass": false,
+ "name": "openebs-hostpath",
+ "nodeAffinityLabels": [],
+ "reclaimPolicy": "Delete",
+ "xfsQuota": {
+ "enabled": false,
+ "hardLimitGrace": "0%",
+ "softLimitGrace": "0%"
+ }
+ },
+ "imagePullSecrets": null,
+ "localpv": {
+ "affinity": {},
+ "annotations": {},
+ "basePath": "/var/openebs/local",
+ "enableLeaderElection": true,
+ "enabled": true,
+ "healthCheck": {
+ "initialDelaySeconds": 30,
+ "periodSeconds": 60
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "registry": null,
+ "repository": "openebs/provisioner-localpv",
+ "tag": "4.0.0"
+ },
+ "name": "localpv-provisioner",
+ "nodeSelector": {},
+ "podAnnotations": {},
+ "podLabels": {
+ "name": "openebs-localpv-provisioner"
+ },
+ "privileged": true,
+ "replicas": 1,
+ "resources": null,
+ "securityContext": {},
+ "tolerations": [],
+ "updateStrategy": {
+ "type": "RollingUpdate"
+ }
+ },
+ "nameOverride": "",
+ "podSecurityContext": {},
+ "rbac": {
+ "create": true
+ },
+ "serviceAccount": {
+ "annotations": {},
+ "create": true,
+ "name": null
+ }
+ },
+ "lvm-localpv": {
+ "crd": {
+ "enableInstall": false,
+ "volumeSnapshot": false
+ },
+ "enabled": false
+ },
+ "portal": {
+ "open": {
+ "enabled": false
+ }
+ },
+ "release_name": "openebs",
+ "service": {
+ "main": {
+ "enabled": false,
+ "ports": {
+ "main": {
+ "enabled": false
+ }
+ }
+ }
+ },
+ "storageClass": {
+ "zfs-main": {
+ "allowVolumeExpansion": true,
+ "enabled": true,
+ "isDefault": true,
+ "parameters": {
+ "compression": "zstd-6",
+ "dedup": "off",
+ "encryption": "off",
+ "fstype": "zfs",
+ "poolname": "Apps/pvcApps",
+ "recordsize": "64k",
+ "shared": "yes",
+ "thinprovision": "yes"
+ },
+ "provisioner": "zfs.csi.openebs.io",
+ "reclaimPolicy": "Delete"
+ }
+ },
+ "volumeSnapshotClass": {
+ "zfspv": {
+ "deletionPolicy": "Delete",
+ "driver": "zfs.csi.openebs.io",
+ "enabled": true,
+ "isDefault": true
+ }
+ },
+ "workload": {
+ "main": {
+ "enabled": false
+ }
+ },
+ "zfs-localpv": {
+ "crd": {
+ "enableInstall": false,
+ "enabled": false,
+ "volumeSnapshot": false
+ },
+ "crds": {
+ "enableInstall": false,
+ "enabled": false,
+ "volumeSnapshot": false
+ },
+ "enabled": true
+ }
+ },
+ "version": 19,
+ "namespace": "ix-openebs",
+ "chart_metadata": {
+ "name": "openebs",
+ "home": "https://truecharts.org/charts/system/openebs",
+ "sources": [
+ "https://github.com/cert-manager",
+ "https://cert-manager.io/",
+ "https://github.com/truecharts/charts/tree/master/charts/system/openebs",
+ "https://github.com/truecharts/containers/tree/master/apps/scratch"
+ ],
+ "version": "6.0.1",
+ "description": "OpenEBS is a umbrella chart for multiple container storage provisioners",
+ "keywords": [
+ "openebs",
+ "backup"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/openebs.webp",
+ "apiVersion": "v2",
+ "appVersion": "latest",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "CSI",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "system"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.0",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ },
+ {
+ "name": "zfs-localpv",
+ "version": "2.5.1",
+ "repository": "https://openebs.github.io/zfs-localpv",
+ "condition": "zfs-localpv.enabled",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "6.0.1"
+ },
+ "id": "openebs",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "system",
+ "path": "/mnt/Apps/ix-applications/releases/openebs",
+ "dataset": "Apps/ix-applications/releases/openebs",
+ "status": "ACTIVE",
+ "used_ports": [],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "latest_6.0.1",
+ "human_latest_version": "latest_6.0.1",
+ "container_images_update_available": false,
+ "portals": {}
+}
diff --git a/clustertool/testdata/truenas_exports/overseerr.json b/clustertool/testdata/truenas_exports/overseerr.json
new file mode 100644
index 0000000000000..34f4000050532
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/overseerr.json
@@ -0,0 +1,275 @@
+{
+ "name": "overseerr",
+ "info": {
+ "first_deployed": "2024-04-24T17:33:01.184814985+10:00",
+ "last_deployed": "2024-07-03T20:26:43.975083372+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing overseerr by TrueCharts.\n\n\n## Connecting externally\nYou can use this Chart by opening one of the following links in your browser:\n- host: $node_ip\npath: /\nport: \"5055\"\nportalName: open\nprotocol: http\nurl: http://$node_ip:5055/\nuseNodeIP: true\n## Sources for overseerr\n- https://ghcr.io/sct/overseerr\n- https://github.com/sct/overseerr\n- https://github.com/truecharts/charts/tree/master/charts/stable/overseerr\n\nSee more for **overseerr** at (https://truecharts.org/charts/stable/overseerr)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Melbourne",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-overseerr",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-overseerr",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/sct/overseerr",
+ "tag": "1.33.2@sha256:714ea6db2bc007a2262d112bef7eec74972eb33d9c72bddb9cbd98b8742de950"
+ },
+ "imagePullSecretList": [],
+ "ingress": {
+ "main": {
+ "enabled": false
+ }
+ },
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-overseerr",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "networkPolicy": [],
+ "persistence": {
+ "config": {
+ "enabled": true,
+ "mountPath": "/app/config",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ }
+ },
+ "persistenceList": [],
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": true
+ }
+ },
+ "release_name": "overseerr",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 0
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "UMASK": "0022",
+ "advanced": false,
+ "readOnlyRootFilesystem": false,
+ "runAsGroup": 568,
+ "runAsUser": 568
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "main": {
+ "port": 5055,
+ "targetPort": 5055
+ }
+ },
+ "type": "LoadBalancer"
+ }
+ },
+ "serviceList": [],
+ "workload": {
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "env": {
+ "LOG_LEVEL": "info"
+ },
+ "envList": [],
+ "extraArgs": []
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 15,
+ "namespace": "ix-overseerr",
+ "chart_metadata": {
+ "name": "overseerr",
+ "home": "https://truecharts.org/charts/stable/overseerr",
+ "sources": [
+ "https://ghcr.io/sct/overseerr",
+ "https://github.com/sct/overseerr",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/overseerr"
+ ],
+ "version": "14.0.9",
+ "description": "Overseerr is a free and open source software application for managing requests for your media library. It integrates with your existing services such as Sonarr, Radarr and Plex!",
+ "keywords": [
+ "overseerr",
+ "plex",
+ "sonarr",
+ "radarr"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/overseerr.webp",
+ "apiVersion": "v2",
+ "appVersion": "1.33.2",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "media",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "14.0.9"
+ },
+ "id": "overseerr",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/overseerr",
+ "dataset": "Apps/ix-applications/releases/overseerr",
+ "status": "ACTIVE",
+ "used_ports": [
+ {
+ "port": 5055,
+ "protocol": "TCP"
+ }
+ ],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "1.33.2_14.0.9",
+ "human_latest_version": "1.33.2_14.0.9",
+ "container_images_update_available": false,
+ "portals": {
+ "open": [
+ "http://10.0.0.20:5055/"
+ ]
+ }
+}
diff --git a/clustertool/testdata/truenas_exports/plexmediaserver.json b/clustertool/testdata/truenas_exports/plexmediaserver.json
new file mode 100644
index 0000000000000..abf17aef29ec1
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/plexmediaserver.json
@@ -0,0 +1,324 @@
+{
+ "name": "plexmediaserver",
+ "info": {
+ "first_deployed": "2024-04-24T17:36:15.914417566+10:00",
+ "last_deployed": "2024-07-03T20:27:30.168004041+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing plex by TrueCharts.\n\n\n## Connecting externally\nYou can use this Chart by opening one of the following links in your browser:\n- host: $node_ip\npath: /\nport: \"32400\"\nportalName: open\nprotocol: http\nurl: http://$node_ip:32400/\nuseNodeIP: true\n## Sources for plex\n- https://ghcr.io/onedr0p/plex\n- https://github.com/k8s-at-home/container-images/pkgs/container/plex\n- https://github.com/truecharts/charts/tree/master/charts/stable/plex\n\nSee more for **plex** at (https://truecharts.org/charts/stable/plex)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-plexmediaserver",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-plexmediaserver",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/onedr0p/plex",
+ "tag": "1.40.2.8395-c67dce28e@sha256:3861cc940ecdf97b773fa24d826407cea86559d0f26366d7acd10cef1704f46c"
+ },
+ "imagePullSecretList": [],
+ "ingress": {
+ "main": {
+ "enabled": false
+ }
+ },
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-plexmediaserver",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "networkPolicy": [],
+ "persistence": {
+ "config": {
+ "enabled": true,
+ "mountPath": "/config",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ },
+ "transcode": {
+ "enabled": true,
+ "medium": "Memory",
+ "mountPath": "/transcode",
+ "targetSelectAll": true,
+ "type": "emptyDir"
+ }
+ },
+ "persistenceList": [
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Apps/vApps/PlexMediaServer/data",
+ "mountPath": "/data",
+ "readOnly": false,
+ "type": "hostPath"
+ },
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Main/Media",
+ "mountPath": "/Media",
+ "readOnly": false,
+ "type": "hostPath"
+ }
+ ],
+ "plex": {
+ "additionalAdvertiseURL": "",
+ "disableGDM": true,
+ "requireHTTPS": false,
+ "serverIP": "10.0.0.20"
+ },
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": true
+ }
+ },
+ "release_name": "plexmediaserver",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 1
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "UMASK": "0022",
+ "advanced": false,
+ "runAsGroup": 568,
+ "runAsUser": 568
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "main": {
+ "port": 32400,
+ "protocol": "http",
+ "targetPort": 32400
+ }
+ },
+ "type": "LoadBalancer"
+ }
+ },
+ "serviceList": [],
+ "workload": {
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "env": {
+ "ALLOWED_NETWORKS": "172.16.0.0/12,10.0.0.0/8,192.168.0.0/16",
+ "PLEX_CLAIM": "REDACTED",
+ "PLEX_PREFERENCE_GDM": "GdmEnabled={{ ternary \"0\" \"1\" .Values.plex.disableGDM }}",
+ "PLEX_PREFERENCE_SEC_CON": "secureConnections={{ ternary \"0\" \"1\" .Values.plex.requireHTTPS }}"
+ },
+ "envList": [],
+ "extraArgs": [],
+ "probes": {
+ "liveness": {
+ "enabled": true,
+ "path": "/identity"
+ },
+ "readiness": {
+ "enabled": true,
+ "path": "/identity"
+ },
+ "startup": {
+ "enabled": true,
+ "path": "/identity"
+ }
+ }
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 17,
+ "namespace": "ix-plexmediaserver",
+ "chart_metadata": {
+ "name": "plex",
+ "home": "https://truecharts.org/charts/stable/plex",
+ "sources": [
+ "https://ghcr.io/onedr0p/plex",
+ "https://github.com/k8s-at-home/container-images/pkgs/container/plex",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/plex"
+ ],
+ "version": "18.0.12",
+ "description": "Plex Media Server",
+ "keywords": [
+ "plex",
+ "plex-media-server"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/plex.webp",
+ "apiVersion": "v2",
+ "appVersion": "1.40.2.8395",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "media",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "18.0.12"
+ },
+ "id": "plexmediaserver",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/plexmediaserver",
+ "dataset": "Apps/ix-applications/releases/plexmediaserver",
+ "status": "ACTIVE",
+ "used_ports": [
+ {
+ "port": 32400,
+ "protocol": "TCP"
+ }
+ ],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "1.40.2.8395_18.0.12",
+ "human_latest_version": "1.40.2.8395_18.0.12",
+ "container_images_update_available": false,
+ "portals": {
+ "open": [
+ "http://10.0.0.20:32400/web"
+ ]
+ }
+}
diff --git a/clustertool/testdata/truenas_exports/prometheus-operator.json b/clustertool/testdata/truenas_exports/prometheus-operator.json
new file mode 100644
index 0000000000000..57681a32a7cf4
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/prometheus-operator.json
@@ -0,0 +1,387 @@
+{
+ "name": "prometheus-operator",
+ "info": {
+ "first_deployed": "2024-01-03T14:45:02.101999414+11:00",
+ "last_deployed": "2024-05-03T02:01:17.788662558+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing prometheus-operator by TrueCharts.\n\n\n## Sources for prometheus-operator\n- https://github.com/prometheus-operator\n- https://github.com/truecharts/charts/tree/master/charts/system/prometheus-operator\n- https://github.com/truecharts/containers/tree/master/apps/alpine\n\nSee more for **prometheus-operator** at (https://truecharts.org/charts/system/prometheus-operator)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "credentialsList": [],
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": false,
+ "isUpgrade": true,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPGRADE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-prometheus-operator",
+ "upgradeMetadata": {
+ "newChartVersion": "7.0.4",
+ "oldChartVersion": "7.0.3",
+ "preUpgradeRevision": 43
+ }
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "tccr.io/tccr/alpine",
+ "tag": "latest@sha256:ade0065e19edaa4f6903d464ee70605111a48394536deb94f31b661264704558"
+ },
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": false,
+ "isUpgrade": true,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPGRADE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-prometheus-operator",
+ "upgradeMetadata": {
+ "newChartVersion": "7.0.4",
+ "oldChartVersion": "7.0.3",
+ "preUpgradeRevision": 43
+ }
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "kps": {
+ "alertmanager": {
+ "enabled": false
+ },
+ "coreDns": {
+ "enabled": false
+ },
+ "crds": {
+ "enabled": true
+ },
+ "defaultRules": {
+ "create": false
+ },
+ "global": {
+ "rbac": {
+ "create": true
+ }
+ },
+ "grafana": {
+ "defaultDashboardsEnabled": false,
+ "enabled": false,
+ "forceDeployDashboards": false
+ },
+ "kubeApiServer": {
+ "enabled": false
+ },
+ "kubeControllerManager": {
+ "enabled": false
+ },
+ "kubeDns": {
+ "enabled": false
+ },
+ "kubeEtcd": {
+ "enabled": false
+ },
+ "kubeProxy": {
+ "enabled": false
+ },
+ "kubeScheduler": {
+ "enabled": false
+ },
+ "kubeStateMetrics": {
+ "enabled": false
+ },
+ "kubelet": {
+ "enabled": false
+ },
+ "kubernetesServiceMonitors": {
+ "enabled": false
+ },
+ "nodeExporter": {
+ "enabled": false
+ },
+ "prometheus": {
+ "enabled": false
+ },
+ "prometheus-windows-exporter": {
+ "prometheus": {
+ "monitor": {
+ "enabled": false
+ }
+ }
+ },
+ "prometheusOperator": {
+ "enabled": true
+ },
+ "thanosRuler": {
+ "enabled": false
+ },
+ "windowsMonitoring": {
+ "enabled": false
+ }
+ },
+ "operator": {
+ "register": true
+ },
+ "portal": {
+ "open": {
+ "enabled": false
+ }
+ },
+ "release_name": "prometheus-operator",
+ "service": {
+ "main": {
+ "enabled": false,
+ "ports": {
+ "main": {
+ "enabled": false
+ }
+ }
+ }
+ },
+ "workload": {
+ "main": {
+ "enabled": false
+ }
+ }
+ },
+ "hooks": [
+ {
+ "name": "prometheus-operator-kps-admission",
+ "kind": "ServiceAccount",
+ "path": "prometheus-operator/charts/kps/templates/prometheus-operator/admission-webhooks/job-patch/serviceaccount.yaml",
+ "manifest": "apiVersion: v1\nkind: ServiceAccount\nmetadata:\n name: prometheus-operator-kps-admission\n namespace: ix-prometheus-operator\n annotations:\n \"helm.sh/hook\": pre-install,pre-upgrade,post-install,post-upgrade\n \"helm.sh/hook-delete-policy\": before-hook-creation,hook-succeeded\n labels:\n app: kps-admission\n \n app.kubernetes.io/managed-by: Helm\n app.kubernetes.io/instance: prometheus-operator\n app.kubernetes.io/version: \"56.21.0\"\n app.kubernetes.io/part-of: kps\n chart: kps-56.21.0\n release: \"prometheus-operator\"\n heritage: \"Helm\"\n app.kubernetes.io/name: kps-prometheus-operator\n app.kubernetes.io/component: prometheus-operator-webhook",
+ "events": [
+ "pre-install",
+ "pre-upgrade",
+ "post-install",
+ "post-upgrade"
+ ],
+ "last_run": {
+ "started_at": "2024-05-03T02:01:43.428117866+10:00",
+ "completed_at": "2024-05-03T02:01:43.441540986+10:00",
+ "phase": "Succeeded"
+ },
+ "delete_policies": [
+ "before-hook-creation",
+ "hook-succeeded"
+ ]
+ },
+ {
+ "name": "prometheus-operator-kps-admission",
+ "kind": "ClusterRole",
+ "path": "prometheus-operator/charts/kps/templates/prometheus-operator/admission-webhooks/job-patch/clusterrole.yaml",
+ "manifest": "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n name: prometheus-operator-kps-admission\n annotations:\n \"helm.sh/hook\": pre-install,pre-upgrade,post-install,post-upgrade\n \"helm.sh/hook-delete-policy\": before-hook-creation,hook-succeeded\n labels:\n app: kps-admission\n \n app.kubernetes.io/managed-by: Helm\n app.kubernetes.io/instance: prometheus-operator\n app.kubernetes.io/version: \"56.21.0\"\n app.kubernetes.io/part-of: kps\n chart: kps-56.21.0\n release: \"prometheus-operator\"\n heritage: \"Helm\"\n app.kubernetes.io/name: kps-prometheus-operator\n app.kubernetes.io/component: prometheus-operator-webhook\nrules:\n - apiGroups:\n - admissionregistration.k8s.io\n resources:\n - validatingwebhookconfigurations\n - mutatingwebhookconfigurations\n verbs:\n - get\n - update",
+ "events": [
+ "pre-install",
+ "pre-upgrade",
+ "post-install",
+ "post-upgrade"
+ ],
+ "last_run": {
+ "started_at": "2024-05-03T02:01:43.468809289+10:00",
+ "completed_at": "2024-05-03T02:01:43.480540697+10:00",
+ "phase": "Succeeded"
+ },
+ "delete_policies": [
+ "before-hook-creation",
+ "hook-succeeded"
+ ]
+ },
+ {
+ "name": "prometheus-operator-kps-admission",
+ "kind": "ClusterRoleBinding",
+ "path": "prometheus-operator/charts/kps/templates/prometheus-operator/admission-webhooks/job-patch/clusterrolebinding.yaml",
+ "manifest": "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n name: prometheus-operator-kps-admission\n annotations:\n \"helm.sh/hook\": pre-install,pre-upgrade,post-install,post-upgrade\n \"helm.sh/hook-delete-policy\": before-hook-creation,hook-succeeded\n labels:\n app: kps-admission\n \n app.kubernetes.io/managed-by: Helm\n app.kubernetes.io/instance: prometheus-operator\n app.kubernetes.io/version: \"56.21.0\"\n app.kubernetes.io/part-of: kps\n chart: kps-56.21.0\n release: \"prometheus-operator\"\n heritage: \"Helm\"\n app.kubernetes.io/name: kps-prometheus-operator\n app.kubernetes.io/component: prometheus-operator-webhook\nroleRef:\n apiGroup: rbac.authorization.k8s.io\n kind: ClusterRole\n name: prometheus-operator-kps-admission\nsubjects:\n - kind: ServiceAccount\n name: prometheus-operator-kps-admission\n namespace: ix-prometheus-operator",
+ "events": [
+ "pre-install",
+ "pre-upgrade",
+ "post-install",
+ "post-upgrade"
+ ],
+ "last_run": {
+ "started_at": "2024-05-03T02:01:43.507854125+10:00",
+ "completed_at": "2024-05-03T02:01:43.51852047+10:00",
+ "phase": "Succeeded"
+ },
+ "delete_policies": [
+ "before-hook-creation",
+ "hook-succeeded"
+ ]
+ },
+ {
+ "name": "prometheus-operator-kps-admission",
+ "kind": "Role",
+ "path": "prometheus-operator/charts/kps/templates/prometheus-operator/admission-webhooks/job-patch/role.yaml",
+ "manifest": "apiVersion: rbac.authorization.k8s.io/v1\nkind: Role\nmetadata:\n name: prometheus-operator-kps-admission\n namespace: ix-prometheus-operator\n annotations:\n \"helm.sh/hook\": pre-install,pre-upgrade,post-install,post-upgrade\n \"helm.sh/hook-delete-policy\": before-hook-creation,hook-succeeded\n labels:\n app: kps-admission\n \n app.kubernetes.io/managed-by: Helm\n app.kubernetes.io/instance: prometheus-operator\n app.kubernetes.io/version: \"56.21.0\"\n app.kubernetes.io/part-of: kps\n chart: kps-56.21.0\n release: \"prometheus-operator\"\n heritage: \"Helm\"\n app.kubernetes.io/name: kps-prometheus-operator\n app.kubernetes.io/component: prometheus-operator-webhook\nrules:\n - apiGroups:\n - \"\"\n resources:\n - secrets\n verbs:\n - get\n - create",
+ "events": [
+ "pre-install",
+ "pre-upgrade",
+ "post-install",
+ "post-upgrade"
+ ],
+ "last_run": {
+ "started_at": "2024-05-03T02:01:43.54748275+10:00",
+ "completed_at": "2024-05-03T02:01:43.558090864+10:00",
+ "phase": "Succeeded"
+ },
+ "delete_policies": [
+ "before-hook-creation",
+ "hook-succeeded"
+ ]
+ },
+ {
+ "name": "prometheus-operator-kps-admission",
+ "kind": "RoleBinding",
+ "path": "prometheus-operator/charts/kps/templates/prometheus-operator/admission-webhooks/job-patch/rolebinding.yaml",
+ "manifest": "apiVersion: rbac.authorization.k8s.io/v1\nkind: RoleBinding\nmetadata:\n name: prometheus-operator-kps-admission\n namespace: ix-prometheus-operator\n annotations:\n \"helm.sh/hook\": pre-install,pre-upgrade,post-install,post-upgrade\n \"helm.sh/hook-delete-policy\": before-hook-creation,hook-succeeded\n labels:\n app: kps-admission\n \n app.kubernetes.io/managed-by: Helm\n app.kubernetes.io/instance: prometheus-operator\n app.kubernetes.io/version: \"56.21.0\"\n app.kubernetes.io/part-of: kps\n chart: kps-56.21.0\n release: \"prometheus-operator\"\n heritage: \"Helm\"\n app.kubernetes.io/name: kps-prometheus-operator\n app.kubernetes.io/component: prometheus-operator-webhook\nroleRef:\n apiGroup: rbac.authorization.k8s.io\n kind: Role\n name: prometheus-operator-kps-admission\nsubjects:\n - kind: ServiceAccount\n name: prometheus-operator-kps-admission\n namespace: ix-prometheus-operator",
+ "events": [
+ "pre-install",
+ "pre-upgrade",
+ "post-install",
+ "post-upgrade"
+ ],
+ "last_run": {
+ "started_at": "2024-05-03T02:01:43.586601716+10:00",
+ "completed_at": "2024-05-03T02:01:43.596184173+10:00",
+ "phase": "Succeeded"
+ },
+ "delete_policies": [
+ "before-hook-creation",
+ "hook-succeeded"
+ ]
+ },
+ {
+ "name": "prometheus-operator-kps-admission-create",
+ "kind": "Job",
+ "path": "prometheus-operator/charts/kps/templates/prometheus-operator/admission-webhooks/job-patch/job-createSecret.yaml",
+ "manifest": "apiVersion: batch/v1\nkind: Job\nmetadata:\n name: prometheus-operator-kps-admission-create\n namespace: ix-prometheus-operator\n annotations:\n \"helm.sh/hook\": pre-install,pre-upgrade\n \"helm.sh/hook-delete-policy\": before-hook-creation,hook-succeeded\n labels:\n app: kps-admission-create\n \n app.kubernetes.io/managed-by: Helm\n app.kubernetes.io/instance: prometheus-operator\n app.kubernetes.io/version: \"56.21.0\"\n app.kubernetes.io/part-of: kps\n chart: kps-56.21.0\n release: \"prometheus-operator\"\n heritage: \"Helm\"\n app.kubernetes.io/name: kps-prometheus-operator\n app.kubernetes.io/component: prometheus-operator-webhook\nspec:\n template:\n metadata:\n name: prometheus-operator-kps-admission-create\n labels:\n app: kps-admission-create\n \n app.kubernetes.io/managed-by: Helm\n app.kubernetes.io/instance: prometheus-operator\n app.kubernetes.io/version: \"56.21.0\"\n app.kubernetes.io/part-of: kps\n chart: kps-56.21.0\n release: \"prometheus-operator\"\n heritage: \"Helm\"\n app.kubernetes.io/name: kps-prometheus-operator\n app.kubernetes.io/component: prometheus-operator-webhook\n spec:\n containers:\n - name: create\n image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v20221220-controller-v1.5.1-58-g787ea74b6\n imagePullPolicy: IfNotPresent\n args:\n - create\n - --host=prometheus-operator-kps-operator,prometheus-operator-kps-operator.ix-prometheus-operator.svc\n - --namespace=ix-prometheus-operator\n - --secret-name=prometheus-operator-kps-admission\n securityContext:\n \n allowPrivilegeEscalation: false\n capabilities:\n drop:\n - ALL\n readOnlyRootFilesystem: true\n resources:\n {}\n restartPolicy: OnFailure\n serviceAccountName: prometheus-operator-kps-admission\n securityContext:\n runAsGroup: 2000\n runAsNonRoot: true\n runAsUser: 2000\n seccompProfile:\n type: RuntimeDefault",
+ "events": [
+ "pre-install",
+ "pre-upgrade"
+ ],
+ "last_run": {
+ "started_at": "2024-05-03T02:01:18.619108089+10:00",
+ "completed_at": "2024-05-03T02:01:42.910001285+10:00",
+ "phase": "Succeeded"
+ },
+ "delete_policies": [
+ "before-hook-creation",
+ "hook-succeeded"
+ ]
+ },
+ {
+ "name": "prometheus-operator-kps-admission-patch",
+ "kind": "Job",
+ "path": "prometheus-operator/charts/kps/templates/prometheus-operator/admission-webhooks/job-patch/job-patchWebhook.yaml",
+ "manifest": "apiVersion: batch/v1\nkind: Job\nmetadata:\n name: prometheus-operator-kps-admission-patch\n namespace: ix-prometheus-operator\n annotations:\n \"helm.sh/hook\": post-install,post-upgrade\n \"helm.sh/hook-delete-policy\": before-hook-creation,hook-succeeded\n labels:\n app: kps-admission-patch\n \n app.kubernetes.io/managed-by: Helm\n app.kubernetes.io/instance: prometheus-operator\n app.kubernetes.io/version: \"56.21.0\"\n app.kubernetes.io/part-of: kps\n chart: kps-56.21.0\n release: \"prometheus-operator\"\n heritage: \"Helm\"\n app.kubernetes.io/name: kps-prometheus-operator\n app.kubernetes.io/component: prometheus-operator-webhook\nspec:\n template:\n metadata:\n name: prometheus-operator-kps-admission-patch\n labels:\n app: kps-admission-patch\n \n app.kubernetes.io/managed-by: Helm\n app.kubernetes.io/instance: prometheus-operator\n app.kubernetes.io/version: \"56.21.0\"\n app.kubernetes.io/part-of: kps\n chart: kps-56.21.0\n release: \"prometheus-operator\"\n heritage: \"Helm\"\n app.kubernetes.io/name: kps-prometheus-operator\n app.kubernetes.io/component: prometheus-operator-webhook\n spec:\n containers:\n - name: patch\n image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v20221220-controller-v1.5.1-58-g787ea74b6\n imagePullPolicy: IfNotPresent\n args:\n - patch\n - --webhook-name=prometheus-operator-kps-admission\n - --namespace=ix-prometheus-operator\n - --secret-name=prometheus-operator-kps-admission\n - --patch-failure-policy=\n securityContext:\n \n allowPrivilegeEscalation: false\n capabilities:\n drop:\n - ALL\n readOnlyRootFilesystem: true\n resources:\n {}\n restartPolicy: OnFailure\n serviceAccountName: prometheus-operator-kps-admission\n securityContext:\n runAsGroup: 2000\n runAsNonRoot: true\n runAsUser: 2000\n seccompProfile:\n type: RuntimeDefault",
+ "events": [
+ "post-install",
+ "post-upgrade"
+ ],
+ "last_run": {
+ "started_at": "2024-05-03T02:01:43.629838703+10:00",
+ "completed_at": "2024-05-03T02:01:55.514722424+10:00",
+ "phase": "Succeeded"
+ },
+ "delete_policies": [
+ "before-hook-creation",
+ "hook-succeeded"
+ ]
+ }
+ ],
+ "version": 44,
+ "namespace": "ix-prometheus-operator",
+ "chart_metadata": {
+ "name": "prometheus-operator",
+ "home": "https://truecharts.org/charts/system/prometheus-operator",
+ "sources": [
+ "https://github.com/prometheus-operator",
+ "https://github.com/truecharts/charts/tree/master/charts/system/prometheus-operator",
+ "https://github.com/truecharts/containers/tree/master/apps/alpine"
+ ],
+ "version": "7.0.4",
+ "description": "Prometheus Operator is an operator for prometheus",
+ "keywords": [
+ "operator",
+ "prometheus",
+ "metics"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/prometheus-operator.webp",
+ "apiVersion": "v2",
+ "appVersion": "latest",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "operators",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "system"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.0",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ },
+ {
+ "name": "kps",
+ "version": "56.21.0",
+ "repository": "oci://ghcr.io/prometheus-community/charts",
+ "enabled": true,
+ "alias": "kps"
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "7.0.4"
+ },
+ "id": "prometheus-operator",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "system",
+ "path": "/mnt/Apps/ix-applications/releases/prometheus-operator",
+ "dataset": "Apps/ix-applications/releases/prometheus-operator",
+ "status": "ACTIVE",
+ "used_ports": [],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "latest_7.0.4",
+ "human_latest_version": "latest_7.0.4",
+ "container_images_update_available": false,
+ "portals": {}
+}
diff --git a/clustertool/testdata/truenas_exports/prowlarr.json b/clustertool/testdata/truenas_exports/prowlarr.json
new file mode 100644
index 0000000000000..2219a828a35e3
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/prowlarr.json
@@ -0,0 +1,388 @@
+{
+ "name": "prowlarr",
+ "info": {
+ "first_deployed": "2024-04-24T17:37:53.687759378+10:00",
+ "last_deployed": "2024-07-03T20:28:33.988204899+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing prowlarr by TrueCharts.\n\n\n## Connecting externally\nYou can use this Chart by opening one of the following links in your browser:\n- host: $node_ip\npath: /\nport: \"9696\"\nportalName: open\nprotocol: http\nurl: http://$node_ip:9696/\nuseNodeIP: true\n## Sources for prowlarr\n- https://ghcr.io/onedr0p/exportarr\n- https://ghcr.io/onedr0p/prowlarr-develop\n- https://github.com/Prowlarr/Prowlarr\n- https://github.com/k8s-at-home/container-images\n- https://github.com/truecharts/charts/tree/master/charts/stable/prowlarr\n\nSee more for **prowlarr** at (https://truecharts.org/charts/stable/prowlarr)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "configmap": {
+ "dashboard": {
+ "data": {
+ "prowlarr.json": "{{ .Files.Get \"dashboard.json\" | indent 8 }}"
+ },
+ "enabled": true,
+ "labels": {
+ "grafana_dashboard": "1"
+ }
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-prowlarr",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "exportarrImage": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/onedr0p/exportarr",
+ "tag": "v2.0.1@sha256:727e7bc8f2f0934a2117978c59f4476b954018b849a010ea6cfb380bd6539644"
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-prowlarr",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/onedr0p/prowlarr-develop",
+ "tag": "1.17.2.4511@sha256:01dce2a9c0e29a2a5338a9457698ea3e027727bed6b9f0ab7ac4a259cafb991b"
+ },
+ "imagePullSecretList": [],
+ "ingress": {
+ "main": {
+ "enabled": false
+ }
+ },
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-prowlarr",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "metrics": {
+ "main": {
+ "enabled": false,
+ "endpoints": [
+ {
+ "path": "/metrics",
+ "port": "metrics"
+ }
+ ],
+ "prometheusRule": {
+ "enabled": false
+ },
+ "targetSelector": "metrics",
+ "type": "servicemonitor"
+ }
+ },
+ "networkPolicy": [],
+ "persistence": {
+ "config": {
+ "enabled": true,
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "targetSelector": {
+ "exportarr": {
+ "exportarr": {
+ "mountPath": "/config",
+ "readOnly": true
+ }
+ },
+ "main": {
+ "main": {
+ "mountPath": "/config"
+ }
+ }
+ },
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ }
+ },
+ "persistenceList": [],
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": true
+ }
+ },
+ "release_name": "prowlarr",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 0
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "UMASK": "0022",
+ "advanced": false,
+ "readOnlyRootFilesystem": false,
+ "runAsGroup": 568,
+ "runAsUser": 568
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "main": {
+ "port": 9696
+ }
+ },
+ "type": "LoadBalancer"
+ },
+ "metrics": {
+ "enabled": true,
+ "ports": {
+ "metrics": {
+ "enabled": true,
+ "port": 9697,
+ "targetSelector": "exportarr"
+ }
+ },
+ "targetSelector": "exportarr",
+ "type": "ClusterIP"
+ }
+ },
+ "serviceList": [],
+ "updated": true,
+ "workload": {
+ "exportarr": {
+ "enabled": true,
+ "podSpec": {
+ "containers": {
+ "exportarr": {
+ "args": [
+ "prowlarr"
+ ],
+ "enabled": true,
+ "env": {
+ "CONFIG": "/config/config.xml",
+ "INTERFACE": "0.0.0.0",
+ "PORT": "{{ .Values.service.metrics.ports.metrics.port }}",
+ "URL": "{{ printf \"http://%v:%v\" (include \"tc.v1.common.lib.chart.names.fullname\" $) .Values.service.main.ports.main.port }}"
+ },
+ "imageSelector": "exportarrImage",
+ "primary": true,
+ "probes": {
+ "liveness": {
+ "enabled": true,
+ "path": "/healthz",
+ "port": "{{ .Values.service.metrics.ports.metrics.port }}",
+ "type": "http"
+ },
+ "readiness": {
+ "enabled": true,
+ "path": "/healthz",
+ "port": "{{ .Values.service.metrics.ports.metrics.port }}",
+ "type": "http"
+ },
+ "startup": {
+ "enabled": true,
+ "path": "/healthz",
+ "port": "{{ .Values.service.metrics.ports.metrics.port }}",
+ "type": "http"
+ }
+ }
+ }
+ }
+ },
+ "replicas": 1,
+ "strategy": "RollingUpdate",
+ "type": "Deployment"
+ },
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "env": {
+ "PROWLARR__AUTHENTICATION_METHOD": "",
+ "PROWLARR__PORT": "{{ .Values.service.main.ports.main.port }}"
+ },
+ "envList": [],
+ "extraArgs": [],
+ "probes": {
+ "liveness": {
+ "path": "/ping"
+ },
+ "readiness": {
+ "path": "/ping"
+ },
+ "startup": {
+ "type": "tcp"
+ }
+ }
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 19,
+ "namespace": "ix-prowlarr",
+ "chart_metadata": {
+ "name": "prowlarr",
+ "home": "https://truecharts.org/charts/stable/prowlarr",
+ "sources": [
+ "https://ghcr.io/onedr0p/exportarr",
+ "https://ghcr.io/onedr0p/prowlarr-develop",
+ "https://github.com/Prowlarr/Prowlarr",
+ "https://github.com/k8s-at-home/container-images",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/prowlarr"
+ ],
+ "version": "17.1.7",
+ "description": "Indexer manager/proxy built on the popular arr net base stack to integrate with your various PVR apps.",
+ "keywords": [
+ "prowlarr",
+ "torrent",
+ "usenet"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/prowlarr.webp",
+ "apiVersion": "v2",
+ "appVersion": "1.17.2.4511",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "media",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "17.1.7"
+ },
+ "id": "prowlarr",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/prowlarr",
+ "dataset": "Apps/ix-applications/releases/prowlarr",
+ "status": "ACTIVE",
+ "used_ports": [
+ {
+ "port": 9696,
+ "protocol": "TCP"
+ }
+ ],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "1.17.2.4511_17.1.7",
+ "human_latest_version": "1.17.2.4511_17.1.7",
+ "container_images_update_available": false,
+ "portals": {
+ "open": [
+ "http://10.0.0.20:9696/"
+ ]
+ }
+}
diff --git a/clustertool/testdata/truenas_exports/qbittorrent.json b/clustertool/testdata/truenas_exports/qbittorrent.json
new file mode 100644
index 0000000000000..c17b248dd9b8e
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/qbittorrent.json
@@ -0,0 +1,387 @@
+{
+ "name": "qbittorrent",
+ "info": {
+ "first_deployed": "2024-04-24T17:39:30.195157013+10:00",
+ "last_deployed": "2024-07-03T20:29:21.31130368+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing qbittorrent by TrueCharts.\n\n\n## Connecting externally\nYou can use this Chart by opening one of the following links in your browser:\n- host: $node_ip\npath: /\nport: \"10095\"\nportalName: open\nprotocol: http\nurl: http://$node_ip:10095/\nuseNodeIP: true\n## Sources for qbittorrent\n- https://ghcr.io/onedr0p/qbittorrent\n- https://github.com/qbittorrent/qBittorrent\n- https://github.com/truecharts/charts/tree/master/charts/stable/qbittorrent\n- https://hub.docker.com/r/mjmeli/qbittorrent-port-forward-gluetun-server\n\nSee more for **qbittorrent** at (https://truecharts.org/charts/stable/qbittorrent)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-qbittorrent",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-qbittorrent",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/onedr0p/qbittorrent",
+ "tag": "4.6.5@sha256:c019af23966ebafcaf1713d4553bc043246858b711a7d57d8bee358a89990a3e"
+ },
+ "imagePullSecretList": [],
+ "ingress": {
+ "main": {
+ "enabled": false
+ }
+ },
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-qbittorrent",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "networkPolicy": [],
+ "persistence": {
+ "config": {
+ "enabled": true,
+ "mountPath": "/config",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ }
+ },
+ "persistenceList": [
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Scratch/Scratch/arrTemp/Downloads",
+ "mountPath": "/Downloads",
+ "readOnly": false,
+ "type": "hostPath"
+ },
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Main/Media/Movie",
+ "mountPath": "/Media/Movie",
+ "readOnly": false,
+ "type": "hostPath"
+ },
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Main/Media/Shows",
+ "mountPath": "/Media/Shows",
+ "readOnly": false,
+ "type": "hostPath"
+ }
+ ],
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": true
+ }
+ },
+ "qbitportforward": {
+ "QBT_PASSWORD": "adminadmin",
+ "QBT_USERNAME": "admin",
+ "enabled": false
+ },
+ "qbitportforwardImage": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "mjmeli/qbittorrent-port-forward-gluetun-server",
+ "tag": "latest@sha256:67d0d21ed792cf80716d4211e7162b6d375af5c12f3cf096c9032ad705dddaa8"
+ },
+ "release_name": "qbittorrent",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 0
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "UMASK": "0022",
+ "advanced": false,
+ "readOnlyRootFilesystem": false,
+ "runAsGroup": 568,
+ "runAsUser": 568
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "gluetun": {
+ "enabled": true,
+ "ports": {
+ "gluetun": {
+ "enabled": true,
+ "port": 8000,
+ "protocol": "http",
+ "targetPort": 8000
+ }
+ },
+ "type": "ClusterIP"
+ },
+ "main": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "main": {
+ "port": 10095
+ }
+ },
+ "type": "LoadBalancer"
+ },
+ "torrent": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "torrent": {
+ "enabled": true,
+ "port": 6881,
+ "protocol": "tcp"
+ },
+ "torrentudp": {
+ "enabled": true,
+ "port": "{{ .Values.service.torrent.ports.torrent.port }}",
+ "protocol": "udp"
+ }
+ },
+ "type": "LoadBalancer"
+ }
+ },
+ "serviceList": [],
+ "workload": {
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "env": {
+ "QBITTORRENT__PORT": "{{ .Values.service.main.ports.main.port }}",
+ "QBT_BitTorrent__Session__Port": "{{ .Values.service.torrent.ports.torrent.port }}",
+ "QBT_Preferences__Connection__PortRangeMin": "{{ .Values.service.torrent.ports.torrent.port }}",
+ "QBT_Preferences__WebUI__Address": "*"
+ },
+ "envList": [],
+ "extraArgs": []
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ },
+ "qbitportforward": {
+ "enabled": true,
+ "podSpec": {
+ "containers": {
+ "qbitportforward": {
+ "command": "/usr/src/app/main.sh",
+ "enabled": true,
+ "env": {
+ "GTN_ADDR": "{{ printf \"http://%v-gluetun:8000\" (include \"tc.v1.common.lib.chart.names.fullname\" $) }}",
+ "QBT_ADDR": "{{ printf \"http://%v:%v\" (include \"tc.v1.common.lib.chart.names.fullname\" $) .Values.service.main.ports.main.port }}",
+ "QBT_PASSWORD": "{{ .Values.qbitportforward.QBT_PASSWORD }}",
+ "QBT_USERNAME": "{{ .Values.qbitportforward.QBT_USERNAME }}"
+ },
+ "imageSelector": "qbitportforwardImage",
+ "primary": true,
+ "probes": {
+ "liveness": {
+ "enabled": false
+ },
+ "readiness": {
+ "enabled": false
+ },
+ "startup": {
+ "enabled": false
+ }
+ }
+ }
+ },
+ "restartPolicy": "OnFailure"
+ },
+ "schedule": "*/5 * * * *",
+ "type": "CronJob"
+ }
+ }
+ },
+ "version": 20,
+ "namespace": "ix-qbittorrent",
+ "chart_metadata": {
+ "name": "qbittorrent",
+ "home": "https://truecharts.org/charts/stable/qbittorrent",
+ "sources": [
+ "https://ghcr.io/onedr0p/qbittorrent",
+ "https://github.com/qbittorrent/qBittorrent",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/qbittorrent",
+ "https://hub.docker.com/r/mjmeli/qbittorrent-port-forward-gluetun-server"
+ ],
+ "version": "20.0.11",
+ "description": "qBittorrent is a cross-platform free and open-source BitTorrent client",
+ "keywords": [
+ "qbittorrent",
+ "torrrent"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/qbittorrent.webp",
+ "apiVersion": "v2",
+ "appVersion": "4.6.5",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "media",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "20.0.11"
+ },
+ "id": "qbittorrent",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/qbittorrent",
+ "dataset": "Apps/ix-applications/releases/qbittorrent",
+ "status": "ACTIVE",
+ "used_ports": [
+ {
+ "port": 10095,
+ "protocol": "TCP"
+ },
+ {
+ "port": 6881,
+ "protocol": "TCP"
+ },
+ {
+ "port": 6881,
+ "protocol": "UDP"
+ }
+ ],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "4.6.5_20.0.11",
+ "human_latest_version": "4.6.5_20.0.11",
+ "container_images_update_available": false,
+ "portals": {
+ "open": [
+ "http://10.0.0.20:10095/"
+ ]
+ }
+}
diff --git a/clustertool/testdata/truenas_exports/radarr.json b/clustertool/testdata/truenas_exports/radarr.json
new file mode 100644
index 0000000000000..7a057abe107af
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/radarr.json
@@ -0,0 +1,403 @@
+{
+ "name": "radarr",
+ "info": {
+ "first_deployed": "2024-04-24T17:40:56.311459268+10:00",
+ "last_deployed": "2024-07-03T20:29:59.088075704+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing radarr by TrueCharts.\n\n\n## Connecting externally\nYou can use this Chart by opening one of the following links in your browser:\n- host: $node_ip\npath: /\nport: \"7878\"\nportalName: open\nprotocol: http\nurl: http://$node_ip:7878/\nuseNodeIP: true\n## Sources for radarr\n- https://ghcr.io/onedr0p/exportarr\n- https://ghcr.io/onedr0p/radarr\n- https://github.com/Radarr/Radarr\n- https://github.com/truecharts/charts/tree/master/charts/stable/radarr\n\nSee more for **radarr** at (https://truecharts.org/charts/stable/radarr)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-radarr",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "exportarrImage": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/onedr0p/exportarr",
+ "tag": "v2.0.1@sha256:727e7bc8f2f0934a2117978c59f4476b954018b849a010ea6cfb380bd6539644"
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-radarr",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/onedr0p/radarr",
+ "tag": "5.6.0.8846@sha256:3c75c2adc6ce547131a74b10fec4e0101658113810dba11b96878a0c3990c641"
+ },
+ "imagePullSecretList": [],
+ "ingress": {
+ "main": {
+ "enabled": false
+ }
+ },
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-radarr",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "metrics": {
+ "main": {
+ "enabled": false,
+ "endpoints": [
+ {
+ "path": "/metrics",
+ "port": "metrics"
+ }
+ ],
+ "prometheusRule": {
+ "enabled": false
+ },
+ "targetSelector": "metrics",
+ "type": "servicemonitor"
+ }
+ },
+ "networkPolicy": [],
+ "persistence": {
+ "config": {
+ "enabled": true,
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "targetSelector": {
+ "exportarr": {
+ "exportarr": {
+ "mountPath": "/config",
+ "readOnly": true
+ }
+ },
+ "main": {
+ "main": {
+ "mountPath": "/config"
+ }
+ }
+ },
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ }
+ },
+ "persistenceList": [
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Scratch/Scratch/arrTemp/Downloads",
+ "mountPath": "/Downloads",
+ "readOnly": false,
+ "type": "hostPath"
+ },
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Main/Media/Movie",
+ "mountPath": "/Media/Movie",
+ "readOnly": false,
+ "type": "hostPath"
+ }
+ ],
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": true
+ }
+ },
+ "release_name": "radarr",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 0
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "UMASK": "0022",
+ "advanced": false,
+ "readOnlyRootFilesystem": false,
+ "runAsGroup": 568,
+ "runAsUser": 568
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "main": {
+ "port": 7878
+ }
+ },
+ "type": "LoadBalancer"
+ },
+ "metrics": {
+ "enabled": true,
+ "ports": {
+ "metrics": {
+ "enabled": true,
+ "port": 7879,
+ "targetSelector": "exportarr"
+ }
+ },
+ "targetSelector": "exportarr",
+ "type": "ClusterIP"
+ }
+ },
+ "serviceList": [],
+ "updated": true,
+ "workload": {
+ "exportarr": {
+ "enabled": true,
+ "podSpec": {
+ "containers": {
+ "exportarr": {
+ "args": [
+ "radarr"
+ ],
+ "enabled": true,
+ "env": {
+ "CONFIG": "/config/config.xml",
+ "INTERFACE": "0.0.0.0",
+ "PORT": "{{ .Values.service.metrics.ports.metrics.port }}",
+ "URL": "{{ printf \"http://%v:%v\" (include \"tc.v1.common.lib.chart.names.fullname\" $) .Values.service.main.ports.main.port }}"
+ },
+ "imageSelector": "exportarrImage",
+ "primary": true,
+ "probes": {
+ "liveness": {
+ "enabled": true,
+ "path": "/healthz",
+ "port": "{{ .Values.service.metrics.ports.metrics.port }}",
+ "type": "http"
+ },
+ "readiness": {
+ "enabled": true,
+ "path": "/healthz",
+ "port": "{{ .Values.service.metrics.ports.metrics.port }}",
+ "type": "http"
+ },
+ "startup": {
+ "enabled": true,
+ "path": "/healthz",
+ "port": "{{ .Values.service.metrics.ports.metrics.port }}",
+ "type": "http"
+ }
+ }
+ }
+ }
+ },
+ "replicas": 1,
+ "strategy": "RollingUpdate",
+ "type": "Deployment"
+ },
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "env": {
+ "RADARR__AUTHENTICATION_METHOD": "",
+ "RADARR__PORT": "{{ .Values.service.main.ports.main.port }}"
+ },
+ "envList": [],
+ "extraArgs": [],
+ "probes": {
+ "liveness": {
+ "enabled": true,
+ "path": "/ping",
+ "type": "http"
+ },
+ "readiness": {
+ "enabled": true,
+ "path": "/ping",
+ "type": "http"
+ },
+ "startup": {
+ "enabled": true,
+ "path": "/ping",
+ "type": "http"
+ }
+ }
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 18,
+ "namespace": "ix-radarr",
+ "chart_metadata": {
+ "name": "radarr",
+ "home": "https://truecharts.org/charts/stable/radarr",
+ "sources": [
+ "https://ghcr.io/onedr0p/exportarr",
+ "https://ghcr.io/onedr0p/radarr",
+ "https://github.com/Radarr/Radarr",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/radarr"
+ ],
+ "version": "22.2.4",
+ "description": "A fork of Sonarr to work with movies à la Couchpotato",
+ "keywords": [
+ "radarr",
+ "torrent",
+ "usenet"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/radarr.webp",
+ "apiVersion": "v2",
+ "appVersion": "5.6.0.8846",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "media",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "22.2.4"
+ },
+ "id": "radarr",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/radarr",
+ "dataset": "Apps/ix-applications/releases/radarr",
+ "status": "ACTIVE",
+ "used_ports": [
+ {
+ "port": 7878,
+ "protocol": "TCP"
+ }
+ ],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "5.6.0.8846_22.2.4",
+ "human_latest_version": "5.6.0.8846_22.2.4",
+ "container_images_update_available": false,
+ "portals": {
+ "open": [
+ "http://10.0.0.20:7878/"
+ ]
+ }
+}
diff --git a/clustertool/testdata/truenas_exports/rdtclient.json b/clustertool/testdata/truenas_exports/rdtclient.json
new file mode 100644
index 0000000000000..dfc57ce805ff1
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/rdtclient.json
@@ -0,0 +1,395 @@
+{
+ "name": "rdtclient",
+ "info": {
+ "first_deployed": "2024-04-24T15:03:56.275955013+10:00",
+ "last_deployed": "2024-07-03T20:31:02.976237846+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing rdtclient by TrueCharts.\n\n\n## Connecting externally\nYou can use this Chart by opening one of the following links in your browser:\n- host: $node_ip\npath: /\nport: \"6500\"\nportalName: open\nprotocol: http\nurl: http://$node_ip:6500/\nuseNodeIP: true\n## Sources for rdtclient\n- https://ghcr.io/rogerfar/rdtclient\n- https://github.com/rogerfar/rdt-client\n- https://github.com/truecharts/charts/tree/master/charts/stable/rdtclient\n- https://hub.docker.com/r/p3terx/aria2-pro\n\nSee more for **rdtclient** at (https://truecharts.org/charts/stable/rdtclient)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "aria2": {
+ "custom_trackers_url": "https://trackerslist.com/all_aria2.txt",
+ "disk_cache": "64M",
+ "enabled": false,
+ "rpc_secret": "MY_SECRET",
+ "update_trackers": true
+ },
+ "aria2Image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "p3terx/aria2-pro",
+ "tag": "latest@sha256:086d1a37c586edb07ec0fb956bf9edd89d1d38d138ad5309ff96d510c8c9a011"
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-rdtclient",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-rdtclient",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/rogerfar/rdtclient",
+ "tag": "2.0.78@sha256:6137ed0f2b7394d175d16c0a1c326f63b974d91e92a71ed2fa3ec4fe93b18d25"
+ },
+ "imagePullSecretList": [],
+ "ingress": {
+ "main": {
+ "enabled": false
+ }
+ },
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-rdtclient",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "networkPolicy": [],
+ "persistence": {
+ "config": {
+ "enabled": true,
+ "targetSelector": {
+ "aria2": {
+ "aria2": {
+ "mountPath": "/config"
+ }
+ }
+ }
+ },
+ "db": {
+ "enabled": true,
+ "mountPath": "/data/db",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ }
+ },
+ "persistenceList": [
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Scratch/Scratch/arrTemp/Downloads",
+ "mountPath": "/data/downloads",
+ "readOnly": false,
+ "type": "hostPath"
+ }
+ ],
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": true
+ }
+ },
+ "release_name": "rdtclient",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 0
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "PUID": 568,
+ "UMASK": "0022",
+ "advanced": false,
+ "readOnlyRootFilesystem": false,
+ "runAsGroup": 0,
+ "runAsNonRoot": false,
+ "runAsUser": 0
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "aria2": {
+ "enabled": true,
+ "ports": {
+ "rpc": {
+ "enabled": true,
+ "port": 6800,
+ "targetSelector": "aria2"
+ }
+ },
+ "targetSelector": "aria2",
+ "type": "ClusterIP"
+ },
+ "main": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "main": {
+ "port": 6500,
+ "protocol": "http",
+ "targetPort": 6500
+ }
+ },
+ "type": "LoadBalancer"
+ },
+ "torrent": {
+ "enabled": true,
+ "ports": {
+ "torrent": {
+ "enabled": true,
+ "port": 6888,
+ "targetSelector": "aria2"
+ },
+ "torrent-udp": {
+ "enabled": true,
+ "port": 6888,
+ "protocol": "udp",
+ "targetSelector": "aria2"
+ }
+ },
+ "targetSelector": "aria2",
+ "type": "ClusterIP"
+ }
+ },
+ "serviceList": [],
+ "workload": {
+ "aria2": {
+ "enabled": true,
+ "podSpec": {
+ "containers": {
+ "aria2": {
+ "enabled": true,
+ "env": {
+ "CUSTOM_TRACKER_URL": "{{ .Values.aria2.custom_trackers_url }}",
+ "DISK_CACHE": "{{ .Values.aria2.disk_cache }}",
+ "IPV6_MODE": false,
+ "LISTEN_PORT": "{{ .Values.service.torrent.ports.torrent.port }}",
+ "RPC_PORT": "{{ .Values.service.aria2.ports.rpc.port }}",
+ "RPC_SECRET": "{{ .Values.aria2.rpc_secret }}",
+ "UPDATE_TRACKERS": "{{ .Values.aria2.update_trackers }}"
+ },
+ "imageSelector": "aria2Image",
+ "primary": true,
+ "probes": {
+ "liveness": {
+ "enabled": true,
+ "port": "{{ .Values.service.aria2.ports.rpc.port }}",
+ "type": "tcp"
+ },
+ "readiness": {
+ "enabled": true,
+ "port": "{{ .Values.service.aria2.ports.rpc.port }}",
+ "type": "tcp"
+ },
+ "startup": {
+ "enabled": true,
+ "port": "{{ .Values.service.aria2.ports.rpc.port }}",
+ "type": "tcp"
+ }
+ },
+ "securityContext": {
+ "readOnlyRootFilesystem": false,
+ "runAsGroup": 0,
+ "runAsNonRoot": false,
+ "runAsUser": 0
+ }
+ }
+ }
+ },
+ "replicas": 1,
+ "strategy": "RollingUpdate",
+ "type": "Deployment"
+ },
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "envList": [],
+ "extraArgs": [],
+ "probes": {
+ "liveness": {
+ "path": "/"
+ },
+ "readiness": {
+ "path": "/"
+ },
+ "startup": {
+ "path": "/"
+ }
+ }
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 26,
+ "namespace": "ix-rdtclient",
+ "chart_metadata": {
+ "name": "rdtclient",
+ "home": "https://truecharts.org/charts/stable/rdtclient",
+ "sources": [
+ "https://ghcr.io/rogerfar/rdtclient",
+ "https://github.com/rogerfar/rdt-client",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/rdtclient",
+ "https://hub.docker.com/r/p3terx/aria2-pro"
+ ],
+ "version": "5.0.9",
+ "description": "This is a web interface to manage your torrents on Real-Debrid, AllDebrid or Premiumize.",
+ "keywords": [
+ "rdtclient",
+ "torrent"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/rdtclient.webp",
+ "apiVersion": "v2",
+ "appVersion": "2.0.78",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "media",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "5.0.9"
+ },
+ "id": "rdtclient",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/rdtclient",
+ "dataset": "Apps/ix-applications/releases/rdtclient",
+ "status": "ACTIVE",
+ "used_ports": [
+ {
+ "port": 6500,
+ "protocol": "TCP"
+ }
+ ],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "2.0.78_5.0.9",
+ "human_latest_version": "2.0.78_5.0.9",
+ "container_images_update_available": false,
+ "portals": {
+ "open": [
+ "http://10.0.0.20:6500/"
+ ]
+ }
+}
diff --git a/clustertool/testdata/truenas_exports/readarr.json b/clustertool/testdata/truenas_exports/readarr.json
new file mode 100644
index 0000000000000..331c9057340d3
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/readarr.json
@@ -0,0 +1,411 @@
+{
+ "name": "readarr",
+ "info": {
+ "first_deployed": "2024-04-24T17:42:19.222907642+10:00",
+ "last_deployed": "2024-07-03T21:32:39.571768921+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing readarr by TrueCharts.\n\n\n## Connecting externally\nYou can use this Chart by opening one of the following links in your browser:\n- host: $node_ip\npath: /\nport: \"8787\"\nportalName: open\nprotocol: http\nurl: http://$node_ip:8787/\nuseNodeIP: true\n## Sources for readarr\n- https://ghcr.io/onedr0p/exportarr\n- https://ghcr.io/onedr0p/readarr-develop\n- https://github.com/Readarr/Readarr\n- https://github.com/truecharts/charts/tree/master/charts/stable/readarr\n- https://readarr.com\n\nSee more for **readarr** at (https://truecharts.org/charts/stable/readarr)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-readarr",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "exportarrImage": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/onedr0p/exportarr",
+ "tag": "v2.0.1@sha256:727e7bc8f2f0934a2117978c59f4476b954018b849a010ea6cfb380bd6539644"
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-readarr",
+ "upgradeMetadata": {}
+ },
+ "stopAll": true
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/onedr0p/readarr-develop",
+ "tag": "0.3.27.2538@sha256:6850caa980bfea336b6113c37f59c40a42f61ca2714d5b8f9c13b5933e33c0f2"
+ },
+ "imagePullSecretList": [],
+ "ingress": {
+ "main": {
+ "enabled": false
+ }
+ },
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-readarr",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "metrics": {
+ "main": {
+ "enabled": false,
+ "endpoints": [
+ {
+ "path": "/metrics",
+ "port": "metrics"
+ }
+ ],
+ "prometheusRule": {
+ "enabled": false
+ },
+ "targetSelector": "metrics",
+ "type": "servicemonitor"
+ }
+ },
+ "networkPolicy": [],
+ "persistence": {
+ "config": {
+ "enabled": true,
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "targetSelector": {
+ "exportarr": {
+ "exportarr": {
+ "mountPath": "/config",
+ "readOnly": true
+ }
+ },
+ "main": {
+ "main": {
+ "mountPath": "/config"
+ }
+ }
+ },
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ }
+ },
+ "persistenceList": [
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Main/Media/Audiobooks",
+ "mountPath": "/Audiobooks",
+ "readOnly": false,
+ "type": "hostPath"
+ },
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Apps/vApps/readarr/Backups",
+ "mountPath": "/Backups",
+ "readOnly": false,
+ "type": "hostPath"
+ },
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Scratch/Scratch/arrTemp/Downloads",
+ "mountPath": "/Downloads",
+ "readOnly": false,
+ "type": "hostPath"
+ }
+ ],
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": true
+ }
+ },
+ "release_name": "readarr",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "2000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 0
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "UMASK": "0022",
+ "advanced": false,
+ "readOnlyRootFilesystem": false,
+ "runAsGroup": 568,
+ "runAsUser": 568
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "main": {
+ "port": 8787
+ }
+ },
+ "type": "LoadBalancer"
+ },
+ "metrics": {
+ "enabled": true,
+ "ports": {
+ "metrics": {
+ "enabled": true,
+ "port": 8788,
+ "targetSelector": "exportarr"
+ }
+ },
+ "targetSelector": "exportarr",
+ "type": "ClusterIP"
+ }
+ },
+ "serviceList": [],
+ "updated": true,
+ "workload": {
+ "exportarr": {
+ "enabled": true,
+ "podSpec": {
+ "containers": {
+ "exportarr": {
+ "args": [
+ "readarr"
+ ],
+ "enabled": true,
+ "env": {
+ "CONFIG": "/config/config.xml",
+ "INTERFACE": "0.0.0.0",
+ "PORT": "{{ .Values.service.metrics.ports.metrics.port }}",
+ "URL": "{{ printf \"http://%v:%v\" (include \"tc.v1.common.lib.chart.names.fullname\" $) .Values.service.main.ports.main.port }}"
+ },
+ "imageSelector": "exportarrImage",
+ "primary": true,
+ "probes": {
+ "liveness": {
+ "enabled": true,
+ "path": "/healthz",
+ "port": "{{ .Values.service.metrics.ports.metrics.port }}",
+ "type": "http"
+ },
+ "readiness": {
+ "enabled": true,
+ "path": "/healthz",
+ "port": "{{ .Values.service.metrics.ports.metrics.port }}",
+ "type": "http"
+ },
+ "startup": {
+ "enabled": true,
+ "path": "/healthz",
+ "port": "{{ .Values.service.metrics.ports.metrics.port }}",
+ "type": "http"
+ }
+ }
+ }
+ }
+ },
+ "replicas": 1,
+ "strategy": "RollingUpdate",
+ "type": "Deployment"
+ },
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "env": {
+ "READARR__AUTHENTICATION_METHOD": "",
+ "READARR__PORT": "{{ .Values.service.main.ports.main.port }}"
+ },
+ "envList": [],
+ "extraArgs": [],
+ "probes": {
+ "liveness": {
+ "enabled": true,
+ "path": "/ping",
+ "type": "http"
+ },
+ "readiness": {
+ "enabled": true,
+ "path": "/ping",
+ "type": "http"
+ },
+ "startup": {
+ "enabled": true,
+ "path": "/ping",
+ "type": "http"
+ }
+ }
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 22,
+ "namespace": "ix-readarr",
+ "chart_metadata": {
+ "name": "readarr",
+ "home": "https://truecharts.org/charts/stable/readarr",
+ "sources": [
+ "https://ghcr.io/onedr0p/exportarr",
+ "https://ghcr.io/onedr0p/readarr-develop",
+ "https://github.com/Readarr/Readarr",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/readarr",
+ "https://readarr.com"
+ ],
+ "version": "22.0.12",
+ "description": "A fork of Radarr to work with Books & AudioBooks",
+ "keywords": [
+ "readarr",
+ "torrent",
+ "usenet",
+ "AudioBooks",
+ "ebooks"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/readarr.webp",
+ "apiVersion": "v2",
+ "appVersion": "0.3.27.2538",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "media",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "22.0.12"
+ },
+ "id": "readarr",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/readarr",
+ "dataset": "Apps/ix-applications/releases/readarr",
+ "status": "STOPPED",
+ "used_ports": [],
+ "pod_status": {
+ "desired": 0,
+ "available": 0
+ },
+ "update_available": false,
+ "human_version": "0.3.27.2538_22.0.12",
+ "human_latest_version": "0.3.27.2538_22.0.12",
+ "container_images_update_available": false,
+ "portals": {
+ "open": [
+ "http://10.0.0.20:8787/"
+ ]
+ }
+}
diff --git a/clustertool/testdata/truenas_exports/scrutiny.json b/clustertool/testdata/truenas_exports/scrutiny.json
new file mode 100644
index 0000000000000..0ab7aa1b68c89
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/scrutiny.json
@@ -0,0 +1,313 @@
+{
+ "name": "scrutiny",
+ "info": {
+ "first_deployed": "2024-04-24T17:43:55.839282862+10:00",
+ "last_deployed": "2024-07-03T20:32:23.745786939+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing scrutiny by TrueCharts.\n\n\n## Connecting externally\nYou can use this Chart by opening one of the following links in your browser:\n- host: $node_ip\npath: /\nport: \"10151\"\nportalName: open\nprotocol: http\nurl: http://$node_ip:10151/\nuseNodeIP: true\n## Sources for scrutiny\n- https://ghcr.io/analogj/scrutiny\n- https://github.com/truecharts/charts/tree/master/charts/stable/scrutiny\n\nSee more for **scrutiny** at (https://truecharts.org/charts/stable/scrutiny)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-scrutiny",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-scrutiny",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/analogj/scrutiny",
+ "tag": "v0.8.1-omnibus@sha256:66a65d1d7f2bf330a55e0bb073a3b2496a7b61dc6414c8c53550bc0c3f6885dd"
+ },
+ "imagePullSecretList": [],
+ "ingress": {
+ "main": {
+ "enabled": false
+ }
+ },
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-scrutiny",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "networkPolicy": [],
+ "persistence": {
+ "config": {
+ "enabled": true,
+ "mountPath": "/opt/scrutiny/config",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ },
+ "influxdb": {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Apps/vApps/Scrutiny/db",
+ "mountPath": "/opt/scrutiny/influxdb",
+ "readOnly": false,
+ "type": "hostPath"
+ },
+ "udev": {
+ "enabled": true,
+ "hostPath": "/run/udev",
+ "mountPath": "/run/udev",
+ "readOnly": true,
+ "type": "hostPath"
+ },
+ "varrun": {
+ "enabled": true
+ }
+ },
+ "persistenceList": [],
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": true
+ }
+ },
+ "release_name": "scrutiny",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 0
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "PUID": 568,
+ "UMASK": "0022",
+ "advanced": false,
+ "allowPrivilegeEscalation": true,
+ "capabilities": {
+ "add": [
+ "SYS_RAWIO",
+ "SYS_ADMIN"
+ ]
+ },
+ "privileged": true,
+ "readOnlyRootFilesystem": false,
+ "runAsGroup": 0,
+ "runAsNonRoot": false,
+ "runAsUser": 0
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "main": {
+ "port": 10151,
+ "targetPort": 8080
+ }
+ },
+ "type": "LoadBalancer"
+ }
+ },
+ "serviceList": [],
+ "workload": {
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "env": {
+ "COLLECTOR_CRON_SCHEDULE": "0 0 * * *",
+ "COLLECTOR_HOST_ID": "TrueNAS Scale"
+ },
+ "envList": [],
+ "extraArgs": [],
+ "probes": {
+ "liveness": {
+ "path": "/api/health"
+ },
+ "readiness": {
+ "path": "/api/health"
+ },
+ "startup": {
+ "path": "/api/health"
+ }
+ }
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 15,
+ "namespace": "ix-scrutiny",
+ "chart_metadata": {
+ "name": "scrutiny",
+ "home": "https://truecharts.org/charts/stable/scrutiny",
+ "sources": [
+ "https://ghcr.io/analogj/scrutiny",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/scrutiny"
+ ],
+ "version": "11.0.8",
+ "description": "Scrutiny WebUI for smartd S.M.A.R.T monitoring. Scrutiny is a Hard Drive Health Dashboard & Monitoring solution.",
+ "keywords": [
+ "scrutiny"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/scrutiny.webp",
+ "apiVersion": "v2",
+ "appVersion": "0.8.1",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "utilities",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "11.0.8"
+ },
+ "id": "scrutiny",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/scrutiny",
+ "dataset": "Apps/ix-applications/releases/scrutiny",
+ "status": "ACTIVE",
+ "used_ports": [
+ {
+ "port": 10151,
+ "protocol": "TCP"
+ }
+ ],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "0.8.1_11.0.8",
+ "human_latest_version": "0.8.1_11.0.8",
+ "container_images_update_available": false,
+ "portals": {
+ "open": [
+ "http://10.0.0.20:10151/"
+ ]
+ }
+}
diff --git a/clustertool/testdata/truenas_exports/sonarr.json b/clustertool/testdata/truenas_exports/sonarr.json
new file mode 100644
index 0000000000000..5510e24d200ba
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/sonarr.json
@@ -0,0 +1,412 @@
+{
+ "name": "sonarr",
+ "info": {
+ "first_deployed": "2024-04-24T17:45:36.659038612+10:00",
+ "last_deployed": "2024-07-03T20:33:04.651431797+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing sonarr by TrueCharts.\n\n\n## Connecting externally\nYou can use this Chart by opening one of the following links in your browser:\n- host: $node_ip\npath: /\nport: \"8989\"\nportalName: open\nprotocol: http\nurl: http://$node_ip:8989/\nuseNodeIP: true\n## Sources for sonarr\n- https://ghcr.io/onedr0p/exportarr\n- https://ghcr.io/onedr0p/sonarr\n- https://github.com/Sonarr/Sonarr\n- https://github.com/truecharts/charts/tree/master/charts/stable/sonarr\n\nSee more for **sonarr** at (https://truecharts.org/charts/stable/sonarr)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-sonarr",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "exportarrImage": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/onedr0p/exportarr",
+ "tag": "v2.0.1@sha256:727e7bc8f2f0934a2117978c59f4476b954018b849a010ea6cfb380bd6539644"
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-sonarr",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/onedr0p/sonarr",
+ "tag": "4.0.4.1491@sha256:b513d3836c5b86d3e5c2eb7cb4908e0002856d922b0a360f136781aaa89ef38a"
+ },
+ "imagePullSecretList": [],
+ "ingress": {
+ "main": {
+ "enabled": false
+ }
+ },
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-sonarr",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "metrics": {
+ "main": {
+ "enabled": false,
+ "endpoints": [
+ {
+ "path": "/metrics",
+ "port": "metrics"
+ }
+ ],
+ "prometheusRule": {
+ "enabled": false
+ },
+ "targetSelector": "metrics",
+ "type": "servicemonitor"
+ }
+ },
+ "networkPolicy": [],
+ "persistence": {
+ "config": {
+ "enabled": true,
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "targetSelector": {
+ "exportarr": {
+ "exportarr": {
+ "mountPath": "/config",
+ "readOnly": true
+ }
+ },
+ "main": {
+ "main": {
+ "mountPath": "/config"
+ }
+ }
+ },
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ }
+ },
+ "persistenceList": [
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Scratch/Scratch/arrTemp/Downloads",
+ "mountPath": "/Downloads",
+ "readOnly": false,
+ "type": "hostPath"
+ },
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Main/Media/Other",
+ "mountPath": "/Media/Other",
+ "readOnly": false,
+ "type": "hostPath"
+ },
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Main/Media/Shows",
+ "mountPath": "/Media/Shows",
+ "readOnly": false,
+ "type": "hostPath"
+ }
+ ],
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": true
+ }
+ },
+ "release_name": "sonarr",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 0
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "UMASK": "0022",
+ "advanced": false,
+ "runAsGroup": 568,
+ "runAsUser": 568
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "main": {
+ "port": 8989
+ }
+ },
+ "type": "LoadBalancer"
+ },
+ "metrics": {
+ "enabled": true,
+ "ports": {
+ "metrics": {
+ "enabled": true,
+ "port": 8990,
+ "targetSelector": "exportarr"
+ }
+ },
+ "targetSelector": "exportarr",
+ "type": "ClusterIP"
+ }
+ },
+ "serviceList": [],
+ "updated": true,
+ "workload": {
+ "exportarr": {
+ "enabled": true,
+ "podSpec": {
+ "containers": {
+ "exportarr": {
+ "args": [
+ "sonarr"
+ ],
+ "enabled": true,
+ "env": {
+ "CONFIG": "/config/config.xml",
+ "INTERFACE": "0.0.0.0",
+ "PORT": "{{ .Values.service.metrics.ports.metrics.port }}",
+ "URL": "{{ printf \"http://%v:%v\" (include \"tc.v1.common.lib.chart.names.fullname\" $) .Values.service.main.ports.main.port }}"
+ },
+ "imageSelector": "exportarrImage",
+ "primary": true,
+ "probes": {
+ "liveness": {
+ "enabled": true,
+ "path": "/healthz",
+ "port": "{{ .Values.service.metrics.ports.metrics.port }}",
+ "type": "http"
+ },
+ "readiness": {
+ "enabled": true,
+ "path": "/healthz",
+ "port": "{{ .Values.service.metrics.ports.metrics.port }}",
+ "type": "http"
+ },
+ "startup": {
+ "enabled": true,
+ "path": "/healthz",
+ "port": "{{ .Values.service.metrics.ports.metrics.port }}",
+ "type": "http"
+ }
+ }
+ }
+ }
+ },
+ "replicas": 1,
+ "strategy": "RollingUpdate",
+ "type": "Deployment"
+ },
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "env": {
+ "SONARR__AUTHENTICATION_METHOD": "",
+ "SONARR__PORT": "{{ .Values.service.main.ports.main.port }}"
+ },
+ "envList": [],
+ "extraArgs": [],
+ "probes": {
+ "liveness": {
+ "enabled": true,
+ "path": "/ping",
+ "type": "http"
+ },
+ "readiness": {
+ "enabled": true,
+ "path": "/ping",
+ "type": "http"
+ },
+ "startup": {
+ "enabled": true,
+ "path": "/ping",
+ "type": "http"
+ }
+ }
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 17,
+ "namespace": "ix-sonarr",
+ "chart_metadata": {
+ "name": "sonarr",
+ "home": "https://truecharts.org/charts/stable/sonarr",
+ "sources": [
+ "https://ghcr.io/onedr0p/exportarr",
+ "https://ghcr.io/onedr0p/sonarr",
+ "https://github.com/Sonarr/Sonarr",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/sonarr"
+ ],
+ "version": "22.0.10",
+ "description": "Smart PVR for newsgroup and bittorrent users",
+ "keywords": [
+ "sonarr",
+ "torrent",
+ "usenet"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/sonarr.webp",
+ "apiVersion": "v2",
+ "appVersion": "4.0.4.1491",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "media",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "22.0.10"
+ },
+ "id": "sonarr",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/sonarr",
+ "dataset": "Apps/ix-applications/releases/sonarr",
+ "status": "ACTIVE",
+ "used_ports": [
+ {
+ "port": 8989,
+ "protocol": "TCP"
+ }
+ ],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "4.0.4.1491_22.0.10",
+ "human_latest_version": "4.0.4.1491_22.0.10",
+ "container_images_update_available": false,
+ "portals": {
+ "open": [
+ "http://10.0.0.20:8989/"
+ ]
+ }
+}
diff --git a/clustertool/testdata/truenas_exports/stash.json b/clustertool/testdata/truenas_exports/stash.json
new file mode 100644
index 0000000000000..a2e2bcf99e3fb
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/stash.json
@@ -0,0 +1,350 @@
+{
+ "name": "stash",
+ "info": {
+ "first_deployed": "2024-06-03T03:53:17.269612035+10:00",
+ "last_deployed": "2024-07-03T20:33:56.597971608+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing stash by TrueCharts.\n\n\n## Connecting externally\nYou can use this Chart by opening one of the following links in your browser:\n- host: $node_ip\npath: /\nport: \"9999\"\nportalName: open\nprotocol: http\nurl: http://$node_ip:9999/\nuseNodeIP: true\n## Sources for stash\n- https://github.com/stashapp/stash\n- https://github.com/truecharts/charts/tree/master/charts/stable/stash\n- https://hub.docker.com/r/stashapp/stash\n\nSee more for **stash** at (https://truecharts.org/charts/stable/stash)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-stash",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-stash",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "stashapp/stash",
+ "tag": "v0.25.1@sha256:6b8814b61e4fe77bc910bec858dd45e0970c8af6f439c066317ae68f03af4f91"
+ },
+ "imagePullSecretList": [],
+ "ingress": {
+ "main": {
+ "enabled": false
+ }
+ },
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-stash",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "networkPolicy": [],
+ "persistence": {
+ "blobs": {
+ "enabled": true,
+ "mountPath": "/blobs",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ },
+ "cache": {
+ "enabled": true,
+ "mountPath": "/cache",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "type": "pvc",
+ "volsync": [],
+ "volumeSnapshots": []
+ },
+ "config": {
+ "enabled": true,
+ "mountPath": "/root/.stash",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "type": "pvc",
+ "volsync": [],
+ "volumeSnapshots": []
+ },
+ "data": {
+ "enabled": true,
+ "mountPath": "/data",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "type": "pvc",
+ "volsync": [],
+ "volumeSnapshots": []
+ },
+ "generated": {
+ "enabled": true,
+ "mountPath": "/generated",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "type": "pvc",
+ "volsync": [],
+ "volumeSnapshots": []
+ },
+ "metadata": {
+ "enabled": true,
+ "mountPath": "/metadata",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "type": "pvc",
+ "volsync": [],
+ "volumeSnapshots": []
+ }
+ },
+ "persistenceList": [
+ {
+ "autoPermissions": {
+ "enabled": false
+ },
+ "enabled": true,
+ "hostPath": "/mnt/Main/Storage/3X",
+ "mountPath": "/3X",
+ "readOnly": false,
+ "type": "hostPath"
+ }
+ ],
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": true
+ }
+ },
+ "release_name": "stash",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 1
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "PUID": 568,
+ "UMASK": "0022",
+ "advanced": false,
+ "readOnlyRootFilesystem": false,
+ "runAsGroup": 0,
+ "runAsNonRoot": false,
+ "runAsUser": 0
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "main": {
+ "port": 9999
+ }
+ },
+ "type": "LoadBalancer"
+ }
+ },
+ "serviceList": [],
+ "workload": {
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "env": {
+ "STASH_CACHE": "/cache",
+ "STASH_GENERATED": "/generated",
+ "STASH_METADATA": "/metadata",
+ "STASH_PORT": "{{ .Values.service.main.ports.main.port }}",
+ "STASH_STASH": "/data"
+ },
+ "envList": [],
+ "extraArgs": []
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 6,
+ "namespace": "ix-stash",
+ "chart_metadata": {
+ "name": "stash",
+ "home": "https://truecharts.org/charts/stable/stash",
+ "sources": [
+ "https://github.com/stashapp/stash",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/stash",
+ "https://hub.docker.com/r/stashapp/stash"
+ ],
+ "version": "16.0.8",
+ "description": "An organizer for your porn, written in Go",
+ "keywords": [
+ "porn"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/stash.webp",
+ "apiVersion": "v2",
+ "appVersion": "0.25.1",
+ "annotations": {
+ "truecharts.org/category": "media",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "16.0.8"
+ },
+ "id": "stash",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/stash",
+ "dataset": "Apps/ix-applications/releases/stash",
+ "status": "ACTIVE",
+ "used_ports": [
+ {
+ "port": 9999,
+ "protocol": "TCP"
+ }
+ ],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "0.25.1_16.0.8",
+ "human_latest_version": "0.25.1_16.0.8",
+ "container_images_update_available": false,
+ "portals": {
+ "open": [
+ "http://10.0.0.20:9999/"
+ ]
+ }
+}
diff --git a/clustertool/testdata/truenas_exports/tautulli.json b/clustertool/testdata/truenas_exports/tautulli.json
new file mode 100644
index 0000000000000..b8aafd8142264
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/tautulli.json
@@ -0,0 +1,269 @@
+{
+ "name": "tautulli",
+ "info": {
+ "first_deployed": "2024-04-24T17:46:59.903402736+10:00",
+ "last_deployed": "2024-07-03T20:34:52.413866131+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing tautulli by TrueCharts.\n\n\n## Connecting externally\nYou can use this Chart by opening one of the following links in your browser:\n- host: $node_ip\npath: /\nport: \"8181\"\nportalName: open\nprotocol: http\nurl: http://$node_ip:8181/\nuseNodeIP: true\n## Sources for tautulli\n- https://ghcr.io/onedr0p/tautulli\n- https://github.com/Tautulli/Tautulli\n- https://github.com/truecharts/charts/tree/master/charts/stable/tautulli\n\nSee more for **tautulli** at (https://truecharts.org/charts/stable/tautulli)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "credentialsList": [
+ {
+ "accessKey": "placeholderkey",
+ "bucket": "pvccrap-tautulli",
+ "encrKey": "MYSECRETPASSPHRASE",
+ "name": "backblaze",
+ "path": "",
+ "secretKey": "PLACEHOLDERSECRETKEY",
+ "type": "s3",
+ "url": "s3.us-west-004.backblazeb2.com"
+ }
+ ],
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-tautulli",
+ "upgradeMetadata": {}
+ },
+ "stopAll": false
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "ghcr.io/onedr0p/tautulli",
+ "tag": "2.13.4@sha256:633a57b2f8634feb67811064ec3fa52f40a70641be927fdfda6f5d91ebbd5d73"
+ },
+ "imagePullSecretList": [],
+ "ingress": {
+ "main": {
+ "enabled": false
+ }
+ },
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-tautulli",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "networkPolicy": [],
+ "persistence": {
+ "config": {
+ "enabled": true,
+ "mountPath": "/config",
+ "readOnly": false,
+ "size": "256Gi",
+ "static": {
+ "mode": "disabled"
+ },
+ "storageClass": "",
+ "type": "pvc",
+ "volsync": [
+ {
+ "credentials": "backblaze",
+ "dest": {
+ "enabled": false
+ },
+ "name": "config",
+ "src": {
+ "enabled": true
+ },
+ "type": "restic"
+ }
+ ],
+ "volumeSnapshots": []
+ }
+ },
+ "persistenceList": [],
+ "podOptions": {
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": true
+ }
+ },
+ "release_name": "tautulli",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 0
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "scaleExternalInterface": [],
+ "securityContext": {
+ "container": {
+ "UMASK": "0022",
+ "advanced": false,
+ "runAsGroup": 568,
+ "runAsUser": 568
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "Always",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "enabled": true,
+ "loadBalancerIP": "",
+ "ports": {
+ "main": {
+ "port": 8181,
+ "targetPort": 8181
+ }
+ },
+ "type": "LoadBalancer"
+ }
+ },
+ "serviceList": [],
+ "workload": {
+ "main": {
+ "podSpec": {
+ "containers": {
+ "main": {
+ "advanced": false,
+ "envList": [],
+ "extraArgs": []
+ }
+ }
+ },
+ "replicas": 1,
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 24,
+ "namespace": "ix-tautulli",
+ "chart_metadata": {
+ "name": "tautulli",
+ "home": "https://truecharts.org/charts/stable/tautulli",
+ "sources": [
+ "https://ghcr.io/onedr0p/tautulli",
+ "https://github.com/Tautulli/Tautulli",
+ "https://github.com/truecharts/charts/tree/master/charts/stable/tautulli"
+ ],
+ "version": "19.0.8",
+ "description": "A Python based monitoring and tracking tool for Plex Media Server",
+ "keywords": [
+ "tautulli",
+ "plex"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/tautulli.webp",
+ "apiVersion": "v2",
+ "appVersion": "2.13.4",
+ "annotations": {
+ "max_scale_version": "24.04.1",
+ "min_scale_version": "24.04.0",
+ "truecharts.org/SCALE-support": "true",
+ "truecharts.org/category": "media",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "stable"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.10",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "19.0.8"
+ },
+ "id": "tautulli",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "stable",
+ "path": "/mnt/Apps/ix-applications/releases/tautulli",
+ "dataset": "Apps/ix-applications/releases/tautulli",
+ "status": "ACTIVE",
+ "used_ports": [
+ {
+ "port": 8181,
+ "protocol": "TCP"
+ }
+ ],
+ "pod_status": {
+ "desired": 1,
+ "available": 1
+ },
+ "update_available": false,
+ "human_version": "2.13.4_19.0.8",
+ "human_latest_version": "2.13.4_19.0.8",
+ "container_images_update_available": false,
+ "portals": {
+ "open": [
+ "http://10.0.0.20:8181/"
+ ]
+ }
+}
diff --git a/clustertool/testdata/truenas_exports/volsync.json b/clustertool/testdata/truenas_exports/volsync.json
new file mode 100644
index 0000000000000..bd8e1e1585f6e
--- /dev/null
+++ b/clustertool/testdata/truenas_exports/volsync.json
@@ -0,0 +1,828 @@
+{
+ "name": "volsync",
+ "info": {
+ "first_deployed": "2024-07-03T19:59:08.745143345+10:00",
+ "last_deployed": "2024-07-08T23:47:19.42822429+10:00",
+ "deleted": "",
+ "description": "Upgrade complete",
+ "status": "deployed",
+ "notes": "\n# Thank you for installing volsync by TrueCharts.\n\n\n## Sources for volsync\n- https://github.com/truecharts/charts/tree/master/charts/system/volsync\n- https://github.com/volsync/volsync\n- https://github.com/volsync/volsync-helm-chart\n- https://quay.io/backube/volsync\n- https://quay.io/brancz/kube-rbac-proxy\n- https://volsync.readthedocs.io/\n\nSee more for **volsync** at (https://truecharts.org/charts/system/volsync)\n\n## Documentation\nPlease check out the TrueCharts documentation on:\nhttps://truecharts.org\n\nOpenSource can only exist with your help, please consider supporting TrueCharts:\nhttps://truecharts.org/sponsor\n"
+ },
+ "config": {
+ "TZ": "Australia/Victoria",
+ "addons": {
+ "codeserver": {
+ "enabled": false
+ },
+ "netshoot": {
+ "enabled": false
+ },
+ "vpn": {
+ "type": "disabled"
+ }
+ },
+ "deviceList": [],
+ "docs": {
+ "confirmDocs": true
+ },
+ "donateNag": {
+ "confirmDonate": true
+ },
+ "global": {
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-volsync",
+ "upgradeMetadata": {}
+ },
+ "stopAll": true
+ },
+ "image": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "quay.io/backube/volsync",
+ "tag": "0.9.1"
+ },
+ "imagePullSecretList": [],
+ "ingressList": [],
+ "ixCertificateAuthorities": {},
+ "ixChartContext": {
+ "addNvidiaRuntimeClass": false,
+ "hasNFSCSI": true,
+ "hasSMBCSI": true,
+ "isInstall": false,
+ "isStopped": false,
+ "isUpdate": true,
+ "isUpgrade": false,
+ "kubernetes_config": {
+ "cluster_cidr": "172.16.0.0/16",
+ "cluster_dns_ip": "172.17.0.10",
+ "service_cidr": "172.17.0.0/16"
+ },
+ "nfsProvisioner": "nfs.csi.k8s.io",
+ "nvidiaRuntimeClassName": "nvidia",
+ "operation": "UPDATE",
+ "smbProvisioner": "smb.csi.k8s.io",
+ "storageClassName": "ix-storage-class-volsync",
+ "upgradeMetadata": {}
+ },
+ "ixExternalInterfacesConfiguration": [],
+ "ixExternalInterfacesConfigurationNames": [],
+ "ixVolumes": [],
+ "manageCRDs": true,
+ "manageVSCCRD": true,
+ "metrics": {
+ "main": {
+ "enabled": false,
+ "endpoints": [
+ {
+ "path": "/metrics",
+ "port": "metrics"
+ }
+ ],
+ "targetSelector": "metrics",
+ "type": "servicemonitor"
+ }
+ },
+ "networkPolicy": [],
+ "podOptions": {
+ "automountServiceAccountToken": true,
+ "expertPodOpts": false
+ },
+ "portal": {
+ "open": {
+ "enabled": false
+ }
+ },
+ "proxyImage": {
+ "pullPolicy": "IfNotPresent",
+ "repository": "quay.io/brancz/kube-rbac-proxy",
+ "tag": "v0.14.4@sha256:40d0d5d0032f9bd689bb6ee7d1960048c295e019b7110d5fadea5aff9599baa5"
+ },
+ "rbac": {
+ "cluster": {
+ "allServiceAccounts": true,
+ "clusterWide": true,
+ "enabled": true,
+ "primary": false,
+ "rules": [
+ {
+ "apiGroups": [
+ "authentication.k8s.io"
+ ],
+ "resources": [
+ "tokenreviews"
+ ],
+ "verbs": [
+ "create"
+ ]
+ },
+ {
+ "apiGroups": [
+ "authorization.k8s.io"
+ ],
+ "resources": [
+ "subjectaccessreviews"
+ ],
+ "verbs": [
+ "create"
+ ]
+ },
+ {
+ "apiGroups": [
+ "apps"
+ ],
+ "resources": [
+ "deployments"
+ ],
+ "verbs": [
+ "create",
+ "delete",
+ "deletecollection",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ "batch"
+ ],
+ "resources": [
+ "jobs"
+ ],
+ "verbs": [
+ "create",
+ "delete",
+ "deletecollection",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ ""
+ ],
+ "resources": [
+ "configmaps"
+ ],
+ "verbs": [
+ "get",
+ "list",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ ""
+ ],
+ "resources": [
+ "events"
+ ],
+ "verbs": [
+ "create",
+ "patch",
+ "update"
+ ]
+ },
+ {
+ "apiGroups": [
+ ""
+ ],
+ "resources": [
+ "namespaces"
+ ],
+ "verbs": [
+ "get",
+ "list",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ ""
+ ],
+ "resources": [
+ "nodes"
+ ],
+ "verbs": [
+ "get",
+ "list",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ ""
+ ],
+ "resources": [
+ "persistentvolumeclaims"
+ ],
+ "verbs": [
+ "create",
+ "delete",
+ "deletecollection",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ ""
+ ],
+ "resources": [
+ "persistentvolumeclaims/finalizers"
+ ],
+ "verbs": [
+ "create",
+ "delete",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ ""
+ ],
+ "resources": [
+ "persistentvolumes"
+ ],
+ "verbs": [
+ "get",
+ "list",
+ "patch",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ ""
+ ],
+ "resources": [
+ "pods"
+ ],
+ "verbs": [
+ "get",
+ "list",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ ""
+ ],
+ "resources": [
+ "pods/log"
+ ],
+ "verbs": [
+ "get",
+ "list",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ ""
+ ],
+ "resources": [
+ "secrets"
+ ],
+ "verbs": [
+ "create",
+ "delete",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ ""
+ ],
+ "resources": [
+ "serviceaccounts"
+ ],
+ "verbs": [
+ "create",
+ "delete",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ ""
+ ],
+ "resources": [
+ "services"
+ ],
+ "verbs": [
+ "create",
+ "delete",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ "events.k8s.io"
+ ],
+ "resources": [
+ "events"
+ ],
+ "verbs": [
+ "create",
+ "patch",
+ "update"
+ ]
+ },
+ {
+ "apiGroups": [
+ "populator.storage.k8s.io"
+ ],
+ "resources": [
+ "volumepopulators"
+ ],
+ "verbs": [
+ "create",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ "rbac.authorization.k8s.io"
+ ],
+ "resources": [
+ "rolebindings"
+ ],
+ "verbs": [
+ "create",
+ "delete",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ "rbac.authorization.k8s.io"
+ ],
+ "resources": [
+ "roles"
+ ],
+ "verbs": [
+ "create",
+ "delete",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ "security.openshift.io"
+ ],
+ "resources": [
+ "securitycontextconstraints"
+ ],
+ "verbs": [
+ "create",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ "security.openshift.io"
+ ],
+ "resourceNames": [
+ "volsync-privileged-mover"
+ ],
+ "resources": [
+ "securitycontextconstraints"
+ ],
+ "verbs": [
+ "use"
+ ]
+ },
+ {
+ "apiGroups": [
+ "snapshot.storage.k8s.io"
+ ],
+ "resources": [
+ "volumesnapshots"
+ ],
+ "verbs": [
+ "create",
+ "delete",
+ "deletecollection",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ "storage.k8s.io"
+ ],
+ "resources": [
+ "storageclasses"
+ ],
+ "verbs": [
+ "get",
+ "list",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ "volsync.backube"
+ ],
+ "resources": [
+ "replicationdestinations"
+ ],
+ "verbs": [
+ "create",
+ "delete",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ "volsync.backube"
+ ],
+ "resources": [
+ "replicationdestinations/finalizers"
+ ],
+ "verbs": [
+ "create",
+ "delete",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ "volsync.backube"
+ ],
+ "resources": [
+ "replicationdestinations/status"
+ ],
+ "verbs": [
+ "get",
+ "patch",
+ "update"
+ ]
+ },
+ {
+ "apiGroups": [
+ "volsync.backube"
+ ],
+ "resources": [
+ "replicationsources"
+ ],
+ "verbs": [
+ "create",
+ "delete",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ "volsync.backube"
+ ],
+ "resources": [
+ "replicationsources/finalizers"
+ ],
+ "verbs": [
+ "create",
+ "delete",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ },
+ {
+ "apiGroups": [
+ "volsync.backube"
+ ],
+ "resources": [
+ "replicationsources/status"
+ ],
+ "verbs": [
+ "get",
+ "patch",
+ "update"
+ ]
+ }
+ ]
+ },
+ "main": {
+ "clusterWide": false,
+ "enabled": true,
+ "primary": true,
+ "rules": [
+ {
+ "apiGroups": [
+ ""
+ ],
+ "resources": [
+ "configmaps"
+ ],
+ "verbs": [
+ "get",
+ "list",
+ "watch",
+ "create",
+ "update",
+ "patch",
+ "delete"
+ ]
+ },
+ {
+ "apiGroups": [
+ "coordination.k8s.io"
+ ],
+ "resources": [
+ "leases"
+ ],
+ "verbs": [
+ "get",
+ "list",
+ "watch",
+ "create",
+ "update",
+ "patch",
+ "delete"
+ ]
+ },
+ {
+ "apiGroups": [
+ ""
+ ],
+ "resources": [
+ "events"
+ ],
+ "verbs": [
+ "create",
+ "patch"
+ ]
+ }
+ ]
+ }
+ },
+ "release_name": "volsync",
+ "resources": {
+ "limits": {
+ "amd.com/gpu": 0,
+ "cpu": "4000m",
+ "gpu.intel.com/i915": 0,
+ "memory": "8Gi",
+ "nvidia.com/gpu": 0
+ },
+ "requests": {
+ "cpu": "10m",
+ "memory": "50Mi"
+ }
+ },
+ "securityContext": {
+ "container": {
+ "UMASK": "0022",
+ "advanced": false,
+ "runAsGroup": 568,
+ "runAsUser": 568
+ },
+ "pod": {
+ "fsGroup": 568,
+ "fsGroupChangePolicy": "OnRootMismatch",
+ "supplementalGroups": []
+ }
+ },
+ "service": {
+ "main": {
+ "ports": {
+ "main": {
+ "port": 8081,
+ "protocol": "http",
+ "targetPort": 8081
+ }
+ }
+ },
+ "metrics": {
+ "enabled": true,
+ "ports": {
+ "metrics": {
+ "enabled": true,
+ "port": 8443,
+ "protocol": "https",
+ "targetPort": 8443
+ }
+ },
+ "type": "ClusterIP"
+ }
+ },
+ "serviceAccount": {
+ "main": {
+ "enabled": true,
+ "primary": true
+ }
+ },
+ "workload": {
+ "main": {
+ "podSpec": {
+ "containers": {
+ "kube-rbac-proxy": {
+ "args": [
+ "--secure-listen-address=0.0.0.0:8443",
+ "--upstream=http://127.0.0.1:8080/",
+ "--logtostderr=true",
+ "--tls-min-version=VersionTLS12",
+ "--v=0"
+ ],
+ "enabled": true,
+ "imageSelector": "proxyImage",
+ "probes": {
+ "liveness": {
+ "port": 8443,
+ "type": "tcp"
+ },
+ "readiness": {
+ "port": 8443,
+ "type": "tcp"
+ },
+ "startup": {
+ "port": 8443,
+ "type": "tcp"
+ }
+ },
+ "resources": {
+ "limits": {
+ "cpu": "500m",
+ "memory": "128Mi"
+ },
+ "requests": {
+ "cpu": "5m",
+ "memory": "64Mi"
+ }
+ }
+ },
+ "main": {
+ "advanced": false,
+ "args": [
+ "--health-probe-bind-address=:8081",
+ "--metrics-bind-address=127.0.0.1:8080",
+ "--leader-elect",
+ "--rclone-container-image={{ printf \"%s:%s\" .Values.image.repository .Values.image.tag }}",
+ "--restic-container-image={{ printf \"%s:%s\" .Values.image.repository .Values.image.tag }}",
+ "--rsync-container-image={{ printf \"%s:%s\" .Values.image.repository .Values.image.tag }}",
+ "--rsync-tls-container-image={{ printf \"%s:%s\" .Values.image.repository .Values.image.tag }}",
+ "--syncthing-container-image={{ printf \"%s:%s\" .Values.image.repository .Values.image.tag }}",
+ "--scc-name=volsync-privileged-mover"
+ ],
+ "command": [
+ "/manager"
+ ],
+ "envList": [],
+ "extraArgs": [],
+ "probes": {
+ "liveness": {
+ "path": "/healthz"
+ },
+ "readiness": {
+ "path": "/readyz"
+ },
+ "startup": {
+ "path": "/readyz"
+ }
+ }
+ }
+ }
+ },
+ "replicas": 1,
+ "strategy": "RollingUpdate",
+ "type": "Deployment"
+ }
+ }
+ },
+ "version": 4,
+ "namespace": "ix-volsync",
+ "chart_metadata": {
+ "name": "volsync",
+ "home": "https://truecharts.org/charts/system/volsync",
+ "sources": [
+ "https://github.com/truecharts/charts/tree/master/charts/system/volsync",
+ "https://github.com/volsync/volsync",
+ "https://github.com/volsync/volsync-helm-chart",
+ "https://quay.io/backube/volsync",
+ "https://quay.io/brancz/kube-rbac-proxy",
+ "https://volsync.readthedocs.io/"
+ ],
+ "version": "1.0.8",
+ "description": "volsync is a storage backup and synchronisation tool.",
+ "keywords": [
+ "volsync",
+ "ingress"
+ ],
+ "maintainers": [
+ {
+ "name": "TrueCharts",
+ "email": "info@truecharts.org",
+ "url": "https://truecharts.org"
+ }
+ ],
+ "icon": "https://truecharts.org/img/hotlink-ok/chart-icons/volsync.webp",
+ "apiVersion": "v2",
+ "appVersion": "0.9.1",
+ "annotations": {
+ "truecharts.org/category": "network",
+ "truecharts.org/max_helm_version": "3.14",
+ "truecharts.org/min_helm_version": "3.11",
+ "truecharts.org/train": "system"
+ },
+ "kubeVersion": ">=1.24.0-0",
+ "dependencies": [
+ {
+ "name": "common",
+ "version": "23.0.0",
+ "repository": "oci://tccr.io/truecharts",
+ "enabled": true
+ }
+ ],
+ "type": "application",
+ "latest_chart_version": "1.0.8"
+ },
+ "id": "volsync",
+ "catalog": "TRUECHARTS",
+ "catalog_train": "system",
+ "path": "/mnt/Apps/ix-applications/releases/volsync",
+ "dataset": "Apps/ix-applications/releases/volsync",
+ "status": "STOPPED",
+ "used_ports": [],
+ "pod_status": {
+ "desired": 0,
+ "available": 0
+ },
+ "update_available": false,
+ "human_version": "0.9.1_1.0.8",
+ "human_latest_version": "0.9.1_1.0.8",
+ "container_images_update_available": false,
+ "portals": {}
+}
diff --git a/clustertool/testdata/updater/stable/my-app/Chart.yaml b/clustertool/testdata/updater/stable/my-app/Chart.yaml
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/clustertool/testdata/values_yaml/emptyValues.yaml b/clustertool/testdata/values_yaml/emptyValues.yaml
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/clustertool/testdata/values_yaml/malformedValues.yaml b/clustertool/testdata/values_yaml/malformedValues.yaml
new file mode 100644
index 0000000000000..773b222228015
--- /dev/null
+++ b/clustertool/testdata/values_yaml/malformedValues.yaml
@@ -0,0 +1 @@
+image
diff --git a/clustertool/testdata/values_yaml/multiImageValues.yaml b/clustertool/testdata/values_yaml/multiImageValues.yaml
new file mode 100644
index 0000000000000..d5f492fae7fe5
--- /dev/null
+++ b/clustertool/testdata/values_yaml/multiImageValues.yaml
@@ -0,0 +1,70 @@
+image:
+ repository: author/image
+ tag: 1.0.0
+
+# This should not block the go tests
+imageSelector: something
+
+dockerHub1Image:
+ repository: docker.io/author/image
+ tag: 1.0.0
+
+dockerHub2Image:
+ repository: index.docker.io/author/image
+ tag: 1.0.0
+
+dockerHub3Image:
+ repository: registry-1.docker.io/author/image
+ tag: 1.0.0
+
+dockerHub4Image:
+ repository: registry.hub.docker.com/author/image
+ tag: 1.0.0
+
+dockerHub5Image:
+ repository: image
+ tag: 1.0.0
+
+dockerHub6Image:
+ repository: library/image
+ tag: 1.0.0
+
+lscrImage:
+ repository: lscr.io/linuxserver/image
+ tag: 1.0.0
+
+tccrImage:
+ repository: tccr.io/tccr/image
+ tag: 1.0.0
+
+mcrImage:
+ repository: mcr.microsoft.com/author/image
+ tag: 1.0.0
+
+ecrImage:
+ repository: public.ecr.aws/author/image
+ tag: 1.0.0
+
+ghcrImage:
+ repository: ghcr.io/author/image
+ tag: 1.0.0
+
+quayImage:
+ repository: quay.io/author/image
+ tag: 1.0.0
+
+gcrImage:
+ repository: gcr.io/author/image
+ tag: 1.0.0
+
+azurecrImage:
+ repository: author.azurecr.io/image
+ tag: 1.0.0
+
+ocirImage:
+ repository: author.ocir.io/image
+ tag: 1.0.0
+
+unknownImage:
+ repository: unknown.io/author/image
+ tag: 1.0.0
diff --git a/clustertool/testdata/values_yaml/singleImageValues.yaml b/clustertool/testdata/values_yaml/singleImageValues.yaml
new file mode 100644
index 0000000000000..7cbb5be7cc3c2
--- /dev/null
+++ b/clustertool/testdata/values_yaml/singleImageValues.yaml
@@ -0,0 +1,3 @@
+image:
+ repository: "nginx"
+ tag: "1.15.8"
diff --git a/containers/apps/argocd/DockerFile b/containers/apps/argocd/DockerFile
deleted file mode 100644
index c6dc172d9a53c..0000000000000
--- a/containers/apps/argocd/DockerFile
+++ /dev/null
@@ -1,40 +0,0 @@
-ARG ARGO_CD_VERSION="v2.6.15@sha256:834dc238abb0550e94057906f3b22d2d4d737ef410bff0bd1b4eec4017a73d2e"
-# https://github.com/argoproj/argo-cd/blob/master/Dockerfile
-ARG KSOPS_VERSION="v4.2.1@sha256:55d4d4f8986419bd7f591643175c3d8bd064d3de72ff5678bd81311ee157b60a"
-
-#--------------------------------------------#
-#--------Build KSOPS and Kustomize-----------#
-#--------------------------------------------#
-
-FROM viaductoss/ksops:$KSOPS_VERSION as ksops-builder
-
-#--------------------------------------------#
-#--------Build Custom Argo Image-------------#
-#--------------------------------------------#
-
-FROM argoproj/argocd:$ARGO_CD_VERSION
-
-# Switch to root for the ability to perform install
-USER root
-
-ARG PKG_NAME=ksops
-
-# Override the default kustomize executable with the Go built version
-COPY --from=ksops-builder /go/bin/kustomize /usr/local/bin/kustomize
-
-# Add ksops executable to path
-COPY --from=ksops-builder /go/bin/ksops /usr/local/bin/ksops
-
-# Switch back to non-root user
-USER argocd
-
-ARG CONTAINER_NAME
-ARG CONTAINER_VER
-LABEL "maintainer"="TrueCharts "
-LABEL "org.opencontainers.image.source"="https://github.com/truecharts/apps"
-LABEL org.opencontainers.image.title="${CONTAINER_NAME}"
-LABEL org.opencontainers.image.url="https://truecharts.org/docs/charts/${CONTAINER_NAME}"
-LABEL org.opencontainers.image.version="${CONTAINER_VER}"
-LABEL org.opencontainers.image.description="Container for ${CONTAINER_NAME} by TrueCharts"
-LABEL org.opencontainers.image.authors="TrueCharts"
-LABEL org.opencontainers.image.documentation="https://truecharts.org/docs/charts/${CONTAINER_NAME}"
diff --git a/containers/apps/argocd/PLATFORM b/containers/apps/argocd/PLATFORM
deleted file mode 100644
index 303dc7a5a77fb..0000000000000
--- a/containers/apps/argocd/PLATFORM
+++ /dev/null
@@ -1 +0,0 @@
-linux/amd64
diff --git a/containers/apps/charts-ci/Dockerfile b/containers/apps/charts-ci/Dockerfile
new file mode 100644
index 0000000000000..232f845de4da0
--- /dev/null
+++ b/containers/apps/charts-ci/Dockerfile
@@ -0,0 +1,222 @@
+# Define Chart Releaser
+# hadolint ignore=DL3007
+FROM ghcr.io/actions/actions-runner:2.320.0@sha256:8de989a63c2dad8aeb7c44b70f86189d148f887cbd917ecbe83879df54bf6590
+
+SHELL ["/bin/bash", "-c"]
+
+# Environment variables for the versions of the tools
+ENV kubectlVersion=1.21.0
+ENV kustomizeVersion=4.4.1
+ENV helmVersion=3.14.4
+ENV oldhelmVersion=3.12.1
+ENV kubevalVersion=0.15.0
+ENV kubeconformVersion=0.4.12
+ENV conftestVersion=0.25.0
+ENV goyqVersion=4.44.1
+ENV rancherVersion=2.8.4
+ENV tiltVersion=0.33.14
+ENV skaffoldVersion=1.28.0
+ENV kubeScoreVersion=1.18.0
+ENV chartReleaserVersion=1.6.1
+ENV chartTestingVersion=3.11.0
+
+# Default architecture
+ENV arch=amd64
+
+ENV HOMEBREW_NO_ANALYTICS=1 \
+ HOMEBREW_NO_ENV_HINTS=1 \
+ HOMEBREW_NO_INSTALL_CLEANUP=1 \
+ DEBCONF_NONINTERACTIVE_SEEN=true \
+ DEBIAN_FRONTEND="noninteractive" \
+ APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
+
+USER root
+
+# Install base packages
+# hadolint ignore=DL3008,DL3015,SC2086,SC2155
+RUN \
+ apt-get update \
+ && \
+ apt-get install -y --no-install-recommends --no-install-suggests \
+ apt-transport-https \
+ ca-certificates \
+ curl \
+ gcc \
+ git \
+ gnupg \
+ gzip \
+ jo \
+ jq \
+ moreutils \
+ tar \
+ unrar \
+ unzip \
+ wget \
+ zip \
+ && rm -rf /var/lib/apt/lists/*
+
+# Create a directory for the tools
+RUN mkdir -p /tools/bin
+
+# Set the PATH
+ENV PATH="/tools/bin:${PATH}"
+
+# Install python packages
+# hadolint ignore=DL3008,DL3015,SC2086,SC2155
+RUN \
+ apt-get update \
+ && \
+ apt-get install -y --no-install-recommends --no-install-suggests \
+ python3 \
+ python3-pip \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install cwebp for website repo
+# hadolint ignore=DL3008,DL3015,SC2086,SC2155
+RUN \
+ apt-get update \
+ && \
+ apt-get install -y --no-install-recommends --no-install-suggests \
+ webp \
+ && rm -rf /var/lib/apt/lists/*
+
+### Kubernetes-Tools Installer
+
+# Define download and extraction commands
+RUN set -eux; \
+ download_and_extract() { \
+ url=$1; \
+ dest_dir=$2; \
+ command_path=$3; \
+ curl -L "$url" -o temp_archive; \
+ if [[ "$url" == *.tar.gz || "$url" == *.tgz ]]; then \
+ tar -xzf temp_archive -C "$dest_dir" --strip-components=$(dirname "$command_path" | grep -o "/" | wc -l) || tar -xvpf temp_archive -C "$dest_dir" --strip-components=$(dirname "$command_path" | grep -o "/" | wc -l); \
+ elif [[ "$url" == *.zip ]]; then \
+ unzip temp_archive -d "$dest_dir"; \
+ else \
+ cp temp_archive "$dest_dir/$(basename "$command_path")"; \
+ fi; \
+ rm temp_archive; \
+ }; \
+ download_and_copy() { \
+ tool=$1; \
+ version=$2; \
+ rename="${3:-$tool}"; \
+ url=""; \
+ command_path_in_package="$(basename $tool)"; \
+ if [ "$arch" = "amd64" ]; then arch_path="amd64"; elif [ "$arch" = "arm64" ]; then arch_path="arm64"; else arch_path="arm"; fi; \
+ if [ "$arch" = "amd64" ]; then xarch="x86_64"; elif [ "$arch" = "arm64" ]; then xarch="arm64"; else xarch="arm"; fi; \
+ case "$tool" in \
+ kubectl) \
+ url="https://dl.k8s.io/release/v${version}/bin/linux/${arch}/kubectl"; \
+ ;; \
+ kustomize) \
+ url="https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize/v${version}/kustomize_v${version}_linux_${arch}.tar.gz"; \
+ ;; \
+ helm) \
+ url="https://get.helm.sh/helm-v${version}-linux-${arch}.tar.gz"; \
+ command_path_in_package="linux-${arch_path}/helm"; \
+ ;; \
+ kubeval) \
+ url="https://github.com/instrumenta/kubeval/releases/download/${version}/kubeval-linux-${arch}.tar.gz"; \
+ ;; \
+ kubeconform) \
+ url="https://github.com/yannh/kubeconform/releases/download/v${version}/kubeconform-linux-${arch}.tar.gz"; \
+ ;; \
+ conftest) \
+ url="https://github.com/open-policy-agent/conftest/releases/download/v${version}/conftest_${version}_Linux_${xarch}.tar.gz"; \
+ ;; \
+ go-yq) \
+ url="https://github.com/mikefarah/yq/releases/download/v${version}/yq_linux_${arch}"; \
+ command_path_in_package="yq_linux_${arch}"; \
+ ;; \
+ rancher) \
+ url="https://github.com/rancher/cli/releases/download/v${version}/rancher-linux-${arch}-v${version}.tar.gz"; \
+ command_path_in_package="rancher-v${version}/rancher"; \
+ ;; \
+ tilt) \
+ url="https://github.com/tilt-dev/tilt/releases/download/v${version}/tilt.${version}.linux.${xarch}.tar.gz"; \
+ ;; \
+ skaffold) \
+ url="https://storage.googleapis.com/skaffold/releases/v${version}/skaffold-linux-${arch}"; \
+ command_path_in_package="skaffold-linux-${arch}"; \
+ ;; \
+ kube-score) \
+ url="https://github.com/zegl/kube-score/releases/download/v${version}/kube-score_${version}_linux_${arch}.tar.gz"; \
+ ;; \
+ chart-releaser) \
+ url="https://github.com/helm/chart-releaser/releases/download/v${version}/chart-releaser_${version}_linux_${arch}.tar.gz"; \
+ command_path_in_package="cr"; \
+ rename="cr"; \
+ ;; \
+ chart-testing) \
+ url="https://github.com/helm/chart-testing/releases/download/v${version}/chart-testing_${version}_linux_${arch}.tar.gz"; \
+ command_path_in_package="ct"; \
+ rename="ct"; \
+ ;; \
+ *) echo "Unknown tool: $tool"; exit 1 ;; \
+ esac; \
+ mkdir -p /tools/${tool}; \
+ download_and_extract "$url" "/tools/${tool}" "$command_path_in_package"; \
+ cp "/tools/${tool}/$command_path_in_package" "/tools/bin/$rename"; \
+ rm -rf "/tools/${tool}"; \
+ chmod 666 "/tools/bin/$rename"; \
+ chmod +x "/tools/bin/$rename"; \
+ }; \
+ # Download and copy each tool
+ download_and_copy "kubectl" "$kubectlVersion"; \
+ download_and_copy "kustomize" "$kustomizeVersion"; \
+ download_and_copy "helm" "$helmVersion"; \
+ download_and_copy "helm" "$oldhelmVersion" "oldhelm"; \
+ download_and_copy "kubeval" "$kubevalVersion"; \
+ download_and_copy "kubeconform" "$kubeconformVersion"; \
+ download_and_copy "conftest" "$conftestVersion"; \
+ download_and_copy "go-yq" "$goyqVersion"; \
+ download_and_copy "rancher" "$rancherVersion"; \
+ download_and_copy "tilt" "$tiltVersion"; \
+ download_and_copy "skaffold" "$skaffoldVersion"; \
+ download_and_copy "kube-score" "$kubeScoreVersion"; \
+ download_and_copy "chart-releaser" "$chartReleaserVersion"; \
+ download_and_copy "chart-testing" "$chartTestingVersion";
+
+# Install Pre-Commit
+# hadolint ignore=DL3008,DL3015,SC2086,SC2155
+RUN \
+ apt-get update \
+ && \
+ apt-get install -y --no-install-recommends --no-install-suggests \
+ pre-commit \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install golang
+
+COPY --from=golang:1.23.2@sha256:a7f2fc9834049c1f5df787690026a53738e55fc097cd8a4a93faa3e06c67ee32 /usr/local/go/ /usr/local/go/
+
+ENV GOPATH /go
+ENV PATH $GOPATH/bin:$PATH
+
+# hadolint ignore=DL3008,DL3015,SC2086,SC2155
+RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
+
+# don't auto-upgrade the gotoolchain
+# https://github.com/docker-library/golang/issues/472
+ENV GOTOOLCHAIN=local
+
+USER runner
+
+# Install homebrew
+# hadolint ignore=DL3008,DL3015,SC2086,SC2155
+RUN /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
+
+LABEL "maintainer"="TrueCharts "
+LABEL "org.opencontainers.image.source"="https://github.com/truecharts/apps"
+
+ARG CONTAINER_NAME
+ARG CONTAINER_VER
+LABEL org.opencontainers.image.licenses="All-Rights-Reserved"
+LABEL org.opencontainers.image.title="${CONTAINER_NAME}"
+LABEL org.opencontainers.image.url="https://truecharts.org/docs/charts/${CONTAINER_NAME}"
+LABEL org.opencontainers.image.version="${CONTAINER_VER}"
+LABEL org.opencontainers.image.description="Container for ${CONTAINER_NAME} by TrueCharts"
+LABEL org.opencontainers.image.authors="TrueCharts"
+LABEL org.opencontainers.image.documentation="https://truecharts.org/docs/charts/${CONTAINER_NAME}"
diff --git a/containers/apps/charts-ci/LICENSE b/containers/apps/charts-ci/LICENSE
new file mode 100644
index 0000000000000..33a8cbb23f017
--- /dev/null
+++ b/containers/apps/charts-ci/LICENSE
@@ -0,0 +1,106 @@
+Business Source License 1.1
+
+Parameters
+
+Licensor: The TrueCharts Project, it's owner and it's contributors
+Licensed Work: The TrueCharts "Blocky" Helm Chart
+Additional Use Grant: You may use the licensed work in production, as long
+ as it is directly sourced from a TrueCharts provided
+ official repository, catalog or source. You may also make private
+ modification to the directly sourced licenced work,
+ when used in production.
+
+ The following cases are, due to their nature, also
+ defined as 'production use' and explicitly prohibited:
+ - Bundling, including or displaying the licensed work
+ with(in) another work intended for production use,
+ with the apparent intend of facilitating and/or
+ promoting production use by third parties in
+ violation of this license.
+
+Change Date: 2050-01-01
+
+Change License: 3-clause BSD license
+
+For information about alternative licensing arrangements for the Software,
+please contact: legal@truecharts.org
+
+Notice
+
+The Business Source License (this document, or the “License”) is not an Open
+Source license. However, the Licensed Work will eventually be made available
+under an Open Source License, as stated in this License.
+
+License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
+“Business Source License” is a trademark of MariaDB Corporation Ab.
+
+-----------------------------------------------------------------------------
+
+Business Source License 1.1
+
+Terms
+
+The Licensor hereby grants you the right to copy, modify, create derivative
+works, redistribute, and make non-production use of the Licensed Work. The
+Licensor may make an Additional Use Grant, above, permitting limited
+production use.
+
+Effective on the Change Date, or the fourth anniversary of the first publicly
+available distribution of a specific version of the Licensed Work under this
+License, whichever comes first, the Licensor hereby grants you rights under
+the terms of the Change License, and the rights granted in the paragraph
+above terminate.
+
+If your use of the Licensed Work does not comply with the requirements
+currently in effect as described in this License, you must purchase a
+commercial license from the Licensor, its affiliated entities, or authorized
+resellers, or you must refrain from using the Licensed Work.
+
+All copies of the original and modified Licensed Work, and derivative works
+of the Licensed Work, are subject to this License. This License applies
+separately for each version of the Licensed Work and the Change Date may vary
+for each version of the Licensed Work released by Licensor.
+
+You must conspicuously display this License on each original or modified copy
+of the Licensed Work. If you receive the Licensed Work in original or
+modified form from a third party, the terms and conditions set forth in this
+License apply to your use of that work.
+
+Any use of the Licensed Work in violation of this License will automatically
+terminate your rights under this License for the current and all other
+versions of the Licensed Work.
+
+This License does not grant you any right in any trademark or logo of
+Licensor or its affiliates (provided that you may use a trademark or logo of
+Licensor as expressly required by this License).
+
+TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
+AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
+EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
+TITLE.
+
+MariaDB hereby grants you permission to use this License’s text to license
+your works, and to refer to it using the trademark “Business Source License”,
+as long as you comply with the Covenants of Licensor below.
+
+Covenants of Licensor
+
+In consideration of the right to use this License’s text and the “Business
+Source License” name and trademark, Licensor covenants to MariaDB, and to all
+other recipients of the licensed work to be provided by Licensor:
+
+1. To specify as the Change License the GPL Version 2.0 or any later version,
+ or a license that is compatible with GPL Version 2.0 or a later version,
+ where “compatible” means that software provided under the Change License can
+ be included in a program with software provided under GPL Version 2.0 or a
+ later version. Licensor may specify additional Change Licenses without
+ limitation.
+
+2. To either: (a) specify an additional grant of rights to use that does not
+ impose any additional restriction on the right granted in this License, as
+ the Additional Use Grant; or (b) insert the text “None”.
+
+3. To specify a Change Date.
+
+4. Not to modify this License in any other way.
diff --git a/containers/apps/charts-ci/VERSION b/containers/apps/charts-ci/VERSION
new file mode 100644
index 0000000000000..8f0916f768f04
--- /dev/null
+++ b/containers/apps/charts-ci/VERSION
@@ -0,0 +1 @@
+0.5.0
diff --git a/containers/apps/charts-ci/latest-version.sh b/containers/apps/charts-ci/latest-version.sh
new file mode 100755
index 0000000000000..d91483f456421
--- /dev/null
+++ b/containers/apps/charts-ci/latest-version.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+printf "%s" "0.5.0"
diff --git a/containers/apps/db-wait-mariadb/LICENSE b/containers/apps/db-wait-mariadb/LICENSE
new file mode 100644
index 0000000000000..33a8cbb23f017
--- /dev/null
+++ b/containers/apps/db-wait-mariadb/LICENSE
@@ -0,0 +1,106 @@
+Business Source License 1.1
+
+Parameters
+
+Licensor: The TrueCharts Project, it's owner and it's contributors
+Licensed Work: The TrueCharts "Blocky" Helm Chart
+Additional Use Grant: You may use the licensed work in production, as long
+ as it is directly sourced from a TrueCharts provided
+ official repository, catalog or source. You may also make private
+ modification to the directly sourced licenced work,
+ when used in production.
+
+ The following cases are, due to their nature, also
+ defined as 'production use' and explicitly prohibited:
+ - Bundling, including or displaying the licensed work
+ with(in) another work intended for production use,
+ with the apparent intend of facilitating and/or
+ promoting production use by third parties in
+ violation of this license.
+
+Change Date: 2050-01-01
+
+Change License: 3-clause BSD license
+
+For information about alternative licensing arrangements for the Software,
+please contact: legal@truecharts.org
+
+Notice
+
+The Business Source License (this document, or the “License”) is not an Open
+Source license. However, the Licensed Work will eventually be made available
+under an Open Source License, as stated in this License.
+
+License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
+“Business Source License” is a trademark of MariaDB Corporation Ab.
+
+-----------------------------------------------------------------------------
+
+Business Source License 1.1
+
+Terms
+
+The Licensor hereby grants you the right to copy, modify, create derivative
+works, redistribute, and make non-production use of the Licensed Work. The
+Licensor may make an Additional Use Grant, above, permitting limited
+production use.
+
+Effective on the Change Date, or the fourth anniversary of the first publicly
+available distribution of a specific version of the Licensed Work under this
+License, whichever comes first, the Licensor hereby grants you rights under
+the terms of the Change License, and the rights granted in the paragraph
+above terminate.
+
+If your use of the Licensed Work does not comply with the requirements
+currently in effect as described in this License, you must purchase a
+commercial license from the Licensor, its affiliated entities, or authorized
+resellers, or you must refrain from using the Licensed Work.
+
+All copies of the original and modified Licensed Work, and derivative works
+of the Licensed Work, are subject to this License. This License applies
+separately for each version of the Licensed Work and the Change Date may vary
+for each version of the Licensed Work released by Licensor.
+
+You must conspicuously display this License on each original or modified copy
+of the Licensed Work. If you receive the Licensed Work in original or
+modified form from a third party, the terms and conditions set forth in this
+License apply to your use of that work.
+
+Any use of the Licensed Work in violation of this License will automatically
+terminate your rights under this License for the current and all other
+versions of the Licensed Work.
+
+This License does not grant you any right in any trademark or logo of
+Licensor or its affiliates (provided that you may use a trademark or logo of
+Licensor as expressly required by this License).
+
+TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
+AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
+EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
+TITLE.
+
+MariaDB hereby grants you permission to use this License’s text to license
+your works, and to refer to it using the trademark “Business Source License”,
+as long as you comply with the Covenants of Licensor below.
+
+Covenants of Licensor
+
+In consideration of the right to use this License’s text and the “Business
+Source License” name and trademark, Licensor covenants to MariaDB, and to all
+other recipients of the licensed work to be provided by Licensor:
+
+1. To specify as the Change License the GPL Version 2.0 or any later version,
+ or a license that is compatible with GPL Version 2.0 or a later version,
+ where “compatible” means that software provided under the Change License can
+ be included in a program with software provided under GPL Version 2.0 or a
+ later version. Licensor may specify additional Change Licenses without
+ limitation.
+
+2. To either: (a) specify an additional grant of rights to use that does not
+ impose any additional restriction on the right granted in this License, as
+ the Additional Use Grant; or (b) insert the text “None”.
+
+3. To specify a Change Date.
+
+4. Not to modify this License in any other way.
diff --git a/containers/apps/db-wait-mongodb/LICENSE b/containers/apps/db-wait-mongodb/LICENSE
new file mode 100644
index 0000000000000..33a8cbb23f017
--- /dev/null
+++ b/containers/apps/db-wait-mongodb/LICENSE
@@ -0,0 +1,106 @@
+Business Source License 1.1
+
+Parameters
+
+Licensor: The TrueCharts Project, it's owner and it's contributors
+Licensed Work: The TrueCharts "Blocky" Helm Chart
+Additional Use Grant: You may use the licensed work in production, as long
+ as it is directly sourced from a TrueCharts provided
+ official repository, catalog or source. You may also make private
+ modification to the directly sourced licenced work,
+ when used in production.
+
+ The following cases are, due to their nature, also
+ defined as 'production use' and explicitly prohibited:
+ - Bundling, including or displaying the licensed work
+ with(in) another work intended for production use,
+ with the apparent intend of facilitating and/or
+ promoting production use by third parties in
+ violation of this license.
+
+Change Date: 2050-01-01
+
+Change License: 3-clause BSD license
+
+For information about alternative licensing arrangements for the Software,
+please contact: legal@truecharts.org
+
+Notice
+
+The Business Source License (this document, or the “License”) is not an Open
+Source license. However, the Licensed Work will eventually be made available
+under an Open Source License, as stated in this License.
+
+License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
+“Business Source License” is a trademark of MariaDB Corporation Ab.
+
+-----------------------------------------------------------------------------
+
+Business Source License 1.1
+
+Terms
+
+The Licensor hereby grants you the right to copy, modify, create derivative
+works, redistribute, and make non-production use of the Licensed Work. The
+Licensor may make an Additional Use Grant, above, permitting limited
+production use.
+
+Effective on the Change Date, or the fourth anniversary of the first publicly
+available distribution of a specific version of the Licensed Work under this
+License, whichever comes first, the Licensor hereby grants you rights under
+the terms of the Change License, and the rights granted in the paragraph
+above terminate.
+
+If your use of the Licensed Work does not comply with the requirements
+currently in effect as described in this License, you must purchase a
+commercial license from the Licensor, its affiliated entities, or authorized
+resellers, or you must refrain from using the Licensed Work.
+
+All copies of the original and modified Licensed Work, and derivative works
+of the Licensed Work, are subject to this License. This License applies
+separately for each version of the Licensed Work and the Change Date may vary
+for each version of the Licensed Work released by Licensor.
+
+You must conspicuously display this License on each original or modified copy
+of the Licensed Work. If you receive the Licensed Work in original or
+modified form from a third party, the terms and conditions set forth in this
+License apply to your use of that work.
+
+Any use of the Licensed Work in violation of this License will automatically
+terminate your rights under this License for the current and all other
+versions of the Licensed Work.
+
+This License does not grant you any right in any trademark or logo of
+Licensor or its affiliates (provided that you may use a trademark or logo of
+Licensor as expressly required by this License).
+
+TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
+AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
+EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
+TITLE.
+
+MariaDB hereby grants you permission to use this License’s text to license
+your works, and to refer to it using the trademark “Business Source License”,
+as long as you comply with the Covenants of Licensor below.
+
+Covenants of Licensor
+
+In consideration of the right to use this License’s text and the “Business
+Source License” name and trademark, Licensor covenants to MariaDB, and to all
+other recipients of the licensed work to be provided by Licensor:
+
+1. To specify as the Change License the GPL Version 2.0 or any later version,
+ or a license that is compatible with GPL Version 2.0 or a later version,
+ where “compatible” means that software provided under the Change License can
+ be included in a program with software provided under GPL Version 2.0 or a
+ later version. Licensor may specify additional Change Licenses without
+ limitation.
+
+2. To either: (a) specify an additional grant of rights to use that does not
+ impose any additional restriction on the right granted in this License, as
+ the Additional Use Grant; or (b) insert the text “None”.
+
+3. To specify a Change Date.
+
+4. Not to modify this License in any other way.
diff --git a/containers/apps/db-wait-postgres/LICENSE b/containers/apps/db-wait-postgres/LICENSE
new file mode 100644
index 0000000000000..33a8cbb23f017
--- /dev/null
+++ b/containers/apps/db-wait-postgres/LICENSE
@@ -0,0 +1,106 @@
+Business Source License 1.1
+
+Parameters
+
+Licensor: The TrueCharts Project, it's owner and it's contributors
+Licensed Work: The TrueCharts "Blocky" Helm Chart
+Additional Use Grant: You may use the licensed work in production, as long
+ as it is directly sourced from a TrueCharts provided
+ official repository, catalog or source. You may also make private
+ modification to the directly sourced licenced work,
+ when used in production.
+
+ The following cases are, due to their nature, also
+ defined as 'production use' and explicitly prohibited:
+ - Bundling, including or displaying the licensed work
+ with(in) another work intended for production use,
+ with the apparent intend of facilitating and/or
+ promoting production use by third parties in
+ violation of this license.
+
+Change Date: 2050-01-01
+
+Change License: 3-clause BSD license
+
+For information about alternative licensing arrangements for the Software,
+please contact: legal@truecharts.org
+
+Notice
+
+The Business Source License (this document, or the “License”) is not an Open
+Source license. However, the Licensed Work will eventually be made available
+under an Open Source License, as stated in this License.
+
+License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
+“Business Source License” is a trademark of MariaDB Corporation Ab.
+
+-----------------------------------------------------------------------------
+
+Business Source License 1.1
+
+Terms
+
+The Licensor hereby grants you the right to copy, modify, create derivative
+works, redistribute, and make non-production use of the Licensed Work. The
+Licensor may make an Additional Use Grant, above, permitting limited
+production use.
+
+Effective on the Change Date, or the fourth anniversary of the first publicly
+available distribution of a specific version of the Licensed Work under this
+License, whichever comes first, the Licensor hereby grants you rights under
+the terms of the Change License, and the rights granted in the paragraph
+above terminate.
+
+If your use of the Licensed Work does not comply with the requirements
+currently in effect as described in this License, you must purchase a
+commercial license from the Licensor, its affiliated entities, or authorized
+resellers, or you must refrain from using the Licensed Work.
+
+All copies of the original and modified Licensed Work, and derivative works
+of the Licensed Work, are subject to this License. This License applies
+separately for each version of the Licensed Work and the Change Date may vary
+for each version of the Licensed Work released by Licensor.
+
+You must conspicuously display this License on each original or modified copy
+of the Licensed Work. If you receive the Licensed Work in original or
+modified form from a third party, the terms and conditions set forth in this
+License apply to your use of that work.
+
+Any use of the Licensed Work in violation of this License will automatically
+terminate your rights under this License for the current and all other
+versions of the Licensed Work.
+
+This License does not grant you any right in any trademark or logo of
+Licensor or its affiliates (provided that you may use a trademark or logo of
+Licensor as expressly required by this License).
+
+TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
+AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
+EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
+TITLE.
+
+MariaDB hereby grants you permission to use this License’s text to license
+your works, and to refer to it using the trademark “Business Source License”,
+as long as you comply with the Covenants of Licensor below.
+
+Covenants of Licensor
+
+In consideration of the right to use this License’s text and the “Business
+Source License” name and trademark, Licensor covenants to MariaDB, and to all
+other recipients of the licensed work to be provided by Licensor:
+
+1. To specify as the Change License the GPL Version 2.0 or any later version,
+ or a license that is compatible with GPL Version 2.0 or a later version,
+ where “compatible” means that software provided under the Change License can
+ be included in a program with software provided under GPL Version 2.0 or a
+ later version. Licensor may specify additional Change Licenses without
+ limitation.
+
+2. To either: (a) specify an additional grant of rights to use that does not
+ impose any additional restriction on the right granted in this License, as
+ the Additional Use Grant; or (b) insert the text “None”.
+
+3. To specify a Change Date.
+
+4. Not to modify this License in any other way.
diff --git a/containers/apps/db-wait-redis/LICENSE b/containers/apps/db-wait-redis/LICENSE
new file mode 100644
index 0000000000000..33a8cbb23f017
--- /dev/null
+++ b/containers/apps/db-wait-redis/LICENSE
@@ -0,0 +1,106 @@
+Business Source License 1.1
+
+Parameters
+
+Licensor: The TrueCharts Project, it's owner and it's contributors
+Licensed Work: The TrueCharts "Blocky" Helm Chart
+Additional Use Grant: You may use the licensed work in production, as long
+ as it is directly sourced from a TrueCharts provided
+ official repository, catalog or source. You may also make private
+ modification to the directly sourced licenced work,
+ when used in production.
+
+ The following cases are, due to their nature, also
+ defined as 'production use' and explicitly prohibited:
+ - Bundling, including or displaying the licensed work
+ with(in) another work intended for production use,
+ with the apparent intend of facilitating and/or
+ promoting production use by third parties in
+ violation of this license.
+
+Change Date: 2050-01-01
+
+Change License: 3-clause BSD license
+
+For information about alternative licensing arrangements for the Software,
+please contact: legal@truecharts.org
+
+Notice
+
+The Business Source License (this document, or the “License”) is not an Open
+Source license. However, the Licensed Work will eventually be made available
+under an Open Source License, as stated in this License.
+
+License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
+“Business Source License” is a trademark of MariaDB Corporation Ab.
+
+-----------------------------------------------------------------------------
+
+Business Source License 1.1
+
+Terms
+
+The Licensor hereby grants you the right to copy, modify, create derivative
+works, redistribute, and make non-production use of the Licensed Work. The
+Licensor may make an Additional Use Grant, above, permitting limited
+production use.
+
+Effective on the Change Date, or the fourth anniversary of the first publicly
+available distribution of a specific version of the Licensed Work under this
+License, whichever comes first, the Licensor hereby grants you rights under
+the terms of the Change License, and the rights granted in the paragraph
+above terminate.
+
+If your use of the Licensed Work does not comply with the requirements
+currently in effect as described in this License, you must purchase a
+commercial license from the Licensor, its affiliated entities, or authorized
+resellers, or you must refrain from using the Licensed Work.
+
+All copies of the original and modified Licensed Work, and derivative works
+of the Licensed Work, are subject to this License. This License applies
+separately for each version of the Licensed Work and the Change Date may vary
+for each version of the Licensed Work released by Licensor.
+
+You must conspicuously display this License on each original or modified copy
+of the Licensed Work. If you receive the Licensed Work in original or
+modified form from a third party, the terms and conditions set forth in this
+License apply to your use of that work.
+
+Any use of the Licensed Work in violation of this License will automatically
+terminate your rights under this License for the current and all other
+versions of the Licensed Work.
+
+This License does not grant you any right in any trademark or logo of
+Licensor or its affiliates (provided that you may use a trademark or logo of
+Licensor as expressly required by this License).
+
+TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
+AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
+EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
+TITLE.
+
+MariaDB hereby grants you permission to use this License’s text to license
+your works, and to refer to it using the trademark “Business Source License”,
+as long as you comply with the Covenants of Licensor below.
+
+Covenants of Licensor
+
+In consideration of the right to use this License’s text and the “Business
+Source License” name and trademark, Licensor covenants to MariaDB, and to all
+other recipients of the licensed work to be provided by Licensor:
+
+1. To specify as the Change License the GPL Version 2.0 or any later version,
+ or a license that is compatible with GPL Version 2.0 or a later version,
+ where “compatible” means that software provided under the Change License can
+ be included in a program with software provided under GPL Version 2.0 or a
+ later version. Licensor may specify additional Change Licenses without
+ limitation.
+
+2. To either: (a) specify an additional grant of rights to use that does not
+ impose any additional restriction on the right granted in this License, as
+ the Additional Use Grant; or (b) insert the text “None”.
+
+3. To specify a Change Date.
+
+4. Not to modify this License in any other way.
diff --git a/containers/apps/lvm-disk-watcher/LICENSE b/containers/apps/lvm-disk-watcher/LICENSE
new file mode 100644
index 0000000000000..33a8cbb23f017
--- /dev/null
+++ b/containers/apps/lvm-disk-watcher/LICENSE
@@ -0,0 +1,106 @@
+Business Source License 1.1
+
+Parameters
+
+Licensor: The TrueCharts Project, it's owner and it's contributors
+Licensed Work: The TrueCharts "Blocky" Helm Chart
+Additional Use Grant: You may use the licensed work in production, as long
+ as it is directly sourced from a TrueCharts provided
+ official repository, catalog or source. You may also make private
+ modification to the directly sourced licenced work,
+ when used in production.
+
+ The following cases are, due to their nature, also
+ defined as 'production use' and explicitly prohibited:
+ - Bundling, including or displaying the licensed work
+ with(in) another work intended for production use,
+ with the apparent intend of facilitating and/or
+ promoting production use by third parties in
+ violation of this license.
+
+Change Date: 2050-01-01
+
+Change License: 3-clause BSD license
+
+For information about alternative licensing arrangements for the Software,
+please contact: legal@truecharts.org
+
+Notice
+
+The Business Source License (this document, or the “License”) is not an Open
+Source license. However, the Licensed Work will eventually be made available
+under an Open Source License, as stated in this License.
+
+License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
+“Business Source License” is a trademark of MariaDB Corporation Ab.
+
+-----------------------------------------------------------------------------
+
+Business Source License 1.1
+
+Terms
+
+The Licensor hereby grants you the right to copy, modify, create derivative
+works, redistribute, and make non-production use of the Licensed Work. The
+Licensor may make an Additional Use Grant, above, permitting limited
+production use.
+
+Effective on the Change Date, or the fourth anniversary of the first publicly
+available distribution of a specific version of the Licensed Work under this
+License, whichever comes first, the Licensor hereby grants you rights under
+the terms of the Change License, and the rights granted in the paragraph
+above terminate.
+
+If your use of the Licensed Work does not comply with the requirements
+currently in effect as described in this License, you must purchase a
+commercial license from the Licensor, its affiliated entities, or authorized
+resellers, or you must refrain from using the Licensed Work.
+
+All copies of the original and modified Licensed Work, and derivative works
+of the Licensed Work, are subject to this License. This License applies
+separately for each version of the Licensed Work and the Change Date may vary
+for each version of the Licensed Work released by Licensor.
+
+You must conspicuously display this License on each original or modified copy
+of the Licensed Work. If you receive the Licensed Work in original or
+modified form from a third party, the terms and conditions set forth in this
+License apply to your use of that work.
+
+Any use of the Licensed Work in violation of this License will automatically
+terminate your rights under this License for the current and all other
+versions of the Licensed Work.
+
+This License does not grant you any right in any trademark or logo of
+Licensor or its affiliates (provided that you may use a trademark or logo of
+Licensor as expressly required by this License).
+
+TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
+AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
+EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
+TITLE.
+
+MariaDB hereby grants you permission to use this License’s text to license
+your works, and to refer to it using the trademark “Business Source License”,
+as long as you comply with the Covenants of Licensor below.
+
+Covenants of Licensor
+
+In consideration of the right to use this License’s text and the “Business
+Source License” name and trademark, Licensor covenants to MariaDB, and to all
+other recipients of the licensed work to be provided by Licensor:
+
+1. To specify as the Change License the GPL Version 2.0 or any later version,
+ or a license that is compatible with GPL Version 2.0 or a later version,
+ where “compatible” means that software provided under the Change License can
+ be included in a program with software provided under GPL Version 2.0 or a
+ later version. Licensor may specify additional Change Licenses without
+ limitation.
+
+2. To either: (a) specify an additional grant of rights to use that does not
+ impose any additional restriction on the right granted in this License, as
+ the Additional Use Grant; or (b) insert the text “None”.
+
+3. To specify a Change Date.
+
+4. Not to modify this License in any other way.