mirror of
https://github.com/cozystack/cozystack.git
synced 2026-03-03 13:38:56 +00:00
Compare commits
295 Commits
token-prox
...
v0.37.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
072aa9ebc0 | ||
|
|
aff8b0c30a | ||
|
|
51883cfc69 | ||
|
|
29a6cdec05 | ||
|
|
929dae8e24 | ||
|
|
a50f53de2e | ||
|
|
484211f7a0 | ||
|
|
b6eefe4453 | ||
|
|
3b9fa33240 | ||
|
|
9184450b39 | ||
|
|
59f42de1db | ||
|
|
2ae926d04e | ||
|
|
0ba4d4494e | ||
|
|
19c91071d8 | ||
|
|
9ce3f8e53f | ||
|
|
7eb701d846 | ||
|
|
40a3ec1e70 | ||
|
|
43c222decf | ||
|
|
13dccea84b | ||
|
|
e2b4cd8bd0 | ||
|
|
ad2858e113 | ||
|
|
c6e9131f60 | ||
|
|
45036ff249 | ||
|
|
6dd08947ae | ||
|
|
a1fd97f2d7 | ||
|
|
8076f120d8 | ||
|
|
4e766ed82e | ||
|
|
d42f4b1097 | ||
|
|
6b6cee8103 | ||
|
|
7f62e14e86 | ||
|
|
a369171a20 | ||
|
|
dfcdf19554 | ||
|
|
458ca63729 | ||
|
|
1ee3d00128 | ||
|
|
00199a788a | ||
|
|
dfb0838a1e | ||
|
|
42c9d65c7c | ||
|
|
4afda63440 | ||
|
|
50b8dda38e | ||
|
|
e3cebeab47 | ||
|
|
e986e7c16a | ||
|
|
1f158fa909 | ||
|
|
13dba1b8b4 | ||
|
|
9b0f919052 | ||
|
|
da0eb7a829 | ||
|
|
012906cd59 | ||
|
|
f2cd585b45 | ||
|
|
6937b8e2b6 | ||
|
|
a8562f03d1 | ||
|
|
2383bc9f13 | ||
|
|
670341f6bd | ||
|
|
945887f30d | ||
|
|
408b8dde3a | ||
|
|
295fdf1b8e | ||
|
|
8d50dfb73f | ||
|
|
c16e37e079 | ||
|
|
66004c83e2 | ||
|
|
86d6706ee1 | ||
|
|
6de14d679d | ||
|
|
da13a6a2e5 | ||
|
|
82926a8b2a | ||
|
|
cbc7070269 | ||
|
|
94375f3161 | ||
|
|
0bdc801d9a | ||
|
|
4e618adf0a | ||
|
|
8601299a91 | ||
|
|
65bee1a8dc | ||
|
|
ffb1b89d2e | ||
|
|
bb9db7fcaf | ||
|
|
1753df590e | ||
|
|
7c1e103197 | ||
|
|
93ddc4e2c4 | ||
|
|
ded6a9fd69 | ||
|
|
bff8a5b8c7 | ||
|
|
8be9ac48ba | ||
|
|
fcab75177e | ||
|
|
b13ce92024 | ||
|
|
ab11b8e4dd | ||
|
|
e9403425a7 | ||
|
|
28cd5bebf2 | ||
|
|
364cba3100 | ||
|
|
dd76166e44 | ||
|
|
ef7dcabe64 | ||
|
|
b4c9ca36a9 | ||
|
|
37f9065d55 | ||
|
|
f130895b30 | ||
|
|
907dcb5e8b | ||
|
|
d52a2fbe94 | ||
|
|
f41ab0d251 | ||
|
|
58b7a6456c | ||
|
|
772d663bc1 | ||
|
|
e5c1cf97bd | ||
|
|
7605df5f29 | ||
|
|
df89117fa1 | ||
|
|
9873011ebf | ||
|
|
b25aa10243 | ||
|
|
f3b317ceea | ||
|
|
16496e238a | ||
|
|
c282d03330 | ||
|
|
0f8a9ac9ef | ||
|
|
21ca1349c4 | ||
|
|
23e59ea654 | ||
|
|
c81a1aa2b0 | ||
|
|
bb653e5a87 | ||
|
|
0afc3c1e86 | ||
|
|
789666d53b | ||
|
|
152ab20a17 | ||
|
|
9f9d8f8530 | ||
|
|
97f1b29975 | ||
|
|
f871fbdb1e | ||
|
|
5d76e6b626 | ||
|
|
4620f7dfa1 | ||
|
|
562145e69b | ||
|
|
0e1f73999b | ||
|
|
744a0f3ca6 | ||
|
|
3ac83ac48c | ||
|
|
d991c49254 | ||
|
|
48919c0cfe | ||
|
|
7e4883dfcc | ||
|
|
66b53cb1ae | ||
|
|
6005b76e96 | ||
|
|
e34d9613c7 | ||
|
|
ca19529c7d | ||
|
|
b3be1f4e1e | ||
|
|
53fbe7c2ee | ||
|
|
18ff789256 | ||
|
|
3d02fbfba4 | ||
|
|
8c6fc68367 | ||
|
|
9d2fe2605f | ||
|
|
edb3e92585 | ||
|
|
7118232490 | ||
|
|
19f81a2d32 | ||
|
|
b93fe65992 | ||
|
|
541347d321 | ||
|
|
1827d29412 | ||
|
|
a1a107a90b | ||
|
|
6cd0a3409e | ||
|
|
f5c575d12f | ||
|
|
d10b3635cc | ||
|
|
cdf53e89e9 | ||
|
|
37720b9609 | ||
|
|
ce522284c4 | ||
|
|
65a734bb65 | ||
|
|
07384c3605 | ||
|
|
87b2316194 | ||
|
|
585569f285 | ||
|
|
dbe1df8d27 | ||
|
|
17eb1e0ba3 | ||
|
|
b55c9f616d | ||
|
|
f025845a94 | ||
|
|
e54fc63af4 | ||
|
|
9352861051 | ||
|
|
b9eec3f261 | ||
|
|
f2cfb4f870 | ||
|
|
2291d0f7f2 | ||
|
|
15c100d262 | ||
|
|
2c9864bc09 | ||
|
|
bb1e8805dc | ||
|
|
08b5217b72 | ||
|
|
08d2d61f1a | ||
|
|
356fea6a37 | ||
|
|
e1b97e3727 | ||
|
|
16a700dabf | ||
|
|
ea27dc9497 | ||
|
|
f06c5d996d | ||
|
|
87c5540ad3 | ||
|
|
03e18ee02f | ||
|
|
382a9787f4 | ||
|
|
7f8b673dbc | ||
|
|
24482d958b | ||
|
|
2bca6b932c | ||
|
|
601f6bd3c9 | ||
|
|
1243a960e3 | ||
|
|
4dd062d9cd | ||
|
|
3e03b1bd86 | ||
|
|
8f1975d1da | ||
|
|
e15ff2a4d0 | ||
|
|
272185a2df | ||
|
|
be8495dd06 | ||
|
|
7f477eec96 | ||
|
|
cc4b7ea28c | ||
|
|
8335347dc3 | ||
|
|
49d69a5896 | ||
|
|
89a74f653a | ||
|
|
9f2b98d364 | ||
|
|
7090b8adf1 | ||
|
|
c5b46fc79c | ||
|
|
a291badbd4 | ||
|
|
52d749d46a | ||
|
|
9f89ef36bb | ||
|
|
f59d072ef1 | ||
|
|
c0d5e52e65 | ||
|
|
034f71cc9d | ||
|
|
fdd4f167c6 | ||
|
|
8fbebd4e47 | ||
|
|
389ec27b19 | ||
|
|
29df1fdc1e | ||
|
|
c4e048b315 | ||
|
|
ce5fd9d292 | ||
|
|
8e906be9df | ||
|
|
99bfd4884f | ||
|
|
15b213b38b | ||
|
|
8ca8817000 | ||
|
|
9f8c79f5d1 | ||
|
|
ce21299280 | ||
|
|
403d1f9944 | ||
|
|
138e5fbe15 | ||
|
|
fe869b97fd | ||
|
|
a4aeeca2d3 | ||
|
|
33691c2d3a | ||
|
|
08f1bda1aa | ||
|
|
58f65abefd | ||
|
|
9c1563adb7 | ||
|
|
cbbb50b194 | ||
|
|
6684117a00 | ||
|
|
6b9b700177 | ||
|
|
89c80a8178 | ||
|
|
6b5af37e1a | ||
|
|
6cd5e746c8 | ||
|
|
ffa28d0dc0 | ||
|
|
c10f6240b1 | ||
|
|
1ce2df9bc4 | ||
|
|
7690bc6e8a | ||
|
|
a227825336 | ||
|
|
f09fd0b574 | ||
|
|
39042fa04d | ||
|
|
909f55c74e | ||
|
|
32a857fbf2 | ||
|
|
d3bce65081 | ||
|
|
868148709c | ||
|
|
a2134ecce7 | ||
|
|
a1bc9178e3 | ||
|
|
8b49e74a31 | ||
|
|
60965df051 | ||
|
|
4d7992b55a | ||
|
|
c5b64af7e0 | ||
|
|
46c2ee3c31 | ||
|
|
ba6460ea10 | ||
|
|
40b83cab79 | ||
|
|
1743b5d2b3 | ||
|
|
d360c179d1 | ||
|
|
90f6169bad | ||
|
|
64a8a158c3 | ||
|
|
e3a4e284de | ||
|
|
2ef11ff513 | ||
|
|
066571a11e | ||
|
|
41c0c6d829 | ||
|
|
9629ee7298 | ||
|
|
d430048ba3 | ||
|
|
992162f507 | ||
|
|
fbc2c45e7f | ||
|
|
7acd8a2a80 | ||
|
|
21d6c69f73 | ||
|
|
c02a381819 | ||
|
|
c032a4ad49 | ||
|
|
c0f742595f | ||
|
|
168a24ffdf | ||
|
|
f864b40a85 | ||
|
|
39fb4ec8ab | ||
|
|
92f206cb93 | ||
|
|
634b77edad | ||
|
|
e091fa580f | ||
|
|
b1afaf71ca | ||
|
|
70b03ad61a | ||
|
|
a32de78c7c | ||
|
|
330103cc2b | ||
|
|
8b1e55dec2 | ||
|
|
da3f133d89 | ||
|
|
19baa7b14f | ||
|
|
502d31fe8d | ||
|
|
5359c6d991 | ||
|
|
8d4a12e14f | ||
|
|
771fbc817f | ||
|
|
bc22b22341 | ||
|
|
cffff6c49e | ||
|
|
39adc16015 | ||
|
|
896209a004 | ||
|
|
d48b5cfa2f | ||
|
|
c6bceff54b | ||
|
|
ff3305f43c | ||
|
|
58def95f67 | ||
|
|
9bc3b636a2 | ||
|
|
895597eecb | ||
|
|
a91e829cc9 | ||
|
|
be31370540 | ||
|
|
b26dc63b01 | ||
|
|
fafa859660 | ||
|
|
6e119ba940 | ||
|
|
754d5a976d | ||
|
|
c4a2bef4c9 | ||
|
|
cd80a73446 | ||
|
|
299d006d20 | ||
|
|
85063cf624 | ||
|
|
1c2cc0fa28 | ||
|
|
14aba9edb2 |
50
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
50
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!--
|
||||
Thank you for submitting a bug report!
|
||||
Please fill in the fields below to help us investigate the problem.
|
||||
-->
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Environment**
|
||||
- Cozystack version
|
||||
- Provider: on-prem, Hetzner, and so on
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behaviour**
|
||||
When taking the steps to reproduce, what should have happened differently?
|
||||
|
||||
**Actual behaviour**
|
||||
A clear and concise description of what happens when the bug occurs. Explain how the system currently behaves, including error messages, unexpected results, or incorrect functionality observed during execution.
|
||||
|
||||
|
||||
**Logs**
|
||||
```
|
||||
Paste any relevant logs here. Please redact tokens, passwords, private keys.
|
||||
```
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain the problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
**Checklist**
|
||||
- [ ] I have checked the documentation
|
||||
- [ ] I have searched for similar issues
|
||||
- [ ] I have included all required information
|
||||
- [ ] I have provided clear steps to reproduce
|
||||
- [ ] I have included relevant logs
|
||||
2
.github/workflows/pre-commit.yml
vendored
2
.github/workflows/pre-commit.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
- name: Install generate
|
||||
run: |
|
||||
curl -sSL https://github.com/cozystack/readme-generator-for-helm/releases/download/v1.0.0/readme-generator-for-helm-linux-amd64.tar.gz | tar -xzvf- -C /usr/local/bin/ readme-generator-for-helm
|
||||
curl -sSL https://github.com/cozystack/cozyvalues-gen/releases/download/v0.9.0/cozyvalues-gen-linux-amd64.tar.gz | tar -xzvf- -C /usr/local/bin/ cozyvalues-gen
|
||||
|
||||
- name: Run pre-commit hooks
|
||||
run: |
|
||||
|
||||
15
.github/workflows/pull-requests.yaml
vendored
15
.github/workflows/pull-requests.yaml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Pull Request
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.OCIR_REPO }}
|
||||
REGISTRY: ${{ vars.OCIR_REPO }}
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
@@ -32,7 +32,14 @@ jobs:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Set up Docker config
|
||||
run: |
|
||||
if [ -d ~/.docker ]; then
|
||||
cp -r ~/.docker "${{ runner.temp }}/.docker"
|
||||
fi
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.OCIR_USER}}
|
||||
@@ -254,6 +261,11 @@ jobs:
|
||||
done
|
||||
echo "✅ The task completed successfully after $attempt attempts."
|
||||
|
||||
- name: Run OpenAPI tests
|
||||
run: |
|
||||
cd /tmp/$SANDBOX_NAME
|
||||
make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME test-openapi
|
||||
|
||||
detect_test_matrix:
|
||||
name: "Detect e2e test matrix"
|
||||
runs-on: ubuntu-latest
|
||||
@@ -269,6 +281,7 @@ jobs:
|
||||
|
||||
test_apps:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.detect_test_matrix.outputs.matrix) }}
|
||||
name: Test ${{ matrix.app }}
|
||||
runs-on: [self-hosted]
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: gen-versions-map
|
||||
name: Generate versions map and check for changes
|
||||
entry: sh -c 'make -C packages/apps check-version-map && make -C packages/extra check-version-map'
|
||||
language: system
|
||||
types: [file]
|
||||
pass_filenames: false
|
||||
description: Run the script and fail if it generates changes
|
||||
- id: run-make-generate
|
||||
name: Run 'make generate' in all app directories
|
||||
entry: |
|
||||
flock -x .git/pre-commit.lock sh -c '
|
||||
for dir in ./packages/apps/*/ ./packages/extra/*/ ./packages/system/cozystack-api/; do
|
||||
for dir in ./packages/apps/*/ ./packages/extra/*/; do
|
||||
if [ -d "$dir" ]; then
|
||||
echo "Running make generate in $dir"
|
||||
make generate -C "$dir" || exit $?
|
||||
|
||||
@@ -30,3 +30,5 @@ This list is sorted in chronological order, based on the submission date.
|
||||
| [Bootstack](https://bootstack.app/) | @mrkhachaturov | 2024-08-01| At Bootstack, we utilize a Kubernetes operator specifically designed to simplify and streamline cloud infrastructure creation.|
|
||||
| [gohost](https://gohost.kz/) | @karabass_off | 2024-02-01 | Our company has been working in the market of Kazakhstan for more than 15 years, providing clients with a standard set of services: VPS/VDC, IaaS, shared hosting, etc. Now we are expanding the lineup by introducing Bare Metal Kubenetes cluster under Cozystack management. |
|
||||
| [Urmanac](https://urmanac.com) | @kingdonb | 2024-12-04 | Urmanac is the future home of a hosting platform for the knowledge base of a community of personal server enthusiasts. We use Cozystack to provide support services for web sites hosted using both conventional deployments and on SpinKube, with WASM. |
|
||||
| [Hidora](https://hikube.cloud) | @matthieu-robin | 2025-09-17 | Hidora is a Swiss cloud provider delivering managed services and infrastructure solutions through datacenters located in Switzerland, ensuring data sovereignty and reliability. Its sovereign cloud platform, Hikube, is designed to run workloads with high availability across multiple datacenters, providing enterprises with a secure and scalable foundation for their applications based on Cozystack. |
|
||||
|
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
# Code of Conduct
|
||||
|
||||
Cozystack follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
|
||||
|
||||
# Cozystack Vendor Neutrality Manifesto
|
||||
|
||||
Cozystack exists for the cloud-native community. We are committed to a project culture where no single company, product, or commercial agenda directs our roadmap, governance, brand, or releases. Our North Star is user value, technical excellence, and open collaboration under the CNCF umbrella.
|
||||
|
||||
## Our Commitments
|
||||
|
||||
- **Community-first:** Decisions prioritize the broader community over any vendor interest.
|
||||
- **Open collaboration:** Ideas, discussions, and outcomes happen in public spaces; contributions are welcomed from all.
|
||||
- **Merit over affiliation:** Proposals are evaluated on technical merit and user impact, not on who submits them.
|
||||
- **Inclusive stewardship:** Leadership and maintenance are open to contributors who demonstrate sustained, constructive impact.
|
||||
- **Technology choice:** We prefer open, pluggable designs that interoperate with multiple ecosystems and providers.
|
||||
- **Neutral brand & voice:** Our name, logo, website, and documentation do not imply endorsement or preference for any vendor.
|
||||
- **Transparent practices:** Funding acknowledgments, partnerships, and potential conflicts are communicated openly.
|
||||
- **User trust:** Security handling, releases, and communications aim to be timely, transparent, and fair to all users.
|
||||
|
||||
By contributing to Cozystack, we affirm these principles and work together to keep the project open, welcoming, and vendor-neutral.
|
||||
|
||||
*— The Cozystack community*
|
||||
|
||||
151
CONTRIBUTOR_LADDER.md
Normal file
151
CONTRIBUTOR_LADDER.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Contributor Ladder
|
||||
|
||||
* [Contributor Ladder](#contributor-ladder)
|
||||
* [Community Participant](#community-participant)
|
||||
* [Contributor](#contributor)
|
||||
* [Reviewer](#reviewer)
|
||||
* [Maintainer](#maintainer)
|
||||
* [Inactivity](#inactivity)
|
||||
* [Involuntary Removal](#involuntary-removal-or-demotion)
|
||||
* [Stepping Down/Emeritus Process](#stepping-downemeritus-process)
|
||||
* [Contact](#contact)
|
||||
|
||||
|
||||
## Contributor Ladder
|
||||
|
||||
Hello! We are excited that you want to learn more about our project contributor ladder! This contributor ladder outlines the different contributor roles within the project, along with the responsibilities and privileges that come with them. Community members generally start at the first levels of the "ladder" and advance up it as their involvement in the project grows. Our project members are happy to help you advance along the contributor ladder.
|
||||
|
||||
Each of the contributor roles below is organized into lists of three types of things. "Responsibilities" are things that a contributor is expected to do. "Requirements" are qualifications a person needs to meet to be in that role, and "Privileges" are things contributors on that level are entitled to.
|
||||
|
||||
|
||||
### Community Participant
|
||||
Description: A Community Participant engages with the project and its community, contributing their time, thoughts, etc. Community participants are usually users who have stopped being anonymous and started being active in project discussions.
|
||||
|
||||
* Responsibilities:
|
||||
* Must follow the [CNCF CoC](https://github.com/cncf/foundation/blob/main/code-of-conduct.md)
|
||||
* How users can get involved with the community:
|
||||
* Participating in community discussions
|
||||
* Helping other users
|
||||
* Submitting bug reports
|
||||
* Commenting on issues
|
||||
* Trying out new releases
|
||||
* Attending community events
|
||||
|
||||
|
||||
### Contributor
|
||||
Description: A Contributor contributes directly to the project and adds value to it. Contributions need not be code. People at the Contributor level may be new contributors, or they may only contribute occasionally.
|
||||
|
||||
* Responsibilities include:
|
||||
* Follow the [CNCF CoC](https://github.com/cncf/foundation/blob/main/code-of-conduct.md)
|
||||
* Follow the project [contributing guide] (https://github.com/cozystack/cozystack/blob/main/CONTRIBUTING.md)
|
||||
* Requirements (one or several of the below):
|
||||
* Report and sometimes resolve issues
|
||||
* Occasionally submit PRs
|
||||
* Contribute to the documentation
|
||||
* Show up at meetings, takes notes
|
||||
* Answer questions from other community members
|
||||
* Submit feedback on issues and PRs
|
||||
* Test releases and patches and submit reviews
|
||||
* Run or helps run events
|
||||
* Promote the project in public
|
||||
* Help run the project infrastructure
|
||||
* Privileges:
|
||||
* Invitations to contributor events
|
||||
* Eligible to become a Maintainer
|
||||
|
||||
|
||||
### Reviewer
|
||||
Description: A Reviewer has responsibility for specific code, documentation, test, or other project areas. They are collectively responsible, with other Reviewers, for reviewing all changes to those areas and indicating whether those changes are ready to merge. They have a track record of contribution and review in the project.
|
||||
|
||||
Reviewers are responsible for a "specific area." This can be a specific code directory, driver, chapter of the docs, test job, event, or other clearly-defined project component that is smaller than an entire repository or subproject. Most often it is one or a set of directories in one or more Git repositories. The "specific area" below refers to this area of responsibility.
|
||||
|
||||
Reviewers have all the rights and responsibilities of a Contributor, plus:
|
||||
|
||||
* Responsibilities include:
|
||||
* Continues to contribute regularly, as demonstrated by having at least 15 PRs a year, as demonstrated by [Cozystack devstats](https://cozystack.devstats.cncf.io).
|
||||
* Following the reviewing guide
|
||||
* Reviewing most Pull Requests against their specific areas of responsibility
|
||||
* Reviewing at least 40 PRs per year
|
||||
* Helping other contributors become reviewers
|
||||
* Requirements:
|
||||
* Must have successful contributions to the project, including at least one of the following:
|
||||
* 10 accepted PRs,
|
||||
* Reviewed 20 PRs,
|
||||
* Resolved and closed 20 Issues,
|
||||
* Become responsible for a key project management area,
|
||||
* Or some equivalent combination or contribution
|
||||
* Must have been contributing for at least 6 months
|
||||
* Must be actively contributing to at least one project area
|
||||
* Must have two sponsors who are also Reviewers or Maintainers, at least one of whom does not work for the same employer
|
||||
* Has reviewed, or helped review, at least 20 Pull Requests
|
||||
* Has analyzed and resolved test failures in their specific area
|
||||
* Has demonstrated an in-depth knowledge of the specific area
|
||||
* Commits to being responsible for that specific area
|
||||
* Is supportive of new and occasional contributors and helps get useful PRs in shape to commit
|
||||
* Additional privileges:
|
||||
* Has GitHub or CI/CD rights to approve pull requests in specific directories
|
||||
* Can recommend and review other contributors to become Reviewers
|
||||
* May be assigned Issues and Reviews
|
||||
* May give commands to CI/CD automation
|
||||
* Can recommend other contributors to become Reviewers
|
||||
|
||||
|
||||
The process of becoming a Reviewer is:
|
||||
1. The contributor is nominated by opening a PR against the appropriate repository, which adds their GitHub username to the OWNERS file for one or more directories.
|
||||
2. At least two members of the team that owns that repository or main directory, who are already Approvers, approve the PR.
|
||||
|
||||
|
||||
### Maintainer
|
||||
Description: Maintainers are very established contributors who are responsible for the entire project. As such, they have the ability to approve PRs against any area of the project, and are expected to participate in making decisions about the strategy and priorities of the project.
|
||||
|
||||
A Maintainer must meet the responsibilities and requirements of a Reviewer, plus:
|
||||
|
||||
* Responsibilities include:
|
||||
* Reviewing at least 40 PRs per year, especially PRs that involve multiple parts of the project
|
||||
* Mentoring new Reviewers
|
||||
* Writing refactoring PRs
|
||||
* Participating in CNCF maintainer activities
|
||||
* Determining strategy and policy for the project
|
||||
* Participating in, and leading, community meetings
|
||||
* Requirements
|
||||
* Experience as a Reviewer for at least 6 months
|
||||
* Demonstrates a broad knowledge of the project across multiple areas
|
||||
* Is able to exercise judgment for the good of the project, independent of their employer, friends, or team
|
||||
* Mentors other contributors
|
||||
* Can commit to spending at least 10 hours per month working on the project
|
||||
* Additional privileges:
|
||||
* Approve PRs to any area of the project
|
||||
* Represent the project in public as a Maintainer
|
||||
* Communicate with the CNCF on behalf of the project
|
||||
* Have a vote in Maintainer decision-making meetings
|
||||
|
||||
|
||||
Process of becoming a maintainer:
|
||||
1. Any current Maintainer may nominate a current Reviewer to become a new Maintainer, by opening a PR against the root of the cozystack repository adding the nominee as an Approver in the [MAINTAINERS](https://github.com/cozystack/cozystack/blob/main/MAINTAINERS.md) file.
|
||||
2. The nominee will add a comment to the PR testifying that they agree to all requirements of becoming a Maintainer.
|
||||
3. A majority of the current Maintainers must then approve the PR.
|
||||
|
||||
|
||||
## Inactivity
|
||||
It is important for contributors to be and stay active to set an example and show commitment to the project. Inactivity is harmful to the project as it may lead to unexpected delays, contributor attrition, and a lost of trust in the project.
|
||||
|
||||
* Inactivity is measured by:
|
||||
* Periods of no contributions for longer than 6 months
|
||||
* Periods of no communication for longer than 3 months
|
||||
* Consequences of being inactive include:
|
||||
* Involuntary removal or demotion
|
||||
* Being asked to move to Emeritus status
|
||||
|
||||
## Involuntary Removal or Demotion
|
||||
|
||||
Involuntary removal/demotion of a contributor happens when responsibilities and requirements aren't being met. This may include repeated patterns of inactivity, extended period of inactivity, a period of failing to meet the requirements of your role, and/or a violation of the Code of Conduct. This process is important because it protects the community and its deliverables while also opens up opportunities for new contributors to step in.
|
||||
|
||||
Involuntary removal or demotion is handled through a vote by a majority of the current Maintainers.
|
||||
|
||||
## Stepping Down/Emeritus Process
|
||||
If and when contributors' commitment levels change, contributors can consider stepping down (moving down the contributor ladder) vs moving to emeritus status (completely stepping away from the project).
|
||||
|
||||
Contact the Maintainers about changing to Emeritus status, or reducing your contributor level.
|
||||
|
||||
## Contact
|
||||
* For inquiries, please reach out to: @kvaps, @tym83
|
||||
@@ -7,6 +7,6 @@
|
||||
| Kingdon Barrett | [@kingdonb](https://github.com/kingdonb) | Urmanac | FluxCD and flux-operator |
|
||||
| Timofei Larkin | [@lllamnyp](https://github.com/lllamnyp) | 3commas | Etcd-operator Lead |
|
||||
| Artem Bortnikov | [@aobort](https://github.com/aobort) | Timescale | Etcd-operator Lead |
|
||||
| Andrei Gumilev | [@chumkaska](https://github.com/chumkaska) | Ænix | Platform Documentation |
|
||||
| Timur Tukaev | [@tym83](https://github.com/tym83) | Ænix | Cozystack Website, Marketing, Community Management |
|
||||
| Kirill Klinchenkov | [@klinch0](https://github.com/klinch0) | Ænix | Core Maintainer |
|
||||
| Nikita Bykov | [@nbykov0](https://github.com/nbykov0) | Ænix | Maintainer of ARM and stuff |
|
||||
|
||||
6
Makefile
6
Makefile
@@ -18,6 +18,7 @@ build: build-deps
|
||||
make -C packages/system/cilium image
|
||||
make -C packages/system/kubeovn image
|
||||
make -C packages/system/kubeovn-webhook image
|
||||
make -C packages/system/kubeovn-plunger image
|
||||
make -C packages/system/dashboard image
|
||||
make -C packages/system/metallb image
|
||||
make -C packages/system/kamaji image
|
||||
@@ -29,14 +30,9 @@ build: build-deps
|
||||
|
||||
repos:
|
||||
rm -rf _out
|
||||
make -C packages/apps check-version-map
|
||||
make -C packages/extra check-version-map
|
||||
make -C packages/system repo
|
||||
make -C packages/apps repo
|
||||
make -C packages/extra repo
|
||||
mkdir -p _out/logos
|
||||
cp ./packages/apps/*/logos/*.svg ./packages/extra/*/logos/*.svg _out/logos/
|
||||
|
||||
|
||||
manifests:
|
||||
mkdir -p _out/assets
|
||||
|
||||
@@ -19,7 +19,7 @@ Database-as-a-Service, virtual machines, load balancers, HTTP caching services,
|
||||
|
||||
Use Cozystack to build your own cloud or provide a cost-effective development environment.
|
||||
|
||||

|
||||

|
||||
|
||||
## Use-Cases
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
API rule violation: list_type_missing,github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1,ApplicationStatus,Conditions
|
||||
API rule violation: list_type_missing,github.com/cozystack/cozystack/pkg/apis/core/v1alpha1,TenantModuleStatus,Conditions
|
||||
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,Ref
|
||||
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,Schema
|
||||
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,XEmbeddedResource
|
||||
|
||||
255
api/dashboard/v1alpha1/dashboard_resources.go
Normal file
255
api/dashboard/v1alpha1/dashboard_resources.go
Normal file
@@ -0,0 +1,255 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Package v1alpha1 defines front.in-cloud.io API types.
|
||||
//
|
||||
// Group: dashboard.cozystack.io
|
||||
// Version: v1alpha1
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Shared shapes
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// CommonStatus is a generic Status block with Kubernetes conditions.
|
||||
type CommonStatus struct {
|
||||
// ObservedGeneration reflects the most recent generation observed by the controller.
|
||||
// +optional
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
|
||||
// Conditions represent the latest available observations of an object's state.
|
||||
// +optional
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
|
||||
// ArbitrarySpec holds schemaless user data and preserves unknown fields.
|
||||
// We map the entire .spec to a single JSON payload to mirror the CRDs you provided.
|
||||
// NOTE: Using apiextensionsv1.JSON avoids losing arbitrary structure during round-trips.
|
||||
type ArbitrarySpec struct {
|
||||
// +kubebuilder:validation:XPreserveUnknownFields
|
||||
// +kubebuilder:pruning:PreserveUnknownFields
|
||||
v1.JSON `json:",inline"`
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Sidebar
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:path=sidebars,scope=Cluster
|
||||
// +kubebuilder:subresource:status
|
||||
type Sidebar struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ArbitrarySpec `json:"spec"`
|
||||
Status CommonStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
type SidebarList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Sidebar `json:"items"`
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// CustomFormsPrefill (shortName: cfp)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:path=customformsprefills,scope=Cluster,shortName=cfp
|
||||
// +kubebuilder:subresource:status
|
||||
type CustomFormsPrefill struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ArbitrarySpec `json:"spec"`
|
||||
Status CommonStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
type CustomFormsPrefillList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []CustomFormsPrefill `json:"items"`
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// BreadcrumbInside
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:path=breadcrumbsinside,scope=Cluster
|
||||
// +kubebuilder:subresource:status
|
||||
type BreadcrumbInside struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ArbitrarySpec `json:"spec"`
|
||||
Status CommonStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
type BreadcrumbInsideList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []BreadcrumbInside `json:"items"`
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// CustomFormsOverride (shortName: cfo)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:path=customformsoverrides,scope=Cluster,shortName=cfo
|
||||
// +kubebuilder:subresource:status
|
||||
type CustomFormsOverride struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ArbitrarySpec `json:"spec"`
|
||||
Status CommonStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
type CustomFormsOverrideList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []CustomFormsOverride `json:"items"`
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// TableUriMapping
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:path=tableurimappings,scope=Cluster
|
||||
// +kubebuilder:subresource:status
|
||||
type TableUriMapping struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ArbitrarySpec `json:"spec"`
|
||||
Status CommonStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
type TableUriMappingList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []TableUriMapping `json:"items"`
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Breadcrumb
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:path=breadcrumbs,scope=Cluster
|
||||
// +kubebuilder:subresource:status
|
||||
type Breadcrumb struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ArbitrarySpec `json:"spec"`
|
||||
Status CommonStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
type BreadcrumbList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Breadcrumb `json:"items"`
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// MarketplacePanel
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:path=marketplacepanels,scope=Cluster
|
||||
// +kubebuilder:subresource:status
|
||||
type MarketplacePanel struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ArbitrarySpec `json:"spec"`
|
||||
Status CommonStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
type MarketplacePanelList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []MarketplacePanel `json:"items"`
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Navigation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:path=navigations,scope=Cluster
|
||||
// +kubebuilder:subresource:status
|
||||
type Navigation struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ArbitrarySpec `json:"spec"`
|
||||
Status CommonStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
type NavigationList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Navigation `json:"items"`
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// CustomColumnsOverride
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:path=customcolumnsoverrides,scope=Cluster
|
||||
// +kubebuilder:subresource:status
|
||||
type CustomColumnsOverride struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ArbitrarySpec `json:"spec"`
|
||||
Status CommonStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
type CustomColumnsOverrideList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []CustomColumnsOverride `json:"items"`
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Factory
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:path=factories,scope=Cluster
|
||||
// +kubebuilder:subresource:status
|
||||
type Factory struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ArbitrarySpec `json:"spec"`
|
||||
Status CommonStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
type FactoryList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Factory `json:"items"`
|
||||
}
|
||||
75
api/dashboard/v1alpha1/groupversion_info.go
Normal file
75
api/dashboard/v1alpha1/groupversion_info.go
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package v1alpha1 contains API Schema definitions for the v1alpha1 API group.
|
||||
// +kubebuilder:object:generate=true
|
||||
// +groupName=dashboard.cozystack.io
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
// GroupVersion is group version used to register these objects.
|
||||
GroupVersion = schema.GroupVersion{Group: "dashboard.cozystack.io", Version: "v1alpha1"}
|
||||
|
||||
// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
|
||||
|
||||
// AddToScheme adds the types in this group-version to the given scheme.
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(
|
||||
GroupVersion,
|
||||
|
||||
&Sidebar{},
|
||||
&SidebarList{},
|
||||
|
||||
&CustomFormsPrefill{},
|
||||
&CustomFormsPrefillList{},
|
||||
|
||||
&BreadcrumbInside{},
|
||||
&BreadcrumbInsideList{},
|
||||
|
||||
&CustomFormsOverride{},
|
||||
&CustomFormsOverrideList{},
|
||||
|
||||
&TableUriMapping{},
|
||||
&TableUriMappingList{},
|
||||
|
||||
&Breadcrumb{},
|
||||
&BreadcrumbList{},
|
||||
|
||||
&MarketplacePanel{},
|
||||
&MarketplacePanelList{},
|
||||
|
||||
&Navigation{},
|
||||
&NavigationList{},
|
||||
|
||||
&CustomColumnsOverride{},
|
||||
&CustomColumnsOverrideList{},
|
||||
|
||||
&Factory{},
|
||||
&FactoryList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, GroupVersion)
|
||||
return nil
|
||||
}
|
||||
654
api/dashboard/v1alpha1/zz_generated.deepcopy.go
Normal file
654
api/dashboard/v1alpha1/zz_generated.deepcopy.go
Normal file
@@ -0,0 +1,654 @@
|
||||
//go:build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ArbitrarySpec) DeepCopyInto(out *ArbitrarySpec) {
|
||||
*out = *in
|
||||
in.JSON.DeepCopyInto(&out.JSON)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArbitrarySpec.
|
||||
func (in *ArbitrarySpec) DeepCopy() *ArbitrarySpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ArbitrarySpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Breadcrumb) DeepCopyInto(out *Breadcrumb) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Breadcrumb.
|
||||
func (in *Breadcrumb) DeepCopy() *Breadcrumb {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Breadcrumb)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Breadcrumb) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BreadcrumbInside) DeepCopyInto(out *BreadcrumbInside) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BreadcrumbInside.
|
||||
func (in *BreadcrumbInside) DeepCopy() *BreadcrumbInside {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BreadcrumbInside)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *BreadcrumbInside) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BreadcrumbInsideList) DeepCopyInto(out *BreadcrumbInsideList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]BreadcrumbInside, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BreadcrumbInsideList.
|
||||
func (in *BreadcrumbInsideList) DeepCopy() *BreadcrumbInsideList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BreadcrumbInsideList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *BreadcrumbInsideList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BreadcrumbList) DeepCopyInto(out *BreadcrumbList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Breadcrumb, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BreadcrumbList.
|
||||
func (in *BreadcrumbList) DeepCopy() *BreadcrumbList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BreadcrumbList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *BreadcrumbList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CommonStatus) DeepCopyInto(out *CommonStatus) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]v1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonStatus.
|
||||
func (in *CommonStatus) DeepCopy() *CommonStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CommonStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CustomColumnsOverride) DeepCopyInto(out *CustomColumnsOverride) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomColumnsOverride.
|
||||
func (in *CustomColumnsOverride) DeepCopy() *CustomColumnsOverride {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CustomColumnsOverride)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *CustomColumnsOverride) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CustomColumnsOverrideList) DeepCopyInto(out *CustomColumnsOverrideList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]CustomColumnsOverride, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomColumnsOverrideList.
|
||||
func (in *CustomColumnsOverrideList) DeepCopy() *CustomColumnsOverrideList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CustomColumnsOverrideList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *CustomColumnsOverrideList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CustomFormsOverride) DeepCopyInto(out *CustomFormsOverride) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomFormsOverride.
|
||||
func (in *CustomFormsOverride) DeepCopy() *CustomFormsOverride {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CustomFormsOverride)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *CustomFormsOverride) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CustomFormsOverrideList) DeepCopyInto(out *CustomFormsOverrideList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]CustomFormsOverride, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomFormsOverrideList.
|
||||
func (in *CustomFormsOverrideList) DeepCopy() *CustomFormsOverrideList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CustomFormsOverrideList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *CustomFormsOverrideList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CustomFormsPrefill) DeepCopyInto(out *CustomFormsPrefill) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomFormsPrefill.
|
||||
func (in *CustomFormsPrefill) DeepCopy() *CustomFormsPrefill {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CustomFormsPrefill)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *CustomFormsPrefill) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CustomFormsPrefillList) DeepCopyInto(out *CustomFormsPrefillList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]CustomFormsPrefill, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomFormsPrefillList.
|
||||
func (in *CustomFormsPrefillList) DeepCopy() *CustomFormsPrefillList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CustomFormsPrefillList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *CustomFormsPrefillList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Factory) DeepCopyInto(out *Factory) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Factory.
|
||||
func (in *Factory) DeepCopy() *Factory {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Factory)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Factory) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FactoryList) DeepCopyInto(out *FactoryList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Factory, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FactoryList.
|
||||
func (in *FactoryList) DeepCopy() *FactoryList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FactoryList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *FactoryList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *MarketplacePanel) DeepCopyInto(out *MarketplacePanel) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MarketplacePanel.
|
||||
func (in *MarketplacePanel) DeepCopy() *MarketplacePanel {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(MarketplacePanel)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *MarketplacePanel) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *MarketplacePanelList) DeepCopyInto(out *MarketplacePanelList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]MarketplacePanel, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MarketplacePanelList.
|
||||
func (in *MarketplacePanelList) DeepCopy() *MarketplacePanelList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(MarketplacePanelList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *MarketplacePanelList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Navigation) DeepCopyInto(out *Navigation) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Navigation.
|
||||
func (in *Navigation) DeepCopy() *Navigation {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Navigation)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Navigation) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *NavigationList) DeepCopyInto(out *NavigationList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Navigation, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NavigationList.
|
||||
func (in *NavigationList) DeepCopy() *NavigationList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(NavigationList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *NavigationList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Sidebar) DeepCopyInto(out *Sidebar) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Sidebar.
|
||||
func (in *Sidebar) DeepCopy() *Sidebar {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Sidebar)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Sidebar) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SidebarList) DeepCopyInto(out *SidebarList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Sidebar, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SidebarList.
|
||||
func (in *SidebarList) DeepCopy() *SidebarList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SidebarList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *SidebarList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *TableUriMapping) DeepCopyInto(out *TableUriMapping) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TableUriMapping.
|
||||
func (in *TableUriMapping) DeepCopy() *TableUriMapping {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(TableUriMapping)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *TableUriMapping) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *TableUriMappingList) DeepCopyInto(out *TableUriMappingList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]TableUriMapping, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TableUriMappingList.
|
||||
func (in *TableUriMappingList) DeepCopy() *TableUriMappingList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(TableUriMappingList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *TableUriMappingList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
)
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:scope=Cluster
|
||||
|
||||
// CozystackResourceDefinition is the Schema for the cozystackresourcedefinitions API
|
||||
type CozystackResourceDefinition struct {
|
||||
@@ -32,7 +33,7 @@ type CozystackResourceDefinition struct {
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// CozystackResourceDefinitionList contains a list of CozystackResourceDefinition
|
||||
// CozystackResourceDefinitionList contains a list of CozystackResourceDefinitions
|
||||
type CozystackResourceDefinitionList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
@@ -48,6 +49,16 @@ type CozystackResourceDefinitionSpec struct {
|
||||
Application CozystackResourceDefinitionApplication `json:"application"`
|
||||
// Release configuration
|
||||
Release CozystackResourceDefinitionRelease `json:"release"`
|
||||
|
||||
// Secret selectors
|
||||
Secrets CozystackResourceDefinitionResources `json:"secrets,omitempty"`
|
||||
// Service selectors
|
||||
Services CozystackResourceDefinitionResources `json:"services,omitempty"`
|
||||
// Ingress selectors
|
||||
Ingresses CozystackResourceDefinitionResources `json:"ingresses,omitempty"`
|
||||
|
||||
// Dashboard configuration for this resource
|
||||
Dashboard *CozystackResourceDefinitionDashboard `json:"dashboard,omitempty"`
|
||||
}
|
||||
|
||||
type CozystackResourceDefinitionChart struct {
|
||||
@@ -87,3 +98,96 @@ type CozystackResourceDefinitionRelease struct {
|
||||
// Prefix for the release name
|
||||
Prefix string `json:"prefix"`
|
||||
}
|
||||
|
||||
// CozystackResourceDefinitionResourceSelector extends metav1.LabelSelector with resourceNames support.
|
||||
// A resource matches this selector only if it satisfies ALL criteria:
|
||||
// - Label selector conditions (matchExpressions and matchLabels)
|
||||
// - AND has a name that matches one of the names in resourceNames (if specified)
|
||||
//
|
||||
// The resourceNames field supports Go templates with the following variables available:
|
||||
// - {{ .name }}: The name of the managing application (from apps.cozystack.io/application.name)
|
||||
// - {{ .kind }}: The lowercased kind of the managing application (from apps.cozystack.io/application.kind)
|
||||
// - {{ .namespace }}: The namespace of the resource being processed
|
||||
//
|
||||
// Example YAML:
|
||||
// secrets:
|
||||
// include:
|
||||
// - matchExpressions:
|
||||
// - key: badlabel
|
||||
// operator: DoesNotExist
|
||||
// matchLabels:
|
||||
// goodlabel: goodvalue
|
||||
// resourceNames:
|
||||
// - "{{ .name }}-secret"
|
||||
// - "{{ .kind }}-{{ .name }}-tls"
|
||||
// - "specificname"
|
||||
type CozystackResourceDefinitionResourceSelector struct {
|
||||
metav1.LabelSelector `json:",inline"`
|
||||
// ResourceNames is a list of resource names to match
|
||||
// If specified, the resource must have one of these exact names to match the selector
|
||||
// +optional
|
||||
ResourceNames []string `json:"resourceNames,omitempty"`
|
||||
}
|
||||
|
||||
type CozystackResourceDefinitionResources struct {
|
||||
// Exclude contains an array of resource selectors that target resources.
|
||||
// If a resource matches the selector in any of the elements in the array, it is
|
||||
// hidden from the user, regardless of the matches in the include array.
|
||||
Exclude []*CozystackResourceDefinitionResourceSelector `json:"exclude,omitempty"`
|
||||
// Include contains an array of resource selectors that target resources.
|
||||
// If a resource matches the selector in any of the elements in the array, and
|
||||
// matches none of the selectors in the exclude array that resource is marked
|
||||
// as a tenant resource and is visible to users.
|
||||
Include []*CozystackResourceDefinitionResourceSelector `json:"include,omitempty"`
|
||||
}
|
||||
|
||||
// ---- Dashboard types ----
|
||||
|
||||
// DashboardTab enumerates allowed UI tabs.
|
||||
// +kubebuilder:validation:Enum=workloads;ingresses;services;secrets;yaml
|
||||
type DashboardTab string
|
||||
|
||||
const (
|
||||
DashboardTabWorkloads DashboardTab = "workloads"
|
||||
DashboardTabIngresses DashboardTab = "ingresses"
|
||||
DashboardTabServices DashboardTab = "services"
|
||||
DashboardTabSecrets DashboardTab = "secrets"
|
||||
DashboardTabYAML DashboardTab = "yaml"
|
||||
)
|
||||
|
||||
// CozystackResourceDefinitionDashboard describes how this resource appears in the UI.
|
||||
type CozystackResourceDefinitionDashboard struct {
|
||||
// Human-readable name shown in the UI (e.g., "Bucket")
|
||||
Singular string `json:"singular"`
|
||||
// Plural human-readable name (e.g., "Buckets")
|
||||
Plural string `json:"plural"`
|
||||
// Hard-coded name used in the UI (e.g., "bucket")
|
||||
// +optional
|
||||
Name string `json:"name,omitempty"`
|
||||
// Whether this resource is singular (not a collection) in the UI
|
||||
// +optional
|
||||
SingularResource bool `json:"singularResource,omitempty"`
|
||||
// Order weight for sorting resources in the UI (lower first)
|
||||
// +optional
|
||||
Weight int `json:"weight,omitempty"`
|
||||
// Short description shown in catalogs or headers (e.g., "S3 compatible storage")
|
||||
// +optional
|
||||
Description string `json:"description,omitempty"`
|
||||
// Icon encoded as a string (e.g., inline SVG, base64, or data URI)
|
||||
// +optional
|
||||
Icon string `json:"icon,omitempty"`
|
||||
// Category used to group resources in the UI (e.g., "Storage", "Networking")
|
||||
Category string `json:"category"`
|
||||
// Free-form tags for search and filtering
|
||||
// +optional
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
// Which tabs to show for this resource
|
||||
// +optional
|
||||
Tabs []DashboardTab `json:"tabs,omitempty"`
|
||||
// Order of keys in the YAML view
|
||||
// +optional
|
||||
KeysOrder [][]string `json:"keysOrder,omitempty"`
|
||||
// Whether this resource is a module (tenant module)
|
||||
// +optional
|
||||
Module bool `json:"module,omitempty"`
|
||||
}
|
||||
|
||||
@@ -82,6 +82,42 @@ func (in *CozystackResourceDefinitionChart) DeepCopy() *CozystackResourceDefinit
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionDashboard) DeepCopyInto(out *CozystackResourceDefinitionDashboard) {
|
||||
*out = *in
|
||||
if in.Tags != nil {
|
||||
in, out := &in.Tags, &out.Tags
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Tabs != nil {
|
||||
in, out := &in.Tabs, &out.Tabs
|
||||
*out = make([]DashboardTab, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.KeysOrder != nil {
|
||||
in, out := &in.KeysOrder, &out.KeysOrder
|
||||
*out = make([][]string, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionDashboard.
|
||||
func (in *CozystackResourceDefinitionDashboard) DeepCopy() *CozystackResourceDefinitionDashboard {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CozystackResourceDefinitionDashboard)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionList) DeepCopyInto(out *CozystackResourceDefinitionList) {
|
||||
*out = *in
|
||||
@@ -137,11 +173,77 @@ func (in *CozystackResourceDefinitionRelease) DeepCopy() *CozystackResourceDefin
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionResourceSelector) DeepCopyInto(out *CozystackResourceDefinitionResourceSelector) {
|
||||
*out = *in
|
||||
in.LabelSelector.DeepCopyInto(&out.LabelSelector)
|
||||
if in.ResourceNames != nil {
|
||||
in, out := &in.ResourceNames, &out.ResourceNames
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionResourceSelector.
|
||||
func (in *CozystackResourceDefinitionResourceSelector) DeepCopy() *CozystackResourceDefinitionResourceSelector {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CozystackResourceDefinitionResourceSelector)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionResources) DeepCopyInto(out *CozystackResourceDefinitionResources) {
|
||||
*out = *in
|
||||
if in.Exclude != nil {
|
||||
in, out := &in.Exclude, &out.Exclude
|
||||
*out = make([]*CozystackResourceDefinitionResourceSelector, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = new(CozystackResourceDefinitionResourceSelector)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
}
|
||||
if in.Include != nil {
|
||||
in, out := &in.Include, &out.Include
|
||||
*out = make([]*CozystackResourceDefinitionResourceSelector, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = new(CozystackResourceDefinitionResourceSelector)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionResources.
|
||||
func (in *CozystackResourceDefinitionResources) DeepCopy() *CozystackResourceDefinitionResources {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CozystackResourceDefinitionResources)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionSpec) DeepCopyInto(out *CozystackResourceDefinitionSpec) {
|
||||
*out = *in
|
||||
out.Application = in.Application
|
||||
in.Release.DeepCopyInto(&out.Release)
|
||||
in.Secrets.DeepCopyInto(&out.Secrets)
|
||||
in.Services.DeepCopyInto(&out.Services)
|
||||
in.Ingresses.DeepCopyInto(&out.Ingresses)
|
||||
if in.Dashboard != nil {
|
||||
in, out := &in.Dashboard, &out.Dashboard
|
||||
*out = new(CozystackResourceDefinitionDashboard)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionSpec.
|
||||
|
||||
@@ -26,8 +26,8 @@ import (
|
||||
|
||||
func main() {
|
||||
ctx := genericapiserver.SetupSignalContext()
|
||||
options := server.NewAppsServerOptions(os.Stdout, os.Stderr)
|
||||
cmd := server.NewCommandStartAppsServer(ctx, options)
|
||||
options := server.NewCozyServerOptions(os.Stdout, os.Stderr)
|
||||
cmd := server.NewCommandStartCozyServer(ctx, options)
|
||||
code := cli.Run(cmd)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ import (
|
||||
|
||||
cozystackiov1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
"github.com/cozystack/cozystack/internal/controller"
|
||||
"github.com/cozystack/cozystack/internal/controller/dashboard"
|
||||
lcw "github.com/cozystack/cozystack/internal/lineagecontrollerwebhook"
|
||||
"github.com/cozystack/cozystack/internal/telemetry"
|
||||
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
@@ -53,6 +55,7 @@ func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
|
||||
utilruntime.Must(cozystackiov1alpha1.AddToScheme(scheme))
|
||||
utilruntime.Must(dashboard.AddToScheme(scheme))
|
||||
utilruntime.Must(helmv2.AddToScheme(scheme))
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
@@ -150,7 +153,12 @@ func main() {
|
||||
// this setup is not recommended for production.
|
||||
}
|
||||
|
||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||
// Configure rate limiting for the Kubernetes client
|
||||
config := ctrl.GetConfigOrDie()
|
||||
config.QPS = 50.0 // Increased from default 5.0
|
||||
config.Burst = 100 // Increased from default 10
|
||||
|
||||
mgr, err := ctrl.NewManager(config, ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsServerOptions,
|
||||
WebhookServer: webhookServer,
|
||||
@@ -206,6 +214,28 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&controller.CozystackResourceDefinitionReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "CozystackResourceDefinitionReconciler")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// special one that's both a webhook and a reconciler
|
||||
lineageControllerWebhook := &lcw.LineageControllerWebhook{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}
|
||||
if err := lineageControllerWebhook.SetupWithManagerAsController(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to setup controller", "controller", "LineageController")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := lineageControllerWebhook.SetupWithManagerAsWebhook(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to setup webhook", "webhook", "LineageWebhook")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
|
||||
176
cmd/kubeovn-plunger/main.go
Normal file
176
cmd/kubeovn-plunger/main.go
Normal file
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
"sigs.k8s.io/controller-runtime/pkg/metrics"
|
||||
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
|
||||
"github.com/cozystack/cozystack/internal/controller/kubeovnplunger"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
)
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
var kubeOVNNamespace string
|
||||
var ovnCentralName string
|
||||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var disableTelemetry bool
|
||||
var tlsOpts []func(*tls.Config)
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
||||
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
|
||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||
flag.StringVar(&kubeOVNNamespace, "kube-ovn-namespace", "cozy-kubeovn", "Namespace where kube-OVN is deployed.")
|
||||
flag.StringVar(&ovnCentralName, "ovn-central-name", "ovn-central", "Ovn-central deployment name.")
|
||||
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||
"Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
flag.BoolVar(&secureMetrics, "metrics-secure", true,
|
||||
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
|
||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||
flag.BoolVar(&disableTelemetry, "disable-telemetry", false,
|
||||
"Disable telemetry collection")
|
||||
opts := zap.Options{
|
||||
Development: false,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||
// due to its vulnerabilities. More specifically, disabling http/2 will
|
||||
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
|
||||
// Rapid Reset CVEs. For more information see:
|
||||
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
|
||||
// - https://github.com/advisories/GHSA-4374-p667-p6c8
|
||||
disableHTTP2 := func(c *tls.Config) {
|
||||
setupLog.Info("disabling http/2")
|
||||
c.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
|
||||
if !enableHTTP2 {
|
||||
tlsOpts = append(tlsOpts, disableHTTP2)
|
||||
}
|
||||
|
||||
webhookServer := webhook.NewServer(webhook.Options{
|
||||
TLSOpts: tlsOpts,
|
||||
})
|
||||
|
||||
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
|
||||
// More info:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
|
||||
// - https://book.kubebuilder.io/reference/metrics.html
|
||||
metricsServerOptions := metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
SecureServing: secureMetrics,
|
||||
TLSOpts: tlsOpts,
|
||||
}
|
||||
|
||||
if secureMetrics {
|
||||
// FilterProvider is used to protect the metrics endpoint with authn/authz.
|
||||
// These configurations ensure that only authorized users and service accounts
|
||||
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
|
||||
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/filters#WithAuthenticationAndAuthorization
|
||||
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
|
||||
|
||||
// TODO(user): If CertDir, CertName, and KeyName are not specified, controller-runtime will automatically
|
||||
// generate self-signed certificates for the metrics server. While convenient for development and testing,
|
||||
// this setup is not recommended for production.
|
||||
}
|
||||
|
||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsServerOptions,
|
||||
WebhookServer: webhookServer,
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "29a0338b.cozystack.io",
|
||||
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
|
||||
// when the Manager ends. This requires the binary to immediately end when the
|
||||
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
|
||||
// speeds up voluntary leader transitions as the new leader don't have to wait
|
||||
// LeaseDuration time first.
|
||||
//
|
||||
// In the default scaffold provided, the program ends immediately after
|
||||
// the manager stops, so would be fine to enable this option. However,
|
||||
// if you are doing or is intended to do any operation such as perform cleanups
|
||||
// after the manager stops then its usage might be unsafe.
|
||||
// LeaderElectionReleaseOnCancel: true,
|
||||
})
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to create manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&kubeovnplunger.KubeOVNPlunger{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
Registry: metrics.Registry,
|
||||
}).SetupWithManager(mgr, kubeOVNNamespace, ovnCentralName); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "KubeOVNPlunger")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up health check")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up ready check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupLog.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0..
|
||||
-->
|
||||
|
||||
## Major Features and Improvements
|
||||
|
||||
## Security
|
||||
@@ -9,3 +14,7 @@
|
||||
## Documentation
|
||||
|
||||
## Development, Testing, and CI/CD
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: **Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.34.0...v0.35.0
|
||||
|
||||
@@ -129,7 +129,7 @@ For more information, read the [Cozystack Release Workflow](https://github.com/c
|
||||
* [platform] Reduce requested CPU and RAM for the `kamaji` provider. (@klinch0 in https://github.com/cozystack/cozystack/pull/825)
|
||||
* [platform] Improve the reconciliation loop for the Cozystack system HelmReleases logic. (@klinch0 in https://github.com/cozystack/cozystack/pull/809 and https://github.com/cozystack/cozystack/pull/810, @kvaps in https://github.com/cozystack/cozystack/pull/811)
|
||||
* [platform] Remove extra dependencies for the Piraeus operator. (@klinch0 in https://github.com/cozystack/cozystack/pull/856)
|
||||
* [platform] Refactor dashboard values. (@kvaps in https://github.com/cozystack/cozystack/pull/928, patched by @llamnyp in https://github.com/cozystack/cozystack/pull/952)
|
||||
* [platform] Refactor dashboard values. (@kvaps in https://github.com/cozystack/cozystack/pull/928, patched by @lllamnyp in https://github.com/cozystack/cozystack/pull/952)
|
||||
* [platform] Make FluxCD artifact disabled by default. (@klinch0 in https://github.com/cozystack/cozystack/pull/964)
|
||||
* [kubernetes] Update garbage collection of HelmReleases in tenant Kubernetes clusters. (@kvaps in https://github.com/cozystack/cozystack/pull/835)
|
||||
* [kubernetes] Fix merging `valuesOverride` for tenant clusters. (@kvaps in https://github.com/cozystack/cozystack/pull/879)
|
||||
|
||||
87
docs/changelogs/v0.34.0.md
Normal file
87
docs/changelogs/v0.34.0.md
Normal file
@@ -0,0 +1,87 @@
|
||||
Cozystack v0.34.0 is a stable release.
|
||||
It focuses on cluster reliability, virtualization capabilities, and enhancements to the Cozystack API.
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.34.0
|
||||
-->
|
||||
|
||||
> [!WARNING]
|
||||
> A regression was found in this release and fixed in patch [0.34.3](https://github.com/cozystack/cozystack/releases/tag/v0.34.3).
|
||||
> When upgrading Cozystack, it's recommended to skip this version and upgrade directly to [0.34.3](https://github.com/cozystack/cozystack/releases/tag/v0.34.3).
|
||||
|
||||
|
||||
## Major Features and Improvements
|
||||
|
||||
* [kubernetes] Enable users to select Kubernetes versions in tenant clusters. Supported versions range from 1.28 to 1.33, updated to the latest patches. (@lllamnyp and @IvanHunters in https://github.com/cozystack/cozystack/pull/1202)
|
||||
* [kubernetes] Enable PVC snapshot capability in tenant Kubernetes clusters. (@klinch0 in https://github.com/cozystack/cozystack/pull/1203)
|
||||
* [vpa] Implement autoscaling for the Vertical Pod Autoscaler itself, ensuring that VPA has sufficient resources and reducing the number of configuration parameters that platform administrators have to manage. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1198)
|
||||
* [vm-instance] Enable running [Windows](https://cozystack.io/docs/operations/virtualization/windows/) and [MikroTik RouterOS](https://cozystack.io/docs/operations/virtualization/mikrotik/) in Cozystack. Add `bus` option and always specify `bootOrder` for all disks. (@kvaps in https://github.com/cozystack/cozystack/pull/1168)
|
||||
* [cozystack-api] Specify OpenAPI schema for apps. (@kvaps in https://github.com/cozystack/cozystack/pull/1174)
|
||||
* [cozystack-api] Refactor OpenAPI Schema and support reading it from config. (@kvaps in https://github.com/cozystack/cozystack/pull/1173)
|
||||
* [cozystack-api] Enable using singular resource names in Cozystack API. For example, `kubectl get tenant` is now a valid command, in addition to `kubectl get tenants`. (@kvaps in https://github.com/cozystack/cozystack/pull/1169)
|
||||
* [postgres] Explain how to back up and restore PostgreSQL using Velero backups. (@klinch0 and @NickVolynkin in https://github.com/cozystack/cozystack/pull/1141)
|
||||
* [seaweedfs] Support multi-zone configuration for S3 storage. (@kvaps in https://github.com/cozystack/cozystack/pull/1194)
|
||||
* [dashboard] Put YAML editor first when deploying and upgrading applications, as a more powerful option. Fix handling multiline strings. (@kvaps in https://github.com/cozystack/cozystack/pull/1227)
|
||||
|
||||
## Security
|
||||
|
||||
* [seaweedfs] Ensure that JWT signing keys in the SeaweedFS security configuration remain consistent across Helm upgrades. Resolve an upstream issue. (@kvaps in https://github.com/cozystack/cozystack/pull/1193 and https://github.com/seaweedfs/seaweedfs/pull/6967)
|
||||
|
||||
## Fixes
|
||||
|
||||
* [cozystack-controller] Fix stale workloads not being deleted when marked for deletion. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1210, @kvaps in https://github.com/cozystack/cozystack/pull/1229)
|
||||
* [cozystack-controller] Improve reliability when updating HelmRelease objects to prevent unintended changes during reconciliation. (@klinch0 in https://github.com/cozystack/cozystack/pull/1205)
|
||||
* [kubevirt-csi] Fix a regression by updating the role of the CSI controller. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1165)
|
||||
* [virtual-machine,vm-instance] Adjusted RBAC role to let users read the service associated with the VMs they create. Consequently, users can now see details of the service in the dashboard and therefore read the IP address of the VM. (@klinch0 in https://github.com/cozystack/cozystack/pull/1161)
|
||||
* [virtual-machine] Fix cloudInit and sshKeys processing. (@kvaps in https://github.com/cozystack/cozystack/pull/1175 and https://github.com/cozystack/cozystack/commit/da3ee5d0ea9e87529c8adc4fcccffabe8782292e)
|
||||
* [cozystack-api] Fix an error with `resourceVersion` which resulted in message 'failed to update HelmRelease: helmreleases.helm.toolkit.fluxcd.io "xxx" is invalid...'. (@kvaps in https://github.com/cozystack/cozystack/pull/1170)
|
||||
* [cozystack-api] Fix an error in updating lists in Cozystack objects, which resulted in message "Warning: resource ... is missing the kubectl.kubernetes.io/last-applied-configuration annotation". (@kvaps in https://github.com/cozystack/cozystack/pull/1171)
|
||||
* [cozystack-api] Disable `strategic-json-patch` support. (@kvaps in https://github.com/cozystack/cozystack/pull/1179)
|
||||
* [cozystack-api] Fix non-existing OpenAPI references. (@kvaps in https://github.com/cozystack/cozystack/pull/1208)
|
||||
* [dashboard] Fix the code for removing dashboard comments which used to mistakenly remove shebang from `cloudInit` scripts. (@kvaps in https://github.com/cozystack/cozystack/pull/1175).
|
||||
* [applications] Reorder configuration values in application README's for better readability. (@NickVolynkin in https://github.com/cozystack/cozystack/pull/1214)
|
||||
* [applications] Disallow selecting `resourcePreset = none` in the visual editor when deploying and upgrading applications. (@NickVolynkin in https://github.com/cozystack/cozystack/pull/1196)
|
||||
* [applications] Fix a typo in preset resource tables in the built-in documentation of managed applications. (@NickVolynkin in https://github.com/cozystack/cozystack/pull/1172)
|
||||
* [kubernetes] Enable deleting Velero component from a tenant Kubernetes cluster. (@klinch0 in https://github.com/cozystack/cozystack/pull/1176)
|
||||
* [kubernetes] Explicitly mention available K8s versions for tenant clusters in the README. (@NickVolynkin in https://github.com/cozystack/cozystack/pull/1212)
|
||||
* [oidc] Enable deleting Keycloak service. (@klinch0 in https://github.com/cozystack/cozystack/pull/1178)
|
||||
* [tenant] Enable deleting extra applications from a tenant. (@klinch0 and @kvaps and in https://github.com/cozystack/cozystack/pull/1162)
|
||||
* [nats] Fix a typo in the application template. (@klinch0 in https://github.com/cozystack/cozystack/pull/1195)
|
||||
* [postgres] Resolve an issue with the visibility of PostgreSQL load balancer on the dashboard. (@klinch0 https://github.com/cozystack/cozystack/pull/1204)
|
||||
* [objectstorage] Update COSI controller and sidecar, including fixes from upstream. (@kvaps in https://github.com/cozystack/cozystack/pull/1209, https://github.com/kubernetes-sigs/container-object-storage-interface/pull/89, and https://github.com/kubernetes-sigs/container-object-storage-interface/pull/90)
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
* Update FerretDB from v1 to v2.4.0.<br>**Breaking change:** before upgrading FerretDB instances, back up and restore the data following the [migration guide](https://docs.ferretdb.io/migration/migrating-from-v1/). (@kvaps in https://github.com/cozystack/cozystack/pull/1206)
|
||||
* Update Talos Linux to v1.10.5. (@kvaps in https://github.com/cozystack/cozystack/pull/1186)
|
||||
* Update LINSTOR to v1.31.2. (@kvaps in https://github.com/cozystack/cozystack/pull/1180)
|
||||
* Update KubeVirt to v1.5.2. (@kvaps in https://github.com/cozystack/cozystack/pull/1183)
|
||||
* Update CDI to v1.62.0. (@kvaps in https://github.com/cozystack/cozystack/pull/1183)
|
||||
* Update Flux Operator to 0.24.0. (@kingdonb in https://github.com/cozystack/cozystack/pull/1167)
|
||||
* Update Kamaji to edge-25.7.1. (@kvaps in https://github.com/cozystack/cozystack/pull/1184)
|
||||
* Update Kube-OVN to v1.13.14. (@kvaps in https://github.com/cozystack/cozystack/pull/1182)
|
||||
* Update Cilium to v1.17.5. (@kvaps in https://github.com/cozystack/cozystack/pull/1181)
|
||||
* Update MariaDB Operator to v0.38.1. (@kvaps in https://github.com/cozystack/cozystack/pull/1188)
|
||||
* Update SeaweedFS to v3.94. (@kvaps in https://github.com/cozystack/cozystack/pull/1194)
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
* [Updated Cozystack Roadmap and Backlog for 2024-2026](https://cozystack.io/docs/roadmap/). (@tym83 and @kvapsova in https://github.com/cozystack/website/pull/249)
|
||||
* [Running Windows VMs](https://cozystack.io/docs/operations/virtualization/windows/). (@kvaps and @NickVolynkin in https://github.com/cozystack/website/pull/246)
|
||||
* [Running MikroTik RouterOS VMs](https://cozystack.io/docs/operations/virtualization/mikrotik/). (@kvaps and @NickVolynkin in https://github.com/cozystack/website/pull/247)
|
||||
* [Public-network Kubernetes Deployment](https://cozystack.io/docs/operations/faq/#public-network-kubernetes-deployment). (@klinch0 and @NickVolynkin in https://github.com/cozystack/website/pull/242)
|
||||
* [How to allocate space on system disk for user storage](https://cozystack.io/docs/operations/faq/#how-to-allocate-space-on-system-disk-for-user-storage). (@klinch0 and @NickVolynkin in https://github.com/cozystack/website/pull/242)
|
||||
* [Resource Management in Cozystack](https://cozystack.io/docs/guides/resource-management/). (@NickVolynkin in https://github.com/cozystack/website/pull/233)
|
||||
* [Key Concepts of Cozystack](https://cozystack.io/docs/guides/concepts/). (@NickVolynkin in https://github.com/cozystack/website/pull/254)
|
||||
* [Cozystack Architecture and Platform Stack](https://cozystack.io/docs/guides/platform-stack/). (@NickVolynkin in https://github.com/cozystack/website/pull/252)
|
||||
* Fixed a parameter in Kubespan: `cluster.discovery.enabled = true`. (@lb0o in https://github.com/cozystack/website/pull/241)
|
||||
* Updated the Linux Foundation trademark text on the Cozystack website. (@krook in https://github.com/cozystack/website/pull/251)
|
||||
* Auto-update the managed applications reference pages. (@NickVolynkin in https://github.com/cozystack/website/pull/243 and https://github.com/cozystack/website/pull/245)
|
||||
|
||||
## Development, Testing, and CI/CD
|
||||
|
||||
* [ci] Improve workflow for contributors submitting PRs from forks. Use Oracle Cloud Infrastructure Registry for non-release PRs, bypassing restrictions preventing pushing to ghcr.io with default GitHub token. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1226)
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.33.0...v0.34.0
|
||||
15
docs/changelogs/v0.34.1.md
Normal file
15
docs/changelogs/v0.34.1.md
Normal file
@@ -0,0 +1,15 @@
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.34.1
|
||||
-->
|
||||
|
||||
> [!WARNING]
|
||||
> A regression was found in this release and fixed in patch [0.34.3](https://github.com/cozystack/cozystack/releases/tag/v0.34.3).
|
||||
> When upgrading Cozystack, it's recommended to skip this version and upgrade directly to [0.34.3](https://github.com/cozystack/cozystack/releases/tag/v0.34.3).
|
||||
|
||||
|
||||
## Fixes
|
||||
|
||||
* [kubernetes] Fix regression in `volumesnapshotclass` installation from https://github.com/cozystack/cozystack/pull/1203. (@kvaps in https://github.com/cozystack/cozystack/pull/1238)
|
||||
* [objectstorage] Fix building objectstorage images. (@kvaps in https://github.com/cozystack/cozystack/commit/a9e9dfca1fadde1bf2b4e100753e0731bbcfe923)
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.34.0...v0.34.1
|
||||
14
docs/changelogs/v0.34.2.md
Normal file
14
docs/changelogs/v0.34.2.md
Normal file
@@ -0,0 +1,14 @@
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.34.2
|
||||
-->
|
||||
|
||||
> [!WARNING]
|
||||
> A regression was found in this release and fixed in patch [0.34.3](https://github.com/cozystack/cozystack/releases/tag/v0.34.3).
|
||||
> When upgrading Cozystack, it's recommended to skip this version and upgrade directly to [0.34.3](https://github.com/cozystack/cozystack/releases/tag/v0.34.3).
|
||||
|
||||
|
||||
## Fixes
|
||||
|
||||
* [objectstorage] Fix recording image in objectstorage. (@kvaps in https://github.com/cozystack/cozystack/commit/4d9a8389d6bc7e86d63dd976ec853b374a91a637)
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.34.1...v0.34.2
|
||||
13
docs/changelogs/v0.34.3.md
Normal file
13
docs/changelogs/v0.34.3.md
Normal file
@@ -0,0 +1,13 @@
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.34.3
|
||||
-->
|
||||
|
||||
## Fixes
|
||||
|
||||
* [tenant] Fix tenant network policy to allow traffic to additional tenant-related services across namespace hierarchies. (@klinch0 in https://github.com/cozystack/cozystack/pull/1232, backported in https://github.com/cozystack/cozystack/pull/1272)
|
||||
* [kubernetes] Add dependency for snapshot CRD and migration to latest version. (@kvaps in https://github.com/cozystack/cozystack/pull/1275, backported in https://github.com/cozystack/cozystack/pull/1279)
|
||||
* [seaweedfs] Add support for whitelisting and exporting via nginx-ingress. Update cosi-driver. (@kvaps in https://github.com/cozystack/cozystack/pull/1277)
|
||||
* [kubevirt] Fix building Kubevirt CCM (@kvaps in 3c7e256906e1dbb0f957dc3a205fa77a147d419d)
|
||||
* [virtual-machine] Fix a regression with field `optional=true`. (@kvaps in https://github.com/cozystack/cozystack/commit/01053f7c3180d1bd045d7c5fb949984c2bdaf19d)
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.34.2...v0.34.3
|
||||
21
docs/changelogs/v0.34.4.md
Normal file
21
docs/changelogs/v0.34.4.md
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.34.4
|
||||
-->
|
||||
|
||||
## Security
|
||||
|
||||
* [keycloak] Store administrative passwords in the management cluster's secrets. (@IvanHunters in https://github.com/cozystack/cozystack/pull/1286)
|
||||
* [keycloak] Update Keycloak client redirect URI to use HTTPS instead of HTTP. Enable `cookie-secure`. (@klinch0 in https://github.com/cozystack/cozystack/pull/1287, backported in https://github.com/cozystack/cozystack/pull/1291)
|
||||
|
||||
|
||||
## Fixes
|
||||
|
||||
* [kubernetes] Resolve problems with pod names exceeding allowed length by shortening the name of volume snapshot CRD from `*-volumesnapshot-crd-for-tenant-k8s` to `*-vsnap-crd`. To apply this change, update each affected tenant Kubernetes cluster after updating Cozystack. (@klinch0 in https://github.com/cozystack/cozystack/pull/1284)
|
||||
* [cozystack-api] Show correct `kind` values of `ApplicationList`. (@kvaps in https://github.com/cozystack/cozystack/pull/1290, backported in https://github.com/cozystack/cozystack/pull/1293)
|
||||
|
||||
|
||||
## Development, Testing, and CI/CD
|
||||
|
||||
* [tests] Add tests for S3 buckets. (@IvanHunters in https://github.com/cozystack/cozystack/pull/1283, backported in https://github.com/cozystack/cozystack/pull/1292)
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.34.3...v0.34.4
|
||||
11
docs/changelogs/v0.34.5.md
Normal file
11
docs/changelogs/v0.34.5.md
Normal file
@@ -0,0 +1,11 @@
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.34.5
|
||||
-->
|
||||
|
||||
|
||||
## Fixes
|
||||
|
||||
* [virtual-machine] Enable using custom `instanceType` values in `virtual-machine` and `vm-instance` by disabling field validation. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1300, backported in https://github.com/cozystack/cozystack/pull/1303)
|
||||
* [kubernetes] Disable VPA for VPA in tenant Kubernetes clusters. Tenant clusters have no need for this feature, and it was not designed to work in a tenant cluster, but was enabled by mistake. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1301, backported in https://github.com/cozystack/cozystack/pull/1305)
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.34.4...v0.34.5
|
||||
9
docs/changelogs/v0.34.6.md
Normal file
9
docs/changelogs/v0.34.6.md
Normal file
@@ -0,0 +1,9 @@
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.34.6
|
||||
-->
|
||||
|
||||
## Fixes
|
||||
|
||||
* [dashboard] Fix filling multiline values in the visual editor. (@kvaps in https://github.com/cozystack/cozystack/commit/56fca9bd75efeca25f9483f6c514b6fec26d5d22 and https://github.com/cozystack/kubeapps/commit/4926bc68fabb0914afab574006643c85a597b371)
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.34.5...v0.34.6
|
||||
11
docs/changelogs/v0.34.7.md
Normal file
11
docs/changelogs/v0.34.7.md
Normal file
@@ -0,0 +1,11 @@
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.34.7
|
||||
-->
|
||||
|
||||
## Fixes
|
||||
|
||||
* [seaweedfs] Disable proxy buffering and proxy request buffering for ingress. (@kvaps in https://github.com/cozystack/cozystack/pull/1330, backported in https://github.com/cozystack/cozystack/commit/96d462e911d4458704b596533d3f10e4b5e80862)
|
||||
* [linstor] Update LINSTOR monitoring configuration to use label `controller_node` instead of `node`. (@kvaps in https://github.com/cozystack/cozystack/pull/1326, backported in https://github.com/cozystack/cozystack/pull/1327)
|
||||
* [kubernetes] Disable VPA for VPA in tenant Kubernetes clusters, patched a fix from v0.34.5. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1318, backported in https://github.com/cozystack/cozystack/pull/1319)
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.34.6...v0.34.7
|
||||
11
docs/changelogs/v0.34.8.md
Normal file
11
docs/changelogs/v0.34.8.md
Normal file
@@ -0,0 +1,11 @@
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.34.8
|
||||
-->
|
||||
|
||||
## Fixes
|
||||
|
||||
* [etcd] Fix `topologySpreadConstraints`. (@klinch0 in https://github.com/cozystack/cozystack/pull/1331, backported in https://github.com/cozystack/cozystack/pull/1332)
|
||||
* [linstor] Update LINSTOR monitoring configuration: switch labels on `linstor-satellite` and `linstor-controller`. (@kvaps in https://github.com/cozystack/cozystack/pull/1335, backported in https://github.com/cozystack/cozystack/pull/1336)
|
||||
* [kamaji] Fix broken migration jobs originating from missing environment variables in the in-tree build. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1338, backported in https://github.com/cozystack/cozystack/pull/1340)
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.34.7...v0.34.8
|
||||
138
docs/changelogs/v0.35.0.md
Normal file
138
docs/changelogs/v0.35.0.md
Normal file
@@ -0,0 +1,138 @@
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.35.0
|
||||
-->
|
||||
|
||||
## Feature Highlights
|
||||
|
||||
### External Application Sources in Cozystack
|
||||
|
||||
Cozystack now supports adding external application packages to the platform's application catalog.
|
||||
Platform administrators can include custom or third-party applications alongside built-in ones, using the Cozystack API.
|
||||
|
||||
Adding an application requires making an application package, similar to the ones included in Cozystack
|
||||
under [`packages/apps`](https://github.com/cozystack/cozystack/tree/main/packages/apps).
|
||||
Using external packages is enabled by a new CustomResourceDefinition (CRD) called `CozystackResourceDefinition` and
|
||||
a corresponding controller (reconciler) that watches for these resources.
|
||||
|
||||
Add your own managed application using the [documentation](https://cozystack.io/docs/applications/external/)
|
||||
and an example at [github.com/cozystack/external-apps-example](https://github.com/cozystack/external-apps-example).
|
||||
|
||||
<!--
|
||||
* [platform] Enable using external application packages by adding a `CozystackResourceDefinition` reconciler. Read the documentation on [adding external applications to Cozystack](https://cozystack.io/docs/applications/external/) to learn more. (@klinch0 in https://github.com/cozystack/cozystack/pull/1313)
|
||||
* [cozystack-api] Provide an API for administrators to define custom managed applications alongside existing managed apps. (@klinch in https://github.com/cozystack/cozystack/pull/1230)
|
||||
-->
|
||||
|
||||
|
||||
### Cozystack API Improvements
|
||||
|
||||
This release brings significant improvements to the OpenAPI specs for all managed applications in Cozystack,
|
||||
including databases, tenant Kubernetes, virtual machines, monitoring, and others.
|
||||
These changes include more precise type definitions for fields that were previously defined only as generic objects,
|
||||
and many fields now have value constraints.
|
||||
Now many possible misconfigurations are detected immediately upon API request, and not later, with a failed deployment.
|
||||
|
||||
The Cozystack API now also displays default values for the application resources.
|
||||
Most other fields now have sane default values when such values are possible.
|
||||
|
||||
All these changes pave the road for the new Cozystack UI, which is currently under development.
|
||||
|
||||
### Hetzner RobotLB Support
|
||||
|
||||
MetalLB, the default load balancer included in Cozystack, is built for bare metal and self-hosted VMs,
|
||||
but is not supported on most cloud providers.
|
||||
For example, Hetzner provides its own RobotLB service, which Cozystack now supports as an optional component.
|
||||
|
||||
Read the updated guide on [deploying Cozystack on Hetzner.com](https://cozystack.io/docs/install/providers/hetzner/)
|
||||
to learn more and deploy your own Cozystack cluster on Hetzner.
|
||||
|
||||
### S3 Service: Dedicated Clusters and Monitoring
|
||||
|
||||
You can now deploy dedicated Cozystack clusters to run the S3 service, powered by SeaweedFS.
|
||||
Thanks to the support for [integration with remote filer endpoints](https://cozystack.io/docs/operations/stretched/seaweedfs-multidc/),
|
||||
you can connect your primary Cozystack cluster to use S3 storage in a dedicated cluster.
|
||||
|
||||
For security, platform administrators can now configure the SeaweedFS application with
|
||||
a list of IP addresses or CIDR ranges that are allowed to access the filer service.
|
||||
|
||||
SeaweedFS has also been integrated into the monitoring stack and now has its own Grafana dashboard.
|
||||
Together, these enhancements help Cozystack users build a more reliable, scalable, and observable S3 service.
|
||||
|
||||
### ClickHouse Keeper
|
||||
|
||||
The ClickHouse application now includes a ClickHouse Keeper service to improve cluster reliability and availability.
|
||||
This component is deployed by default with every ClickHouse cluster.
|
||||
|
||||
Learn more in the [ClickHouse configuration reference](https://cozystack.io/docs/applications/clickhouse/#clickhouse-keeper-parameters).
|
||||
|
||||
## Major Features and Improvements
|
||||
|
||||
* [platform] Enable using external application packages by adding a `CozystackResourceDefinition` reconciler. Read the documentation on [adding external applications to Cozystack](https://cozystack.io/docs/applications/external/) to learn more. (@klinch0 in https://github.com/cozystack/cozystack/pull/1313)
|
||||
* [cozystack-api, apps] Add default values, clear type definitions, value constraints and other improvements to the OpenAPI specs and READMEs by migrating to [cozyvalue-gen](https://github.com/cozystack/cozyvalues-gen). (@kvaps and @NickVolynkin in https://github.com/cozystack/cozystack/pull/1216, https://github.com/cozystack/cozystack/pull/1314, https://github.com/cozystack/cozystack/pull/1316, https://github.com/cozystack/cozystack/pull/1321, and https://github.com/cozystack/cozystack/pull/1333)
|
||||
* [cozystack-api] Show default values from the OpenAPI spec in the application resources. (@kvaps in https://github.com/cozystack/cozystack/pull/1241)
|
||||
* [cozystack-api] Provide an API for administrators to define custom managed applications alongside existing managed apps. (@klinch in https://github.com/cozystack/cozystack/pull/1230)
|
||||
* [robotlb] Introduce the Hetzner RobotLB balancer. (@IvanHunters and @gwynbleidd2106 in https://github.com/cozystack/cozystack/pull/1233)
|
||||
* [platform, robotlb] Autodetect if node ports should be assigned to load balancer services. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1271)
|
||||
* [seaweedfs] Enable [integration with remote filer endpoints](https://cozystack.io/docs/operations/stretched/seaweedfs-multidc/) by adding new `Client` topology. (@kvaps in https://github.com/cozystack/cozystack/pull/1239)
|
||||
* [seaweedfs] Add support for whitelisting and exporting via nginx-ingress. Update cosi-driver. (@kvaps in https://github.com/cozystack/cozystack/pull/1277)
|
||||
* [monitoring, seaweedfs] Add monitoring and Grafana dashboard for SeaweedFS. (@IvanHunters in https://github.com/cozystack/cozystack/pull/1285)
|
||||
* [clickhouse] Add the ClickHouse Keeper component. (@klinch0 in https://github.com/cozystack/cozystack/pull/1298 and https://github.com/cozystack/cozystack/pull/1320)
|
||||
|
||||
## Security
|
||||
|
||||
* [keycloak] Store administrative passwords in the management cluster's secrets. (@IvanHunters in https://github.com/cozystack/cozystack/pull/1286)
|
||||
* [keycloak] Update Keycloak client redirect URI to use HTTPS instead of HTTP. Enable `cookie-secure`. (@klinch0 in https://github.com/cozystack/cozystack/pull/1287)
|
||||
|
||||
## Fixes
|
||||
|
||||
* [platform] Introduce a fixed 2-second delay at the start of reconciliation for system and tenant Helm operations. (@klinch0 in https://github.com/cozystack/cozystack/pull/1343)
|
||||
* [kubernetes] Add dependency for snapshot CRD and migration to the latest version. (@kvaps in https://github.com/cozystack/cozystack/pull/1275)
|
||||
* [kubernetes] Fix regression in `volumesnapshotclass` installation from https://github.com/cozystack/cozystack/pull/1203. (@kvaps in https://github.com/cozystack/cozystack/pull/1238)
|
||||
* [kubernetes] Resolve problems with pod names exceeding allowed length by shortening the name of volume snapshot CRD from `*-volumesnapshot-crd-for-tenant-k8s` to `*-vsnap-crd`. To apply this change, update each affected tenant Kubernetes cluster after updating Cozystack. (@klinch0 in https://github.com/cozystack/cozystack/pull/1284)
|
||||
* [kubernetes] Disable VPA for VPA in tenant Kubernetes clusters. Tenant clusters have no need for this feature, and it was not designed to work in a tenant cluster, but was enabled by mistake. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1301 and https://github.com/cozystack/cozystack/pull/1318)
|
||||
* [kamaji] Fix broken migration jobs originating from missing environment variables in the in-tree build. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1338)
|
||||
* [etcd] Fix the `topologySpreadConstraints` for etcd. (@klinch0 in https://github.com/cozystack/cozystack/pull/1331)
|
||||
* [tenant] Fix tenant network policy to allow traffic to additional tenant-related services across namespace hierarchies. (@klinch0 in https://github.com/cozystack/cozystack/pull/1232)
|
||||
* [tenant, monitoring] Improve the reliability of tenant monitoring by increasing the timeout and number of retries. (@IvanHunters in https://github.com/cozystack/cozystack/pull/1294)
|
||||
* [kubevirt] Fix building KubeVirt CCM image. (@kvaps in https://github.com/cozystack/cozystack/commit/3c7e256906e1dbb0f957dc3a205fa77a147d419d)
|
||||
* [virtual-machine] Fix a regression with `optional=true` field. (@kvaps in https://github.com/cozystack/cozystack/commit/01053f7c3180d1bd045d7c5fb949984c2bdaf19d)
|
||||
* [virtual-machine] Enable using custom `instanceType` values in `virtual-machine` and `vm-instance` by disabling field validation. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1300, backported in https://github.com/cozystack/cozystack/pull/1303)
|
||||
* [cozystack-api] Show correct `kind` values of `ApplicationList`. (@kvaps in https://github.com/cozystack/cozystack/pull/1290)
|
||||
* [cozystack-api] Add missing roles to allow cozystack-controller to read Kubernetes deployments. (@klinch0 in https://github.com/cozystack/cozystack/pull/1342)
|
||||
* [linstor] Update LINSTOR monitoring configuration to use label `controller_node` instead of `node`. (@kvaps in https://github.com/cozystack/cozystack/pull/1326 and https://github.com/cozystack/cozystack/pull/1335)
|
||||
* [seaweedfs] Fix SeaweedFS volume configuration. Increase the volume size limit from 100MB to 30,000MB. (@kvaps in https://github.com/cozystack/cozystack/pull/1328)
|
||||
* [seaweedfs] Disable proxy buffering and proxy request buffering for ingress. (@kvaps in https://github.com/cozystack/cozystack/pull/1330)
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
* Update flux-operator to 0.28.0. (@kingdonb in https://github.com/cozystack/cozystack/pull/1315 and https://github.com/cozystack/cozystack/pull/1344)
|
||||
|
||||
## Documentation
|
||||
|
||||
* [Reimplement Cozystack Roadmap as a GitHub project](https://github.com/orgs/cozystack/projects/1). (@cozystack team)
|
||||
* [SeaweedFS Multi-DC Configuration](https://cozystack.io/docs/operations/stretched/seaweedfs-multidc/). (@kvaps and @NickVolynkin in https://github.com/cozystack/website/pull/272)
|
||||
* [Troubleshooting Kube-OVN](https://cozystack.io/docs/operations/troubleshooting/#kube-ovn-crash). (@kvaps and @NickVolynkin in https://github.com/cozystack/website/pull/273)
|
||||
* [Removing failed nodes from Cozystack cluster](https://cozystack.io/docs/operations/troubleshooting/#remove-a-failed-node-from-the-cluster). (@kvaps and @NickVolynkin in https://github.com/cozystack/website/pull/273)
|
||||
* [Installing Talos with `kexec`](https://cozystack.io/docs/talos/install/kexec/). (@kvaps and @NickVolynkin in https://github.com/cozystack/website/pull/268)
|
||||
* [Rewrite Cozystack tutorial](https://cozystack.io/docs/getting-started/). (@NickVolynkin in https://github.com/cozystack/website/pull/262 and https://github.com/cozystack/website/pull/268)
|
||||
* [How to install Cozystack in Hetzner](https://cozystack.io/docs/install/providers/hetzner/). (@NickVolynkin and @IvanHunters in https://github.com/cozystack/website/pull/280)
|
||||
* [Adding External Applications to Cozystack Catalog](https://cozystack.io/docs/applications/external/). (@klinch0 and @NickVolynkin in https://github.com/cozystack/website/pull/283)
|
||||
* [Creating and Using Named VM Images (Golden Images)](https://cozystack.io/docs/virtualization/vm-image/) (@NickVolynkin and @kvaps in https://github.com/cozystack/website/pull/276)
|
||||
* [Creating Encrypted Storage on LINSTOR](https://cozystack.io/docs/operations/storage/disk-encryption/). (@kvaps and @NickVolynkin in https://github.com/cozystack/website/pull/282)
|
||||
* [Adding and removing components on Cozystack installation using `bundle-enable` and `bundle-disable`](https://cozystack.io/docs/operations/bundles/#how-to-enable-and-disable-bundle-components) (@NickVolynkin in https://github.com/cozystack/website/pull/281)
|
||||
* Restructure Cozystack documentation. Bring [managed Kubernetes](https://cozystack.io/docs/kubernetes/), [managed applications](https://cozystack.io/docs/applications/), [virtualization](https://cozystack.io/docs/virtualization/), and [networking](https://cozystack.io/docs/networking/) guides to the top level. (@NickVolynkin in https://github.com/cozystack/website/pull/266)
|
||||
|
||||
|
||||
## Development, Testing, and CI/CD
|
||||
|
||||
* [tests] Add tests for S3 buckets. (@IvanHunters in https://github.com/cozystack/cozystack/pull/1283)
|
||||
* [tests, ci] Simplify test discovery logic; run two k8s tests as separate jobs; delete Clickhouse application after a successful test. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1236)
|
||||
* [dx] When running `make` commands with `BUILDER` value specified, `PLATFORM` is optional. (@kvaps in https://github.com/cozystack/cozystack/pull/1288)
|
||||
* [tests] Fix resource specification in virtual machine tests. (@IvanHunters in https://github.com/cozystack/cozystack/pull/1308)
|
||||
* [tests] Increase available space for e2e tests. (@kvaps in https://github.com/cozystack/cozystack/commit/168a24ffdf1202b3bf2e7d2b5ef54b72b7403baf)
|
||||
* [tests, ci] Continue application tests after one of them fails. (@NickVolynkin in https://github.com/cozystack/cozystack/commit/634b77edad6c32c101f3e5daea6a5ffc0c83d904)
|
||||
* [ci] Use a subdomain of aenix.org for Nexus service in CI. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1322)
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.34.0...v0.35.0
|
||||
10
docs/changelogs/v0.35.1.md
Normal file
10
docs/changelogs/v0.35.1.md
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.35.1
|
||||
-->
|
||||
|
||||
## Fixes
|
||||
|
||||
* [cozy-lib] Fix malformed retrieval of `cozyConfig` in the cozy-lib template. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1348)
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.35.0...v0.35.1
|
||||
22
docs/changelogs/v0.35.2.md
Normal file
22
docs/changelogs/v0.35.2.md
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.35.2
|
||||
-->
|
||||
|
||||
## Features and Improvements
|
||||
|
||||
* [talos] Add LLDPD (`ghcr.io/siderolabs/lldpd`) as a built-in system extension, enabling LLDP-based neighbor discovery out of the box. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1351 and https://github.com/cozystack/cozystack/pull/1360)
|
||||
|
||||
## Fixes
|
||||
|
||||
* [cozystack-api] Sanitize the OpenAPI v2 schema. (@kvaps in https://github.com/cozystack/cozystack/pull/1353)
|
||||
* [seaweedfs] Fix a problem where S3 gateway would be moved to an external pod, resulting in authentication failure. (@kvaps in https://github.com/cozystack/cozystack/pull/1361)
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
* Update LINSTOR to v1.31.3. (@kvaps in https://github.com/cozystack/cozystack/pull/1358)
|
||||
* Update SeaweedFS to v3.96. (@kvaps in https://github.com/cozystack/cozystack/pull/1361)
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.35.1...v0.35.2
|
||||
3
go.mod
3
go.mod
@@ -59,6 +59,7 @@ require (
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||
github.com/imdario/mergo v0.3.6 // indirect
|
||||
@@ -66,9 +67,11 @@ require (
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/moby/spdystream v0.4.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||
|
||||
6
go.sum
6
go.sum
@@ -2,6 +2,8 @@ github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cq
|
||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||
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-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -115,6 +117,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
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/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=
|
||||
@@ -122,6 +126,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
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/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=
|
||||
|
||||
@@ -48,7 +48,7 @@ kubectl get ns --no-headers | awk '$2 != "Active"' |
|
||||
|
||||
echo "Collecting helmreleases..."
|
||||
kubectl get hr -A > $REPORT_DIR/kubernetes/helmreleases.txt 2>&1
|
||||
kubectl get hr -A | awk '$4 != "True"' | \
|
||||
kubectl get hr -A --no-headers | awk '$4 != "True"' | \
|
||||
while read NAMESPACE NAME _; do
|
||||
DIR=$REPORT_DIR/kubernetes/helmreleases/$NAMESPACE/$NAME
|
||||
mkdir -p $DIR
|
||||
@@ -105,7 +105,7 @@ kubectl get svc -A --no-headers | awk '$4 == "<pending>"' |
|
||||
|
||||
echo "Collecting pvcs..."
|
||||
kubectl get pvc -A > $REPORT_DIR/kubernetes/pvcs.txt 2>&1
|
||||
kubectl get pvc -A | awk '$3 != "Bound"' |
|
||||
kubectl get pvc -A --no-headers | awk '$3 != "Bound"' |
|
||||
while read NAMESPACE NAME _; do
|
||||
DIR=$REPORT_DIR/kubernetes/pvc/$NAMESPACE/$NAME
|
||||
mkdir -p $DIR
|
||||
|
||||
@@ -24,7 +24,7 @@ run_one() {
|
||||
|
||||
echo "╭ » Run test: $title"
|
||||
START=$(date +%s)
|
||||
skip_next="+ $fn" # первую строку трассировки с именем функции пропустим
|
||||
skip_next="+ $fn"
|
||||
|
||||
{
|
||||
(
|
||||
@@ -83,11 +83,11 @@ awk '
|
||||
}
|
||||
printf("### %s\n", title)
|
||||
printf("%s() {\n", fname)
|
||||
print " set -e" # ошибка → падение теста
|
||||
print " set -e"
|
||||
next
|
||||
}
|
||||
/^}$/ {
|
||||
print " return 0" # если автор не сделал exit 1 — тест ОК
|
||||
print " return 0"
|
||||
print "}"
|
||||
next
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ spec:
|
||||
s3AccessKey: oobaiRus9pah8PhohL1ThaeTa4UVa7gu
|
||||
s3SecretKey: ju3eum4dekeich9ahM1te8waeGai0oog
|
||||
resticPassword: ChaXoveekoh6eigh4siesheeda2quai0
|
||||
clickhouseKeeper:
|
||||
enabled: true
|
||||
resourcesPreset: "micro"
|
||||
size: "1Gi"
|
||||
resources: {}
|
||||
resourcesPreset: "nano"
|
||||
EOF
|
||||
|
||||
@@ -57,9 +57,6 @@ spec:
|
||||
instanceType: u1.medium
|
||||
maxReplicas: 10
|
||||
minReplicas: 0
|
||||
resources:
|
||||
cpu: ""
|
||||
memory: ""
|
||||
roles:
|
||||
- ingress-nginx
|
||||
storageClass: replicated
|
||||
@@ -82,22 +79,73 @@ EOF
|
||||
|
||||
# Wait for the machine deployment to scale to 2 replicas (timeout after 1 minute)
|
||||
kubectl wait machinedeployment kubernetes-${test_name}-md0 -n tenant-test --timeout=1m --for=jsonpath='{.status.replicas}'=2
|
||||
|
||||
# Get the admin kubeconfig and save it to a file
|
||||
kubectl get secret kubernetes-${test_name}-admin-kubeconfig -ojsonpath='{.data.super-admin\.conf}' -n tenant-test | base64 -d > tenantkubeconfig
|
||||
|
||||
# Update the kubeconfig to use localhost for the API server
|
||||
yq -i ".clusters[0].cluster.server = \"https://localhost:${port}\"" tenantkubeconfig
|
||||
|
||||
# Set up port forwarding to the Kubernetes API server for a 40 second timeout
|
||||
bash -c 'timeout 40s kubectl port-forward service/kubernetes-'"${test_name}"' -n tenant-test '"${port}"':6443 > /dev/null 2>&1 &'
|
||||
|
||||
# Set up port forwarding to the Kubernetes API server for a 200 second timeout
|
||||
bash -c 'timeout 200s kubectl port-forward service/kubernetes-'"${test_name}"' -n tenant-test '"${port}"':6443 > /dev/null 2>&1 &'
|
||||
# Verify the Kubernetes version matches what we expect (retry for up to 20 seconds)
|
||||
timeout 20 sh -ec 'until kubectl --kubeconfig tenantkubeconfig version 2>/dev/null | grep -Fq "Server Version: ${k8s_version}"; do sleep 5; done'
|
||||
|
||||
# Wait for the nodes to be ready (timeout after 2 minutes)
|
||||
timeout 2m bash -c '
|
||||
until [ "$(kubectl --kubeconfig tenantkubeconfig get nodes -o jsonpath="{.items[*].metadata.name}" | wc -w)" -eq 2 ]; do
|
||||
sleep 3
|
||||
done
|
||||
'
|
||||
# Verify the nodes are ready
|
||||
kubectl --kubeconfig tenantkubeconfig wait node --all --timeout=2m --for=condition=Ready
|
||||
kubectl --kubeconfig tenantkubeconfig get nodes -o wide
|
||||
|
||||
# Verify the kubelet version matches what we expect
|
||||
versions=$(kubectl --kubeconfig tenantkubeconfig get nodes -o jsonpath='{.items[*].status.nodeInfo.kubeletVersion}')
|
||||
node_ok=true
|
||||
|
||||
if [[ "$k8s_version" == v1.32* ]]; then
|
||||
echo "⚠️ TODO: Temporary stub — allowing nodes with v1.33 while k8s_version is v1.32"
|
||||
fi
|
||||
|
||||
for v in $versions; do
|
||||
case "$k8s_version" in
|
||||
v1.32|v1.32.*)
|
||||
case "$v" in
|
||||
v1.32 | v1.32.* | v1.32-* | v1.33 | v1.33.* | v1.33-*)
|
||||
;;
|
||||
*)
|
||||
node_ok=false
|
||||
break
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
case "$v" in
|
||||
"${k8s_version}" | "${k8s_version}".* | "${k8s_version}"-*)
|
||||
;;
|
||||
*)
|
||||
node_ok=false
|
||||
break
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! $node_ok; then
|
||||
echo "Kubelet versions did not match expected ${k8s_version}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Wait for all machine deployment replicas to be ready (timeout after 10 minutes)
|
||||
kubectl wait machinedeployment kubernetes-${test_name}-md0 -n tenant-test --timeout=10m --for=jsonpath='{.status.v1beta2.readyReplicas}'=2
|
||||
|
||||
for component in cilium coredns csi ingress-nginx vsnap-crd; do
|
||||
kubectl wait hr kubernetes-${test_name}-${component} -n tenant-test --timeout=1m --for=condition=ready
|
||||
done
|
||||
|
||||
# Clean up by deleting the Kubernetes resource
|
||||
kubectl -n tenant-test delete kuberneteses.apps.cozystack.io $test_name
|
||||
|
||||
|
||||
@@ -20,9 +20,7 @@ spec:
|
||||
storage: 5Gi
|
||||
storageClass: replicated
|
||||
gpus: []
|
||||
resources:
|
||||
cpu: ""
|
||||
memory: ""
|
||||
resources: {}
|
||||
sshKeys:
|
||||
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPht0dPk5qQ+54g1hSX7A6AUxXJW5T6n/3d7Ga2F8gTF
|
||||
test@test
|
||||
|
||||
@@ -18,8 +18,8 @@ spec:
|
||||
EOF
|
||||
sleep 5
|
||||
kubectl -n tenant-test wait hr vm-disk-$name --timeout=5s --for=condition=ready
|
||||
kubectl -n tenant-test wait dv vm-disk-$name --timeout=150s --for=condition=ready
|
||||
kubectl -n tenant-test wait pvc vm-disk-$name --timeout=100s --for=jsonpath='{.status.phase}'=Bound
|
||||
kubectl -n tenant-test wait dv vm-disk-$name --timeout=250s --for=condition=ready
|
||||
kubectl -n tenant-test wait pvc vm-disk-$name --timeout=200s --for=jsonpath='{.status.phase}'=Bound
|
||||
}
|
||||
|
||||
@test "Create a VM Instance" {
|
||||
@@ -42,9 +42,6 @@ spec:
|
||||
disks:
|
||||
- name: $diskName
|
||||
gpus: []
|
||||
resources:
|
||||
cpu: ""
|
||||
memory: ""
|
||||
sshKeys:
|
||||
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPht0dPk5qQ+54g1hSX7A6AUxXJW5T6n/3d7Ga2F8gTF
|
||||
test@test
|
||||
|
||||
@@ -123,16 +123,24 @@ EOF
|
||||
|
||||
@test "Configure Tenant and wait for applications" {
|
||||
# Patch root tenant and wait for its releases
|
||||
|
||||
kubectl patch tenants/root -n tenant-root --type merge -p '{"spec":{"host":"example.org","ingress":true,"monitoring":true,"etcd":true,"isolated":true, "seaweedfs": true}}'
|
||||
|
||||
timeout 60 sh -ec 'until kubectl get hr -n tenant-root etcd ingress monitoring seaweedfs tenant-root >/dev/null 2>&1; do sleep 1; done'
|
||||
kubectl wait hr/etcd hr/ingress hr/tenant-root hr/seaweedfs -n tenant-root --timeout=4m --for=condition=ready
|
||||
|
||||
# TODO: Workaround ingress unvailability issue
|
||||
if ! kubectl wait hr/monitoring -n tenant-root --timeout=2m --for=condition=ready; then
|
||||
flux reconcile hr monitoring -n tenant-root --force
|
||||
kubectl wait hr/monitoring -n tenant-root --timeout=2m --for=condition=ready
|
||||
fi
|
||||
|
||||
if ! kubectl wait hr/seaweedfs-system -n tenant-root --timeout=2m --for=condition=ready; then
|
||||
flux reconcile hr seaweedfs-system -n tenant-root --force
|
||||
kubectl wait hr/seaweedfs-system -n tenant-root --timeout=2m --for=condition=ready
|
||||
fi
|
||||
|
||||
|
||||
# Expose Cozystack services through ingress
|
||||
kubectl patch configmap/cozystack -n cozy-system --type merge -p '{"data":{"expose-services":"api,dashboard,cdi-uploadproxy,vm-exportproxy,keycloak"}}'
|
||||
|
||||
@@ -144,7 +152,7 @@ EOF
|
||||
kubectl wait sts/etcd -n tenant-root --for=jsonpath='{.status.readyReplicas}'=3 --timeout=5m
|
||||
|
||||
# VictoriaMetrics components
|
||||
kubectl wait vmalert/vmalert-shortterm vmalertmanager/alertmanager -n tenant-root --for=jsonpath='{.status.updateStatus}'=operational --timeout=5m
|
||||
kubectl wait vmalert/vmalert-shortterm vmalertmanager/alertmanager -n tenant-root --for=jsonpath='{.status.updateStatus}'=operational --timeout=15m
|
||||
kubectl wait vlogs/generic -n tenant-root --for=jsonpath='{.status.updateStatus}'=operational --timeout=5m
|
||||
kubectl wait vmcluster/shortterm vmcluster/longterm -n tenant-root --for=jsonpath='{.status.clusterStatus}'=operational --timeout=5m
|
||||
|
||||
@@ -181,9 +189,22 @@ spec:
|
||||
ingress: false
|
||||
isolated: true
|
||||
monitoring: false
|
||||
resourceQuotas: {}
|
||||
resourceQuotas:
|
||||
cpu: "60"
|
||||
memory: "128Gi"
|
||||
storage: "100Gi"
|
||||
seaweedfs: false
|
||||
EOF
|
||||
kubectl wait hr/tenant-test -n tenant-root --timeout=1m --for=condition=ready
|
||||
kubectl wait namespace tenant-test --timeout=20s --for=jsonpath='{.status.phase}'=Active
|
||||
# Wait for ResourceQuota to appear and assert values
|
||||
timeout 60 sh -ec 'until [ "$(kubectl get quota -n tenant-test --no-headers 2>/dev/null | wc -l)" -ge 1 ]; do sleep 1; done'
|
||||
kubectl get quota -n tenant-test \
|
||||
-o jsonpath='{range .items[*]}{.spec.hard.requests\.memory}{" "}{.spec.hard.requests\.storage}{"\n"}{end}' \
|
||||
| grep -qx '137438953472 100Gi'
|
||||
|
||||
# Assert LimitRange defaults for containers
|
||||
kubectl get limitrange -n tenant-test \
|
||||
-o jsonpath='{range .items[*].spec.limits[*]}{.default.cpu}{" "}{.default.memory}{" "}{.defaultRequest.cpu}{" "}{.defaultRequest.memory}{"\n"}{end}' \
|
||||
| grep -qx '250m 128Mi 25m 128Mi'
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ EOF
|
||||
for i in 1 2 3; do
|
||||
cp nocloud-amd64.raw srv${i}/system.img
|
||||
qemu-img resize srv${i}/system.img 50G
|
||||
qemu-img create srv${i}/data.img 100G
|
||||
qemu-img create srv${i}/data.img 200G
|
||||
done
|
||||
}
|
||||
|
||||
@@ -132,29 +132,30 @@ machine:
|
||||
- usermode_helper=disabled
|
||||
- name: zfs
|
||||
- name: spl
|
||||
- name: lldpd
|
||||
registries:
|
||||
mirrors:
|
||||
docker.io:
|
||||
endpoints:
|
||||
- https://dockerio.nexus.lllamnyp.su
|
||||
- https://dockerio.nexus.aenix.org
|
||||
cr.fluentbit.io:
|
||||
endpoints:
|
||||
- https://fluentbit.nexus.lllamnyp.su
|
||||
- https://fluentbit.nexus.aenix.org
|
||||
docker-registry3.mariadb.com:
|
||||
endpoints:
|
||||
- https://mariadb.nexus.lllamnyp.su
|
||||
- https://mariadb.nexus.aenix.org
|
||||
gcr.io:
|
||||
endpoints:
|
||||
- https://gcr.nexus.lllamnyp.su
|
||||
- https://gcr.nexus.aenix.org
|
||||
ghcr.io:
|
||||
endpoints:
|
||||
- https://ghcr.nexus.lllamnyp.su
|
||||
- https://ghcr.nexus.aenix.org
|
||||
quay.io:
|
||||
endpoints:
|
||||
- https://quay.nexus.lllamnyp.su
|
||||
- https://quay.nexus.aenix.org
|
||||
registry.k8s.io:
|
||||
endpoints:
|
||||
- https://k8s.nexus.lllamnyp.su
|
||||
- https://k8s.nexus.aenix.org
|
||||
files:
|
||||
- content: |
|
||||
[plugins]
|
||||
|
||||
20
hack/e2e-test-openapi.bats
Normal file
20
hack/e2e-test-openapi.bats
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bats
|
||||
# -----------------------------------------------------------------------------
|
||||
# Test OpenAPI endpoints in a Kubernetes cluster
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@test "Test OpenAPI v2 endpoint" {
|
||||
kubectl get -v7 --raw '/openapi/v2?timeout=32s' > /dev/null
|
||||
}
|
||||
|
||||
@test "Test OpenAPI v3 endpoint" {
|
||||
kubectl get -v7 --raw '/openapi/v3/apis/apps.cozystack.io/v1alpha1' > /dev/null
|
||||
}
|
||||
|
||||
@test "Test OpenAPI v2 endpoint (protobuf)" {
|
||||
(
|
||||
kubectl proxy --port=21234 & sleep 0.5
|
||||
trap "kill $!" EXIT
|
||||
curl -sS --fail 'http://localhost:21234/openapi/v2?timeout=32s' -H 'Accept: application/com.github.proto-openapi.spec.v2@v1.0+protobuf' > /dev/null
|
||||
)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
file=versions_map
|
||||
|
||||
charts=$(find . -mindepth 2 -maxdepth 2 -name Chart.yaml | awk 'sub("/Chart.yaml", "")')
|
||||
|
||||
new_map=$(
|
||||
for chart in $charts; do
|
||||
awk '/^name:/ {chart=$2} /^version:/ {version=$2} END{printf "%s %s %s\n", chart, version, "HEAD"}' "$chart/Chart.yaml"
|
||||
done
|
||||
)
|
||||
|
||||
if [ ! -f "$file" ] || [ ! -s "$file" ]; then
|
||||
echo "$new_map" > "$file"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
miss_map=$(mktemp)
|
||||
trap 'rm -f "$miss_map"' EXIT
|
||||
echo -n "$new_map" | awk 'NR==FNR { nm[$1 " " $2] = $3; next } { if (!($1 " " $2 in nm)) print $1, $2, $3}' - "$file" > $miss_map
|
||||
|
||||
# search accross all tags sorted by version
|
||||
search_commits=$(git ls-remote --tags origin | awk -F/ '$3 ~ /v[0-9]+.[0-9]+.[0-9]+/ {print}' | sort -k2,2 -rV | awk '{print $1}')
|
||||
|
||||
resolved_miss_map=$(
|
||||
while read -r chart version commit; do
|
||||
# if version is found in HEAD, it's HEAD
|
||||
if [ "$(awk '$1 == "version:" {print $2}' ./${chart}/Chart.yaml)" = "${version}" ]; then
|
||||
echo "$chart $version HEAD"
|
||||
continue
|
||||
fi
|
||||
|
||||
# if commit is not HEAD, check if it's valid
|
||||
if [ "$commit" != "HEAD" ]; then
|
||||
if [ "$(git show "${commit}:./${chart}/Chart.yaml" | awk '$1 == "version:" {print $2}')" != "${version}" ]; then
|
||||
echo "Commit $commit for $chart $version is not valid" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
commit=$(git rev-parse --short "$commit")
|
||||
echo "$chart $version $commit"
|
||||
continue
|
||||
fi
|
||||
|
||||
# if commit is HEAD, but version is not found in HEAD, check all tags
|
||||
found_tag=""
|
||||
for tag in $search_commits; do
|
||||
if [ "$(git show "${tag}:./${chart}/Chart.yaml" | awk '$1 == "version:" {print $2}')" = "${version}" ]; then
|
||||
found_tag=$(git rev-parse --short "${tag}")
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$found_tag" ]; then
|
||||
echo "Can't find $chart $version in any version tag, removing it" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "$chart $version $found_tag"
|
||||
done < $miss_map
|
||||
)
|
||||
|
||||
printf "%s\n" "$new_map" "$resolved_miss_map" | sort -k1,1 -k2,2 -V | awk '$1' > "$file"
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
usage() {
|
||||
printf "%s\n" "Usage:" >&2 ;
|
||||
printf -- "%s\n" '---' >&2 ;
|
||||
printf "%s %s\n" "$0" "INPUT_DIR OUTPUT_DIR TMP_DIR [DEPENDENCY_DIR]" >&2 ;
|
||||
printf -- "%s\n" '---' >&2 ;
|
||||
printf "%s\n" "Takes a helm repository from INPUT_DIR, with an optional library repository in" >&2 ;
|
||||
printf "%s\n" "DEPENDENCY_DIR, prepares a view of the git archive at select points in history" >&2 ;
|
||||
printf "%s\n" "in TMP_DIR and packages helm charts, outputting the tarballs to OUTPUT_DIR" >&2 ;
|
||||
}
|
||||
|
||||
if [ "x$(basename $PWD)" != "xpackages" ]
|
||||
then
|
||||
echo "Error: This script must run from the ./packages/ directory" >&2
|
||||
echo >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "x$#" != "x3" ] && [ "x$#" != "x4" ]
|
||||
then
|
||||
echo "Error: This script takes 3 or 4 arguments" >&2
|
||||
echo "Got $# arguments:" "$@" >&2
|
||||
echo >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
input_dir=$1
|
||||
output_dir=$2
|
||||
tmp_dir=$3
|
||||
|
||||
if [ "x$#" = "x4" ]
|
||||
then
|
||||
dependency_dir=$4
|
||||
fi
|
||||
|
||||
rm -rf "${output_dir:?}"
|
||||
mkdir -p "${output_dir}"
|
||||
while read package _ commit
|
||||
do
|
||||
# this lets devs build the packages from a dirty repo for quick local testing
|
||||
if [ "x$commit" = "xHEAD" ]
|
||||
then
|
||||
helm package "${input_dir}/${package}" -d "${output_dir}"
|
||||
continue
|
||||
fi
|
||||
git archive --format tar "${commit}" "${input_dir}/${package}" | tar -xf- -C "${tmp_dir}/"
|
||||
|
||||
# the library chart is not present in older commits and git archive doesn't fail gracefully if the path is not found
|
||||
if [ "x${dependency_dir}" != "x" ] && git ls-tree --name-only "${commit}" "${dependency_dir}" | grep -qx "${dependency_dir}"
|
||||
then
|
||||
git archive --format tar "${commit}" "${dependency_dir}" | tar -xf- -C "${tmp_dir}/"
|
||||
fi
|
||||
helm package "${tmp_dir}/${input_dir}/${package}" -d "${output_dir}"
|
||||
rm -rf "${tmp_dir:?}/${input_dir:?}/${package:?}"
|
||||
if [ "x${dependency_dir}" != "x" ]
|
||||
then
|
||||
rm -rf "${tmp_dir:?}/${dependency_dir:?}"
|
||||
fi
|
||||
done < "${input_dir}/versions_map"
|
||||
helm repo index "${output_dir}"
|
||||
@@ -53,4 +53,6 @@ kube::codegen::gen_openapi \
|
||||
"${SCRIPT_ROOT}/pkg/apis"
|
||||
|
||||
$CONTROLLER_GEN object:headerFile="hack/boilerplate.go.txt" paths="./api/..."
|
||||
$CONTROLLER_GEN rbac:roleName=manager-role crd paths="./api/..." output:crd:artifacts:config=packages/system/cozystack-controller/templates/crds
|
||||
$CONTROLLER_GEN rbac:roleName=manager-role crd paths="./api/..." output:crd:artifacts:config=packages/system/cozystack-controller/crds
|
||||
mv packages/system/cozystack-controller/crds/cozystack.io_cozystackresourcedefinitions.yaml \
|
||||
packages/system/cozystack-resource-definition-crd/definition/cozystack.io_cozystackresourcedefinitions.yaml
|
||||
|
||||
139
hack/update-crd.sh
Executable file
139
hack/update-crd.sh
Executable file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Requirements: yq (v4), jq, base64
|
||||
need() { command -v "$1" >/dev/null 2>&1 || { echo "need $1"; exit 1; }; }
|
||||
need yq; need jq; need base64
|
||||
|
||||
CHART_YAML="${CHART_YAML:-Chart.yaml}"
|
||||
VALUES_YAML="${VALUES_YAML:-values.yaml}"
|
||||
SCHEMA_JSON="${SCHEMA_JSON:-values.schema.json}"
|
||||
CRD_DIR="../../system/cozystack-resource-definitions/cozyrds"
|
||||
|
||||
[[ -f "$CHART_YAML" ]] || { echo "No $CHART_YAML found"; exit 1; }
|
||||
[[ -f "$SCHEMA_JSON" ]] || { echo "No $SCHEMA_JSON found"; exit 1; }
|
||||
|
||||
# Read basics from Chart.yaml
|
||||
NAME="$(yq -r '.name // ""' "$CHART_YAML")"
|
||||
DESC="$(yq -r '.description // ""' "$CHART_YAML")"
|
||||
ICON_PATH_RAW="$(yq -r '.icon // ""' "$CHART_YAML")"
|
||||
|
||||
if [[ -z "$NAME" ]]; then
|
||||
echo "Chart.yaml: .name is empty"; exit 1
|
||||
fi
|
||||
|
||||
# Resolve icon path
|
||||
# Accepts:
|
||||
# /logos/foo.svg -> ./logos/foo.svg
|
||||
# logos/foo.svg -> logos/foo.svg
|
||||
# ./logos/foo.svg -> ./logos/foo.svg
|
||||
# Fallback: ./logos/${NAME}.svg
|
||||
resolve_icon_path() {
|
||||
local p="$1"
|
||||
if [[ -z "$p" || "$p" == "null" ]]; then
|
||||
echo "./logos/${NAME}.svg"; return
|
||||
fi
|
||||
if [[ "$p" == /* ]]; then
|
||||
echo ".${p}"
|
||||
else
|
||||
echo "$p"
|
||||
fi
|
||||
}
|
||||
ICON_PATH="$(resolve_icon_path "$ICON_PATH_RAW")"
|
||||
|
||||
if [[ ! -f "$ICON_PATH" ]]; then
|
||||
# try fallback
|
||||
ALT="./logos/${NAME}.svg"
|
||||
if [[ -f "$ALT" ]]; then
|
||||
ICON_PATH="$ALT"
|
||||
else
|
||||
echo "Icon not found: $ICON_PATH"; exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Base64 (portable: no -w / -b options)
|
||||
ICON_B64="$(base64 < "$ICON_PATH" | tr -d '\n' | tr -d '\r')"
|
||||
|
||||
# Decide which HelmRepository name to use based on path
|
||||
# .../apps/... -> cozystack-apps
|
||||
# .../extra/... -> cozystack-extra
|
||||
# default: cozystack-apps
|
||||
SOURCE_NAME="cozystack-apps"
|
||||
case "$PWD" in
|
||||
*"/apps/"*) SOURCE_NAME="cozystack-apps" ;;
|
||||
*"/extra/"*) SOURCE_NAME="cozystack-extra" ;;
|
||||
esac
|
||||
|
||||
# If file doesn't exist, create a minimal skeleton
|
||||
OUT="${OUT:-$CRD_DIR/$NAME.yaml}"
|
||||
if [[ ! -f "$OUT" ]]; then
|
||||
cat >"$OUT" <<EOF
|
||||
apiVersion: cozystack.io/v1alpha1
|
||||
kind: CozystackResourceDefinition
|
||||
metadata:
|
||||
name: ${NAME}
|
||||
spec: {}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Export vars for yq env()
|
||||
export RES_NAME="$NAME"
|
||||
export PREFIX="$NAME-"
|
||||
if [ "$SOURCE_NAME" == "cozystack-extra" ]; then
|
||||
export PREFIX=""
|
||||
fi
|
||||
export DESCRIPTION="$DESC"
|
||||
export ICON_B64="$ICON_B64"
|
||||
export SOURCE_NAME="$SOURCE_NAME"
|
||||
export SCHEMA_JSON_MIN="$(jq -c . "$SCHEMA_JSON")"
|
||||
|
||||
# Generate keysOrder from values.yaml
|
||||
export KEYS_ORDER="$(
|
||||
yq -o=json '.' "$VALUES_YAML" | jq -c '
|
||||
def get_paths_recursive(obj; path):
|
||||
obj | to_entries | map(
|
||||
.key as $key |
|
||||
.value as $value |
|
||||
if $value | type == "object" then
|
||||
[path + [$key]] + get_paths_recursive($value; path + [$key])
|
||||
else
|
||||
[path + [$key]]
|
||||
end
|
||||
) | flatten(1)
|
||||
;
|
||||
(
|
||||
[ ["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata","name"] ]
|
||||
)
|
||||
+
|
||||
(
|
||||
get_paths_recursive(.; []) # get all paths in order
|
||||
| map(select(length>0)) # drop root
|
||||
| map(map(select(type != "number"))) # drop array indices
|
||||
| map(["spec"] + .) # prepend "spec"
|
||||
)
|
||||
'
|
||||
)"
|
||||
|
||||
# Update only necessary fields in-place
|
||||
# - openAPISchema is loaded from file as a multi-line string (block scalar)
|
||||
# - labels ensure cozystack.io/ui: "true"
|
||||
# - prefix = "<name>-"
|
||||
# - sourceRef derived from directory (apps|extra)
|
||||
yq -i '
|
||||
.apiVersion = (.apiVersion // "cozystack.io/v1alpha1") |
|
||||
.kind = (.kind // "CozystackResourceDefinition") |
|
||||
.metadata.name = strenv(RES_NAME) |
|
||||
.spec.application.openAPISchema = strenv(SCHEMA_JSON_MIN) |
|
||||
(.spec.application.openAPISchema style="literal") |
|
||||
.spec.release.prefix = (strenv(PREFIX)) |
|
||||
.spec.release.labels."cozystack.io/ui" = "true" |
|
||||
.spec.release.chart.name = strenv(RES_NAME) |
|
||||
.spec.release.chart.sourceRef.kind = "HelmRepository" |
|
||||
.spec.release.chart.sourceRef.name = strenv(SOURCE_NAME) |
|
||||
.spec.release.chart.sourceRef.namespace = "cozy-public" |
|
||||
.spec.dashboard.description = strenv(DESCRIPTION) |
|
||||
.spec.dashboard.icon = strenv(ICON_B64) |
|
||||
.spec.dashboard.keysOrder = env(KEYS_ORDER)
|
||||
' "$OUT"
|
||||
|
||||
echo "Updated $OUT"
|
||||
274
internal/controller/cozystackresource_controller.go
Normal file
274
internal/controller/cozystackresource_controller.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cozystack/cozystack/internal/controller/dashboard"
|
||||
"github.com/cozystack/cozystack/internal/shared/crdmem"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
type CozystackResourceDefinitionReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
|
||||
Debounce time.Duration
|
||||
|
||||
mu sync.Mutex
|
||||
lastEvent time.Time
|
||||
lastHandled time.Time
|
||||
|
||||
mem *crdmem.Memory
|
||||
|
||||
// Track static resources initialization
|
||||
staticResourcesInitialized bool
|
||||
staticResourcesMutex sync.Mutex
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
crd := &cozyv1alpha1.CozystackResourceDefinition{}
|
||||
err := r.Get(ctx, types.NamespacedName{Name: req.Name}, crd)
|
||||
if err == nil {
|
||||
if r.mem != nil {
|
||||
r.mem.Upsert(crd)
|
||||
}
|
||||
|
||||
mgr := dashboard.NewManager(
|
||||
r.Client,
|
||||
r.Scheme,
|
||||
dashboard.WithCRDListFunc(func(c context.Context) ([]cozyv1alpha1.CozystackResourceDefinition, error) {
|
||||
if r.mem != nil {
|
||||
return r.mem.ListFromCacheOrAPI(c, r.Client)
|
||||
}
|
||||
var list cozyv1alpha1.CozystackResourceDefinitionList
|
||||
if err := r.Client.List(c, &list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list.Items, nil
|
||||
}),
|
||||
)
|
||||
|
||||
if res, derr := mgr.EnsureForCRD(ctx, crd); derr != nil || res.Requeue || res.RequeueAfter > 0 {
|
||||
return res, derr
|
||||
}
|
||||
|
||||
// After processing CRD, perform cleanup of orphaned resources
|
||||
// This should be done after cache warming to ensure all current resources are known
|
||||
if cleanupErr := mgr.CleanupOrphanedResources(ctx); cleanupErr != nil {
|
||||
logger.Error(cleanupErr, "Failed to cleanup orphaned dashboard resources")
|
||||
// Don't fail the reconciliation, just log the error
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.lastEvent = time.Now()
|
||||
r.mu.Unlock()
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Handle error cases (err is guaranteed to be non-nil here)
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
// If resource is not found, clean up from memory
|
||||
if r.mem != nil {
|
||||
r.mem.Delete(req.Name)
|
||||
}
|
||||
if req.Namespace == "cozy-system" && req.Name == "cozystack-api" {
|
||||
return r.debouncedRestart(ctx, logger)
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// initializeStaticResourcesOnce ensures static resources are created only once
|
||||
func (r *CozystackResourceDefinitionReconciler) initializeStaticResourcesOnce(ctx context.Context) error {
|
||||
r.staticResourcesMutex.Lock()
|
||||
defer r.staticResourcesMutex.Unlock()
|
||||
|
||||
if r.staticResourcesInitialized {
|
||||
return nil // Already initialized
|
||||
}
|
||||
|
||||
// Create dashboard manager and initialize static resources
|
||||
mgr := dashboard.NewManager(
|
||||
r.Client,
|
||||
r.Scheme,
|
||||
dashboard.WithCRDListFunc(func(c context.Context) ([]cozyv1alpha1.CozystackResourceDefinition, error) {
|
||||
if r.mem != nil {
|
||||
return r.mem.ListFromCacheOrAPI(c, r.Client)
|
||||
}
|
||||
var list cozyv1alpha1.CozystackResourceDefinitionList
|
||||
if err := r.Client.List(c, &list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list.Items, nil
|
||||
}),
|
||||
)
|
||||
|
||||
if err := mgr.InitializeStaticResources(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.staticResourcesInitialized = true
|
||||
log.FromContext(ctx).Info("Static dashboard resources initialized successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
if r.Debounce == 0 {
|
||||
r.Debounce = 5 * time.Second
|
||||
}
|
||||
if r.mem == nil {
|
||||
r.mem = crdmem.Global()
|
||||
}
|
||||
if err := r.mem.EnsurePrimingWithManager(mgr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize static resources once during controller startup using manager.Runnable
|
||||
if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error {
|
||||
if err := r.initializeStaticResourcesOnce(ctx); err != nil {
|
||||
log.FromContext(ctx).Error(err, "Failed to initialize static resources")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named("cozystackresource-controller").
|
||||
For(&cozyv1alpha1.CozystackResourceDefinition{}, builder.WithPredicates()).
|
||||
Watches(
|
||||
&cozyv1alpha1.CozystackResourceDefinition{},
|
||||
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
r.mu.Lock()
|
||||
r.lastEvent = time.Now()
|
||||
r.mu.Unlock()
|
||||
return []reconcile.Request{{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: "cozy-system",
|
||||
Name: "cozystack-api",
|
||||
},
|
||||
}}
|
||||
}),
|
||||
).
|
||||
WithOptions(controller.Options{
|
||||
MaxConcurrentReconciles: 5, // Allow more concurrent reconciles with proper rate limiting
|
||||
}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
type crdHashView struct {
|
||||
Name string `json:"name"`
|
||||
Spec cozyv1alpha1.CozystackResourceDefinitionSpec `json:"spec"`
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionReconciler) computeConfigHash(ctx context.Context) (string, error) {
|
||||
var items []cozyv1alpha1.CozystackResourceDefinition
|
||||
if r.mem != nil {
|
||||
list, err := r.mem.ListFromCacheOrAPI(ctx, r.Client)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
items = list
|
||||
}
|
||||
|
||||
sort.Slice(items, func(i, j int) bool { return items[i].Name < items[j].Name })
|
||||
|
||||
views := make([]crdHashView, 0, len(items))
|
||||
for i := range items {
|
||||
views = append(views, crdHashView{
|
||||
Name: items[i].Name,
|
||||
Spec: items[i].Spec,
|
||||
})
|
||||
}
|
||||
b, err := json.Marshal(views)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sum := sha256.Sum256(b)
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionReconciler) debouncedRestart(ctx context.Context, logger logr.Logger) (ctrl.Result, error) {
|
||||
r.mu.Lock()
|
||||
le := r.lastEvent
|
||||
lh := r.lastHandled
|
||||
debounce := r.Debounce
|
||||
r.mu.Unlock()
|
||||
|
||||
if debounce <= 0 {
|
||||
debounce = 5 * time.Second
|
||||
}
|
||||
if le.IsZero() {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
if d := time.Since(le); d < debounce {
|
||||
return ctrl.Result{RequeueAfter: debounce - d}, nil
|
||||
}
|
||||
if !lh.Before(le) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
newHash, err := r.computeConfigHash(ctx)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
deploy := &appsv1.Deployment{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Namespace: "cozy-system", Name: "cozystack-api"}, deploy); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
if deploy.Spec.Template.Annotations == nil {
|
||||
deploy.Spec.Template.Annotations = map[string]string{}
|
||||
}
|
||||
oldHash := deploy.Spec.Template.Annotations["cozystack.io/config-hash"]
|
||||
|
||||
if oldHash == newHash && oldHash != "" {
|
||||
r.mu.Lock()
|
||||
r.lastHandled = le
|
||||
r.mu.Unlock()
|
||||
logger.Info("No changes in CRD config; skipping restart", "hash", newHash)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
patch := client.MergeFrom(deploy.DeepCopy())
|
||||
deploy.Spec.Template.Annotations["cozystack.io/config-hash"] = newHash
|
||||
|
||||
if err := r.Patch(ctx, deploy, patch); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.lastHandled = le
|
||||
r.mu.Unlock()
|
||||
|
||||
logger.Info("Updated cozystack-api podTemplate config-hash; rollout triggered",
|
||||
"old", oldHash, "new", newHash)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
80
internal/controller/dashboard/breadcrumb.go
Normal file
80
internal/controller/dashboard/breadcrumb.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
dashv1alpha1 "github.com/cozystack/cozystack/api/dashboard/v1alpha1"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
|
||||
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
)
|
||||
|
||||
// ensureBreadcrumb creates or updates a Breadcrumb resource for the given CRD
|
||||
func (m *Manager) ensureBreadcrumb(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
group, version, kind := pickGVK(crd)
|
||||
|
||||
lowerKind := strings.ToLower(kind)
|
||||
detailID := fmt.Sprintf("stock-project-factory-%s-details", lowerKind)
|
||||
|
||||
obj := &dashv1alpha1.Breadcrumb{}
|
||||
obj.SetName(detailID)
|
||||
|
||||
plural := pickPlural(kind, crd)
|
||||
|
||||
// Prefer dashboard.Plural for UI label if provided
|
||||
labelPlural := titleFromKindPlural(kind, plural)
|
||||
if crd != nil && crd.Spec.Dashboard != nil && crd.Spec.Dashboard.Plural != "" {
|
||||
labelPlural = crd.Spec.Dashboard.Plural
|
||||
}
|
||||
|
||||
key := plural // e.g., "virtualmachines"
|
||||
label := labelPlural
|
||||
link := fmt.Sprintf("/openapi-ui/{clusterName}/{namespace}/api-table/%s/%s/%s", strings.ToLower(group), strings.ToLower(version), plural)
|
||||
// If this is a module, change the first breadcrumb item to "Tenant Modules"
|
||||
if crd.Spec.Dashboard != nil && crd.Spec.Dashboard.Module {
|
||||
key = "tenantmodules"
|
||||
label = "Tenant Modules"
|
||||
link = "/openapi-ui/{clusterName}/{namespace}/api-table/core.cozystack.io/v1alpha1/tenantmodules"
|
||||
}
|
||||
|
||||
items := []any{
|
||||
map[string]any{
|
||||
"key": key,
|
||||
"label": label,
|
||||
"link": link,
|
||||
},
|
||||
map[string]any{
|
||||
"key": strings.ToLower(kind), // "etcd"
|
||||
"label": "{6}", // literal, as in your example
|
||||
},
|
||||
}
|
||||
|
||||
spec := map[string]any{
|
||||
"id": detailID,
|
||||
"breadcrumbItems": items,
|
||||
}
|
||||
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error {
|
||||
if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add dashboard labels to dynamic resources
|
||||
m.addDashboardLabels(obj, crd, ResourceTypeDynamic)
|
||||
b, err := json.Marshal(spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only update spec if it's different to avoid unnecessary updates
|
||||
newSpec := dashv1alpha1.ArbitrarySpec{JSON: apiextv1.JSON{Raw: b}}
|
||||
if !compareArbitrarySpecs(obj.Spec, newSpec) {
|
||||
obj.Spec = newSpec
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
168
internal/controller/dashboard/customcolumns.go
Normal file
168
internal/controller/dashboard/customcolumns.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
dashv1alpha1 "github.com/cozystack/cozystack/api/dashboard/v1alpha1"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
|
||||
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
)
|
||||
|
||||
// ensureCustomColumnsOverride creates or updates a CustomColumnsOverride that
|
||||
// renders a header row with a colored badge and resource name link, plus a few
|
||||
// useful columns (Ready, Created, Version).
|
||||
//
|
||||
// Naming convention mirrors your example:
|
||||
//
|
||||
// metadata.name: stock-namespace-<group>.<version>.<plural>
|
||||
// spec.id: stock-namespace-/<group>/<version>/<plural>
|
||||
func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) (controllerutil.OperationResult, error) {
|
||||
g, v, kind := pickGVK(crd)
|
||||
plural := pickPlural(kind, crd)
|
||||
// Details page segment uses lowercase kind, mirroring your example
|
||||
detailsSegment := strings.ToLower(kind) + "-details"
|
||||
|
||||
name := fmt.Sprintf("stock-namespace-%s.%s.%s", g, v, plural)
|
||||
id := fmt.Sprintf("stock-namespace-/%s/%s/%s", g, v, plural)
|
||||
|
||||
// Badge content & color derived from kind
|
||||
badgeText := initialsFromKind(kind) // e.g., "VirtualMachine" -> "VM", "Bucket" -> "B"
|
||||
badgeColor := hexColorForKind(kind) // deterministic, dark enough for white text
|
||||
|
||||
obj := &dashv1alpha1.CustomColumnsOverride{}
|
||||
obj.SetName(name)
|
||||
|
||||
href := fmt.Sprintf("/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/%s/{reqsJsonPath[0]['.metadata.name']['-']}", detailsSegment)
|
||||
if g == "apps.cozystack.io" && kind == "Tenant" && plural == "tenants" {
|
||||
href = "/openapi-ui/{2}/{reqsJsonPath[0]['.status.namespace']['-']}/api-table/core.cozystack.io/v1alpha1/tenantmodules"
|
||||
}
|
||||
|
||||
desired := map[string]any{
|
||||
"spec": map[string]any{
|
||||
"id": id,
|
||||
"additionalPrinterColumns": []any{
|
||||
map[string]any{
|
||||
"name": "Name",
|
||||
"type": "factory",
|
||||
"jsonPath": ".metadata.name",
|
||||
"customProps": map[string]any{
|
||||
"disableEventBubbling": true,
|
||||
"items": []any{
|
||||
map[string]any{
|
||||
"type": "antdFlex",
|
||||
"data": map[string]any{
|
||||
"id": "header-row",
|
||||
"align": "center",
|
||||
"gap": 6,
|
||||
},
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "header-badge",
|
||||
"text": badgeText,
|
||||
"title": strings.ToLower(kind), // optional tooltip
|
||||
"style": map[string]any{
|
||||
"backgroundColor": badgeColor,
|
||||
"borderRadius": "20px",
|
||||
"color": "#fff",
|
||||
"display": "inline-block",
|
||||
"fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif",
|
||||
"fontSize": "15px",
|
||||
"fontWeight": 400,
|
||||
"lineHeight": "24px",
|
||||
"minWidth": 24,
|
||||
"padding": "0 9px",
|
||||
"textAlign": "center",
|
||||
"whiteSpace": "nowrap",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "antdLink",
|
||||
"data": map[string]any{
|
||||
"id": "name-link",
|
||||
"text": "{reqsJsonPath[0]['.metadata.name']['-']}",
|
||||
"href": href,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"name": "Ready",
|
||||
"type": "Boolean",
|
||||
"jsonPath": `.status.conditions[?(@.type=="Ready")].status`,
|
||||
},
|
||||
map[string]any{
|
||||
"name": "Created",
|
||||
"type": "factory",
|
||||
"jsonPath": ".metadata.creationTimestamp",
|
||||
"customProps": map[string]any{
|
||||
"disableEventBubbling": true,
|
||||
"items": []any{
|
||||
map[string]any{
|
||||
"type": "antdFlex",
|
||||
"data": map[string]any{
|
||||
"id": "time-block",
|
||||
"align": "center",
|
||||
"gap": 6,
|
||||
},
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "time-icon",
|
||||
"text": "🌐",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "parsedText",
|
||||
"data": map[string]any{
|
||||
"id": "time-value",
|
||||
"text": "{reqsJsonPath[0]['.metadata.creationTimestamp']['-']}",
|
||||
"formatter": "timestamp",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"name": "Version",
|
||||
"type": "string",
|
||||
"jsonPath": ".status.version",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error {
|
||||
if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add dashboard labels to dynamic resources
|
||||
m.addDashboardLabels(obj, crd, ResourceTypeDynamic)
|
||||
b, err := json.Marshal(desired["spec"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only update spec if it's different to avoid unnecessary updates
|
||||
newSpec := dashv1alpha1.ArbitrarySpec{JSON: apiextv1.JSON{Raw: b}}
|
||||
if !compareArbitrarySpecs(obj.Spec, newSpec) {
|
||||
obj.Spec = newSpec
|
||||
}
|
||||
return nil
|
||||
})
|
||||
// Return OperationResultCreated/Updated is not available here with unstructured; we can mimic Updated when no error.
|
||||
return controllerutil.OperationResultNone, err
|
||||
}
|
||||
75
internal/controller/dashboard/customformsoverride.go
Normal file
75
internal/controller/dashboard/customformsoverride.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
dashv1alpha1 "github.com/cozystack/cozystack/api/dashboard/v1alpha1"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
|
||||
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
)
|
||||
|
||||
// ensureCustomFormsOverride creates or updates a CustomFormsOverride resource for the given CRD
|
||||
func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
g, v, kind := pickGVK(crd)
|
||||
plural := pickPlural(kind, crd)
|
||||
|
||||
name := fmt.Sprintf("%s.%s.%s", g, v, plural)
|
||||
customizationID := fmt.Sprintf("default-/%s/%s/%s", g, v, plural)
|
||||
|
||||
obj := &dashv1alpha1.CustomFormsOverride{}
|
||||
obj.SetName(name)
|
||||
|
||||
// Replicates your Helm includes (system metadata + api + status).
|
||||
hidden := []any{}
|
||||
hidden = append(hidden, hiddenMetadataSystem()...)
|
||||
hidden = append(hidden, hiddenMetadataAPI()...)
|
||||
hidden = append(hidden, hiddenStatus()...)
|
||||
|
||||
// If Name is set, hide metadata
|
||||
if crd.Spec.Dashboard != nil && strings.TrimSpace(crd.Spec.Dashboard.Name) != "" {
|
||||
hidden = append([]interface{}{
|
||||
[]any{"metadata"},
|
||||
}, hidden...)
|
||||
}
|
||||
|
||||
var sort []any
|
||||
if crd.Spec.Dashboard != nil && len(crd.Spec.Dashboard.KeysOrder) > 0 {
|
||||
sort = make([]any, len(crd.Spec.Dashboard.KeysOrder))
|
||||
for i, v := range crd.Spec.Dashboard.KeysOrder {
|
||||
sort[i] = v
|
||||
}
|
||||
}
|
||||
|
||||
spec := map[string]any{
|
||||
"customizationId": customizationID,
|
||||
"hidden": hidden,
|
||||
"sort": sort,
|
||||
"schema": map[string]any{}, // {}
|
||||
"strategy": "merge",
|
||||
}
|
||||
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error {
|
||||
if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add dashboard labels to dynamic resources
|
||||
m.addDashboardLabels(obj, crd, ResourceTypeDynamic)
|
||||
b, err := json.Marshal(spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only update spec if it's different to avoid unnecessary updates
|
||||
newSpec := dashv1alpha1.ArbitrarySpec{JSON: apiextv1.JSON{Raw: b}}
|
||||
if !compareArbitrarySpecs(obj.Spec, newSpec) {
|
||||
obj.Spec = newSpec
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
81
internal/controller/dashboard/customformsprefill.go
Normal file
81
internal/controller/dashboard/customformsprefill.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
dashv1alpha1 "github.com/cozystack/cozystack/api/dashboard/v1alpha1"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
|
||||
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
// ensureCustomFormsPrefill creates or updates a CustomFormsPrefill resource for the given CRD
|
||||
func (m *Manager) ensureCustomFormsPrefill(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) (reconcile.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
app := crd.Spec.Application
|
||||
group, version, kind := pickGVK(crd)
|
||||
plural := pickPlural(kind, crd)
|
||||
|
||||
name := fmt.Sprintf("%s.%s.%s", group, version, plural)
|
||||
customizationID := fmt.Sprintf("default-/%s/%s/%s", group, version, plural)
|
||||
|
||||
values, err := buildPrefillValues(app.OpenAPISchema)
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
// Always prefill metadata.name (empty string if not specified in CRD)
|
||||
var nameValue string
|
||||
if crd.Spec.Dashboard != nil {
|
||||
nameValue = strings.TrimSpace(crd.Spec.Dashboard.Name)
|
||||
}
|
||||
values = append([]interface{}{
|
||||
map[string]interface{}{
|
||||
"path": toIfaceSlice([]string{"metadata", "name"}),
|
||||
"value": nameValue,
|
||||
},
|
||||
}, values...)
|
||||
|
||||
cfp := &dashv1alpha1.CustomFormsPrefill{}
|
||||
cfp.Name = name // cluster-scoped
|
||||
|
||||
specMap := map[string]any{
|
||||
"customizationId": customizationID,
|
||||
"values": values,
|
||||
}
|
||||
// Use json.Marshal with sorted keys to ensure consistent output
|
||||
specBytes, err := json.Marshal(specMap)
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, m.client, cfp, func() error {
|
||||
if err := controllerutil.SetOwnerReference(crd, cfp, m.scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add dashboard labels to dynamic resources
|
||||
m.addDashboardLabels(cfp, crd, ResourceTypeDynamic)
|
||||
|
||||
// Only update spec if it's different to avoid unnecessary updates
|
||||
newSpec := dashv1alpha1.ArbitrarySpec{
|
||||
JSON: apiextv1.JSON{Raw: specBytes},
|
||||
}
|
||||
if !compareArbitrarySpecs(cfp.Spec, newSpec) {
|
||||
cfp.Spec = newSpec
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
logger.Info("Applied CustomFormsPrefill", "name", cfp.Name)
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
515
internal/controller/dashboard/factory.go
Normal file
515
internal/controller/dashboard/factory.go
Normal file
@@ -0,0 +1,515 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
dashv1alpha1 "github.com/cozystack/cozystack/api/dashboard/v1alpha1"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
|
||||
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
)
|
||||
|
||||
// ensureFactory creates or updates a Factory resource for the given CRD
|
||||
func (m *Manager) ensureFactory(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
g, v, kind := pickGVK(crd)
|
||||
plural := pickPlural(kind, crd)
|
||||
|
||||
lowerKind := strings.ToLower(kind)
|
||||
factoryName := fmt.Sprintf("%s-details", lowerKind)
|
||||
resourceFetch := fmt.Sprintf("/api/clusters/{2}/k8s/apis/%s/%s/namespaces/{3}/%s/{6}", g, v, plural)
|
||||
|
||||
flags := factoryFeatureFlags(crd)
|
||||
|
||||
var keysOrder [][]string
|
||||
if crd.Spec.Dashboard != nil {
|
||||
keysOrder = crd.Spec.Dashboard.KeysOrder
|
||||
}
|
||||
tabs := []any{
|
||||
detailsTab(kind, resourceFetch, crd.Spec.Application.OpenAPISchema, keysOrder),
|
||||
}
|
||||
if flags.Workloads {
|
||||
tabs = append(tabs, workloadsTab(kind))
|
||||
}
|
||||
if flags.Ingresses {
|
||||
tabs = append(tabs, ingressesTab(kind))
|
||||
}
|
||||
if flags.Services {
|
||||
tabs = append(tabs, servicesTab(kind))
|
||||
}
|
||||
if flags.Secrets {
|
||||
tabs = append(tabs, secretsTab(kind))
|
||||
}
|
||||
tabs = append(tabs, yamlTab(plural))
|
||||
|
||||
// Use unified factory creation
|
||||
config := UnifiedResourceConfig{
|
||||
Name: factoryName,
|
||||
ResourceType: "factory",
|
||||
Kind: kind,
|
||||
Plural: plural,
|
||||
Title: strings.ToLower(plural),
|
||||
Size: BadgeSizeLarge,
|
||||
}
|
||||
|
||||
spec := createUnifiedFactory(config, tabs, []any{resourceFetch})
|
||||
|
||||
obj := &dashv1alpha1.Factory{}
|
||||
obj.SetName(factoryName)
|
||||
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error {
|
||||
if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add dashboard labels to dynamic resources
|
||||
m.addDashboardLabels(obj, crd, ResourceTypeDynamic)
|
||||
b, err := json.Marshal(spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only update spec if it's different to avoid unnecessary updates
|
||||
newSpec := dashv1alpha1.ArbitrarySpec{JSON: apiextv1.JSON{Raw: b}}
|
||||
if !compareArbitrarySpecs(obj.Spec, newSpec) {
|
||||
obj.Spec = newSpec
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------------- Tabs builders ----------------
|
||||
|
||||
func detailsTab(kind, endpoint, schemaJSON string, keysOrder [][]string) map[string]any {
|
||||
paramsBlocks := buildOpenAPIParamsBlocks(schemaJSON, keysOrder)
|
||||
paramsList := map[string]any{
|
||||
"type": "antdFlex",
|
||||
"data": map[string]any{
|
||||
"id": "params-list",
|
||||
"vertical": true,
|
||||
"gap": float64(24),
|
||||
},
|
||||
"children": paramsBlocks,
|
||||
}
|
||||
|
||||
leftColStack := []any{
|
||||
antdText("details-title", true, kind, map[string]any{
|
||||
"fontSize": float64(20),
|
||||
"marginBottom": float64(12),
|
||||
}),
|
||||
antdFlexVertical("meta-name-block", 4, []any{
|
||||
antdText("meta-name-label", true, "Name", nil),
|
||||
parsedText("meta-name-value", "{reqsJsonPath[0]['.metadata.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("meta-namespace-block", 8, []any{
|
||||
antdText("meta-namespace-label", true, "Namespace", nil),
|
||||
map[string]any{
|
||||
"type": "antdFlex",
|
||||
"data": map[string]any{
|
||||
"id": "namespace-row",
|
||||
"align": "center",
|
||||
"gap": float64(6),
|
||||
},
|
||||
"children": []any{
|
||||
createUnifiedBadgeFromKind("ns-badge", "Namespace", "namespace", BadgeSizeMedium),
|
||||
antdLink("namespace-link",
|
||||
"{reqsJsonPath[0]['.metadata.namespace']['-']}",
|
||||
"/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace",
|
||||
),
|
||||
},
|
||||
},
|
||||
}),
|
||||
antdFlexVertical("meta-created-block", 4, []any{
|
||||
antdText("time-label", true, "Created", nil),
|
||||
antdFlex("time-block", 6, []any{
|
||||
antdText("time-icon", false, "🌐", nil),
|
||||
parsedTextWithFormatter("time-value", "{reqsJsonPath[0]['.metadata.creationTimestamp']['-']}", "timestamp"),
|
||||
}),
|
||||
}),
|
||||
antdFlexVertical("meta-version-block", 4, []any{
|
||||
antdText("version-label", true, "Version", nil),
|
||||
parsedText("version-value", "{reqsJsonPath[0]['.status.version']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("meta-released-block", 4, []any{
|
||||
antdText("released-label", true, "Released", nil),
|
||||
parsedText("released-value", "{reqsJsonPath[0]['.status.conditions[?(@.type==\"Released\")].status']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("meta-ready-block", 4, []any{
|
||||
antdText("ready-label", true, "Ready", nil),
|
||||
parsedText("ready-value", "{reqsJsonPath[0]['.status.conditions[?(@.type==\"Ready\")].status']['-']}", nil),
|
||||
}),
|
||||
}
|
||||
|
||||
rightColStack := []any{
|
||||
antdText("params-title", true, "Parameters", map[string]any{
|
||||
"fontSize": float64(20),
|
||||
"marginBottom": float64(12),
|
||||
}),
|
||||
paramsList,
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"key": "details",
|
||||
"label": "Details",
|
||||
"children": []any{
|
||||
contentCard("details-card", map[string]any{"marginBottom": float64(24)}, []any{
|
||||
map[string]any{
|
||||
"type": "antdRow",
|
||||
"data": map[string]any{
|
||||
"id": "details-grid",
|
||||
"gutter": []any{float64(48), float64(12)},
|
||||
},
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"type": "antdCol",
|
||||
"data": map[string]any{"id": "col-left", "span": float64(12)},
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"type": "antdFlex",
|
||||
"data": map[string]any{"id": "col-left-stack", "vertical": true, "gap": float64(24)},
|
||||
"children": leftColStack,
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "antdCol",
|
||||
"data": map[string]any{"id": "col-right", "span": float64(12)},
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"type": "antdFlex",
|
||||
"data": map[string]any{"id": "col-right-stack", "vertical": true, "gap": float64(24)},
|
||||
"children": rightColStack,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
spacer("conditions-top-spacer", float64(16)),
|
||||
antdText("conditions-title", true, "Conditions", map[string]any{"fontSize": float64(20)}),
|
||||
spacer("conditions-spacer", float64(8)),
|
||||
map[string]any{
|
||||
"type": "EnrichedTable",
|
||||
"data": map[string]any{
|
||||
"id": "conditions-table",
|
||||
"fetchUrl": endpoint,
|
||||
"clusterNamePartOfUrl": "{2}",
|
||||
"customizationId": "factory-status-conditions",
|
||||
"baseprefix": "/openapi-ui",
|
||||
"withoutControls": true,
|
||||
"pathToItems": []any{"status", "conditions"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func workloadsTab(kind string) map[string]any {
|
||||
return map[string]any{
|
||||
"key": "workloads",
|
||||
"label": "Workloads",
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"type": "EnrichedTable",
|
||||
"data": map[string]any{
|
||||
"id": "workloads-table",
|
||||
"fetchUrl": "/api/clusters/{2}/k8s/apis/cozystack.io/v1alpha1/namespaces/{3}/workloadmonitors",
|
||||
"clusterNamePartOfUrl": "{2}",
|
||||
"baseprefix": "/openapi-ui",
|
||||
"customizationId": "factory-details-v1alpha1.cozystack.io.workloadmonitors",
|
||||
"pathToItems": []any{"items"},
|
||||
"labelsSelector": map[string]any{
|
||||
"apps.cozystack.io/application.group": "apps.cozystack.io",
|
||||
"apps.cozystack.io/application.kind": kind,
|
||||
"apps.cozystack.io/application.name": "{reqs[0]['metadata','name']}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func servicesTab(kind string) map[string]any {
|
||||
return map[string]any{
|
||||
"key": "services",
|
||||
"label": "Services",
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"type": "EnrichedTable",
|
||||
"data": map[string]any{
|
||||
"id": "services-table",
|
||||
"fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/services",
|
||||
"clusterNamePartOfUrl": "{2}",
|
||||
"baseprefix": "/openapi-ui",
|
||||
"customizationId": "factory-details-v1.services",
|
||||
"pathToItems": []any{"items"},
|
||||
"labelsSelector": map[string]any{
|
||||
"apps.cozystack.io/application.group": "apps.cozystack.io",
|
||||
"apps.cozystack.io/application.kind": kind,
|
||||
"apps.cozystack.io/application.name": "{reqs[0]['metadata','name']}",
|
||||
"internal.cozystack.io/tenantresource": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ingressesTab(kind string) map[string]any {
|
||||
return map[string]any{
|
||||
"key": "ingresses",
|
||||
"label": "Ingresses",
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"type": "EnrichedTable",
|
||||
"data": map[string]any{
|
||||
"id": "ingresses-table",
|
||||
"fetchUrl": "/api/clusters/{2}/k8s/apis/networking.k8s.io/v1/namespaces/{3}/ingresses",
|
||||
"clusterNamePartOfUrl": "{2}",
|
||||
"baseprefix": "/openapi-ui",
|
||||
"customizationId": "factory-details-networking.k8s.io.v1.ingresses",
|
||||
"pathToItems": []any{"items"},
|
||||
"labelsSelector": map[string]any{
|
||||
"apps.cozystack.io/application.group": "apps.cozystack.io",
|
||||
"apps.cozystack.io/application.kind": kind,
|
||||
"apps.cozystack.io/application.name": "{reqs[0]['metadata','name']}",
|
||||
"internal.cozystack.io/tenantresource": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func secretsTab(kind string) map[string]any {
|
||||
return map[string]any{
|
||||
"key": "secrets",
|
||||
"label": "Secrets",
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"type": "EnrichedTable",
|
||||
"data": map[string]any{
|
||||
"id": "secrets-table",
|
||||
"fetchUrl": "/api/clusters/{2}/k8s/apis/core.cozystack.io/v1alpha1/namespaces/{3}/tenantsecretstables",
|
||||
"clusterNamePartOfUrl": "{2}",
|
||||
"baseprefix": "/openapi-ui",
|
||||
"customizationId": "factory-details-v1alpha1.core.cozystack.io.tenantsecretstables",
|
||||
"pathToItems": []any{"items"},
|
||||
"labelsSelector": map[string]any{
|
||||
"apps.cozystack.io/application.group": "apps.cozystack.io",
|
||||
"apps.cozystack.io/application.kind": kind,
|
||||
"apps.cozystack.io/application.name": "{reqs[0]['metadata','name']}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func yamlTab(plural string) map[string]any {
|
||||
return map[string]any{
|
||||
"key": "yaml",
|
||||
"label": "YAML",
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"type": "YamlEditorSingleton",
|
||||
"data": map[string]any{
|
||||
"id": "yaml-editor",
|
||||
"cluster": "{2}",
|
||||
"isNameSpaced": true,
|
||||
"type": "builtin",
|
||||
"typeName": plural,
|
||||
"prefillValuesRequestIndex": float64(0),
|
||||
"substractHeight": float64(400),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- OpenAPI → Right column ----------------
|
||||
|
||||
func buildOpenAPIParamsBlocks(schemaJSON string, keysOrder [][]string) []any {
|
||||
var blocks []any
|
||||
fields := collectOpenAPILeafFields(schemaJSON, 2, 20)
|
||||
|
||||
// Sort fields according to keysOrder if provided
|
||||
if len(keysOrder) > 0 {
|
||||
fields = sortFieldsByKeysOrder(fields, keysOrder)
|
||||
}
|
||||
|
||||
for idx, f := range fields {
|
||||
id := fmt.Sprintf("param-%d", idx)
|
||||
blocks = append(blocks,
|
||||
antdFlexVertical(id, 4, []any{
|
||||
antdText(id+"-label", true, f.Label, nil),
|
||||
parsedText(id+"-value", fmt.Sprintf("{reqsJsonPath[0]['.spec.%s']['-']}", f.JSONPathSpec), nil),
|
||||
}),
|
||||
)
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
blocks = append(blocks,
|
||||
antdText("params-empty", false, "No scalar parameters detected in schema (see YAML tab for full spec).", map[string]any{"opacity": float64(0.7)}),
|
||||
)
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
// sortFieldsByKeysOrder sorts fields according to the provided keysOrder
|
||||
func sortFieldsByKeysOrder(fields []fieldInfo, keysOrder [][]string) []fieldInfo {
|
||||
// Create a map for quick lookup of field positions
|
||||
orderMap := make(map[string]int)
|
||||
for i, path := range keysOrder {
|
||||
// Convert path to dot notation (e.g., ["spec", "systemDisk", "image"] -> "systemDisk.image")
|
||||
if len(path) > 1 && path[0] == "spec" {
|
||||
dotPath := strings.Join(path[1:], ".")
|
||||
orderMap[dotPath] = i
|
||||
}
|
||||
}
|
||||
|
||||
// Sort fields based on their position in keysOrder
|
||||
sort.Slice(fields, func(i, j int) bool {
|
||||
posI, existsI := orderMap[fields[i].JSONPathSpec]
|
||||
posJ, existsJ := orderMap[fields[j].JSONPathSpec]
|
||||
|
||||
// If both exist in orderMap, sort by position
|
||||
if existsI && existsJ {
|
||||
return posI < posJ
|
||||
}
|
||||
// If only one exists, prioritize the one that exists
|
||||
if existsI {
|
||||
return true
|
||||
}
|
||||
if existsJ {
|
||||
return false
|
||||
}
|
||||
// If neither exists, maintain original order (stable sort)
|
||||
return i < j
|
||||
})
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
func collectOpenAPILeafFields(schemaJSON string, maxDepth, maxFields int) []fieldInfo {
|
||||
type node = map[string]any
|
||||
|
||||
if strings.TrimSpace(schemaJSON) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var root any
|
||||
if err := json.Unmarshal([]byte(schemaJSON), &root); err != nil {
|
||||
// invalid JSON — skip
|
||||
return nil
|
||||
}
|
||||
|
||||
props := map[string]any{}
|
||||
if m, ok := root.(node); ok {
|
||||
if p, ok := m["properties"].(node); ok {
|
||||
props = p
|
||||
}
|
||||
}
|
||||
if len(props) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var out []fieldInfo
|
||||
var visit func(prefix []string, n node, depth int)
|
||||
|
||||
addField := func(path []string, schema node) {
|
||||
// Skip excluded paths (backup/bootstrap/password)
|
||||
if shouldExcludeParamPath(path) {
|
||||
return
|
||||
}
|
||||
// build label "Foo Bar / Baz"
|
||||
label := humanizePath(path)
|
||||
desc := getString(schema, "description")
|
||||
out = append(out, fieldInfo{
|
||||
JSONPathSpec: strings.Join(path, "."),
|
||||
Label: label,
|
||||
Description: desc,
|
||||
})
|
||||
}
|
||||
|
||||
visit = func(prefix []string, n node, depth int) {
|
||||
if len(out) >= maxFields {
|
||||
return
|
||||
}
|
||||
// Scalar?
|
||||
if isScalarType(n) || isIntOrString(n) || hasEnum(n) {
|
||||
addField(prefix, n)
|
||||
return
|
||||
}
|
||||
// Object with properties
|
||||
if props, ok := n["properties"].(node); ok {
|
||||
if depth >= maxDepth {
|
||||
// too deep — stop
|
||||
return
|
||||
}
|
||||
// deterministic ordering
|
||||
keys := make([]string, 0, len(props))
|
||||
for k := range props {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
child, _ := props[k].(node)
|
||||
visit(append(prefix, k), child, depth+1)
|
||||
if len(out) >= maxFields {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
// Arrays: try to render item if it's scalar and depth limit allows
|
||||
if n["type"] == "array" {
|
||||
if items, ok := n["items"].(node); ok && (isScalarType(items) || isIntOrString(items) || hasEnum(items)) {
|
||||
addField(prefix, items)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Otherwise skip (unknown/complex)
|
||||
}
|
||||
|
||||
// top-level: iterate properties
|
||||
keys := make([]string, 0, len(props))
|
||||
for k := range props {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
if child, ok := props[k].(node); ok {
|
||||
visit([]string{k}, child, 1)
|
||||
if len(out) >= maxFields {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------------- Feature flags ----------------
|
||||
|
||||
type factoryFlags struct {
|
||||
Workloads bool
|
||||
Ingresses bool
|
||||
Services bool
|
||||
Secrets bool
|
||||
}
|
||||
|
||||
// factoryFeatureFlags tries several conventional locations so you can evolve the API
|
||||
// without breaking the controller. Defaults are false (hidden).
|
||||
func factoryFeatureFlags(crd *cozyv1alpha1.CozystackResourceDefinition) factoryFlags {
|
||||
var f factoryFlags
|
||||
|
||||
f.Workloads = true
|
||||
f.Ingresses = true
|
||||
f.Services = true
|
||||
f.Secrets = true
|
||||
|
||||
return f
|
||||
}
|
||||
542
internal/controller/dashboard/helpers.go
Normal file
542
internal/controller/dashboard/helpers.go
Normal file
@@ -0,0 +1,542 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
dashv1alpha1 "github.com/cozystack/cozystack/api/dashboard/v1alpha1"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
)
|
||||
|
||||
// ---------------- Types used by OpenAPI parsing ----------------
|
||||
|
||||
type fieldInfo struct {
|
||||
JSONPathSpec string // dotted path under .spec (e.g., "systemDisk.image")
|
||||
Label string // "System Disk / Image" or "systemDisk.image"
|
||||
Description string
|
||||
}
|
||||
|
||||
// ---------------- Public entry: ensure Factory ------------------
|
||||
|
||||
// pickGVK tries to read group/version/kind from the CRD. We prefer the "application" section,
|
||||
// falling back to other likely fields if your schema differs.
|
||||
func pickGVK(crd *cozyv1alpha1.CozystackResourceDefinition) (group, version, kind string) {
|
||||
// Best guess based on your examples:
|
||||
if crd.Spec.Application.Kind != "" {
|
||||
kind = crd.Spec.Application.Kind
|
||||
}
|
||||
|
||||
// For applications, always use apps.cozystack.io group, not the CRD's own group
|
||||
group = "apps.cozystack.io"
|
||||
version = "v1alpha1"
|
||||
|
||||
// Reasonable fallbacks if any are empty:
|
||||
if kind == "" {
|
||||
kind = "Resource"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// pickPlural prefers a field on the CRD if you have it; otherwise do a simple lowercase + "s".
|
||||
func pickPlural(kind string, crd *cozyv1alpha1.CozystackResourceDefinition) string {
|
||||
// If you have crd.Spec.Application.Plural, prefer it. Example:
|
||||
if crd.Spec.Application.Plural != "" {
|
||||
return crd.Spec.Application.Plural
|
||||
}
|
||||
// naive pluralization
|
||||
k := strings.ToLower(kind)
|
||||
if strings.HasSuffix(k, "s") {
|
||||
return k
|
||||
}
|
||||
return k + "s"
|
||||
}
|
||||
|
||||
// initialsFromKind splits CamelCase and returns the first letters in upper case.
|
||||
// "VirtualMachine" -> "VM"; "Bucket" -> "B".
|
||||
func initialsFromKind(kind string) string {
|
||||
parts := splitCamel(kind)
|
||||
if len(parts) == 0 {
|
||||
return strings.ToUpper(kind)
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, p := range parts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
b.WriteString(strings.ToUpper(string(p[0])))
|
||||
// Limit to 3 chars to keep the badge compact (VM, PVC, etc.)
|
||||
if b.Len() >= 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// hexColorForKind returns a dark, saturated color (hex) derived from a stable hash of the kind.
|
||||
// We map the hash to an HSL hue; fix S/L for consistent readability with white text.
|
||||
func hexColorForKind(kind string) string {
|
||||
// Stable short hash (sha1 → bytes → hue)
|
||||
sum := sha1.Sum([]byte(kind))
|
||||
// Use first two bytes for hue [0..359]
|
||||
hue := int(sum[0])<<8 | int(sum[1])
|
||||
hue = hue % 360
|
||||
|
||||
// Fixed S/L chosen to contrast with white text:
|
||||
// S = 80%, L = 35% (dark enough so #fff is readable)
|
||||
r, g, b := hslToRGB(float64(hue), 0.80, 0.35)
|
||||
|
||||
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
|
||||
}
|
||||
|
||||
// hslToRGB converts HSL (0..360, 0..1, 0..1) to sRGB (0..255).
|
||||
func hslToRGB(h float64, s float64, l float64) (uint8, uint8, uint8) {
|
||||
c := (1 - absFloat(2*l-1)) * s
|
||||
hp := h / 60.0
|
||||
x := c * (1 - absFloat(modFloat(hp, 2)-1))
|
||||
var r1, g1, b1 float64
|
||||
switch {
|
||||
case 0 <= hp && hp < 1:
|
||||
r1, g1, b1 = c, x, 0
|
||||
case 1 <= hp && hp < 2:
|
||||
r1, g1, b1 = x, c, 0
|
||||
case 2 <= hp && hp < 3:
|
||||
r1, g1, b1 = 0, c, x
|
||||
case 3 <= hp && hp < 4:
|
||||
r1, g1, b1 = 0, x, c
|
||||
case 4 <= hp && hp < 5:
|
||||
r1, g1, b1 = x, 0, c
|
||||
default:
|
||||
r1, g1, b1 = c, 0, x
|
||||
}
|
||||
m := l - c/2
|
||||
r := uint8(clamp01(r1+m) * 255.0)
|
||||
g := uint8(clamp01(g1+m) * 255.0)
|
||||
b := uint8(clamp01(b1+m) * 255.0)
|
||||
return r, g, b
|
||||
}
|
||||
|
||||
func absFloat(v float64) float64 {
|
||||
if v < 0 {
|
||||
return -v
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func modFloat(a, b float64) float64 {
|
||||
return a - b*float64(int(a/b))
|
||||
}
|
||||
|
||||
func clamp01(v float64) float64 {
|
||||
if v < 0 {
|
||||
return 0
|
||||
}
|
||||
if v > 1 {
|
||||
return 1
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// optional: tiny helper to expose the compact color hash (useful for debugging)
|
||||
func shortHashHex(s string) string {
|
||||
sum := sha1.Sum([]byte(s))
|
||||
return hex.EncodeToString(sum[:4])
|
||||
}
|
||||
|
||||
// ----------------------- Helpers (OpenAPI → values) -----------------------
|
||||
|
||||
// defaultOrZero returns the schema default if present; otherwise a reasonable zero value.
|
||||
func defaultOrZero(sub map[string]interface{}) interface{} {
|
||||
if v, ok := sub["default"]; ok {
|
||||
return v
|
||||
}
|
||||
typ, _ := sub["type"].(string)
|
||||
switch typ {
|
||||
case "string":
|
||||
return ""
|
||||
case "boolean":
|
||||
return false
|
||||
case "array":
|
||||
return []interface{}{}
|
||||
case "integer", "number":
|
||||
return 0
|
||||
case "object":
|
||||
return map[string]interface{}{}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// toIfaceSlice converts []string -> []interface{}.
|
||||
func toIfaceSlice(ss []string) []interface{} {
|
||||
out := make([]interface{}, len(ss))
|
||||
for i, s := range ss {
|
||||
out[i] = s
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// buildPrefillValues converts an OpenAPI schema (JSON string) into a []interface{} "values" list
|
||||
// suitable for CustomFormsPrefill.spec.values.
|
||||
// Rules:
|
||||
// - For top-level primitive/array fields: emit an entry, using default if present, otherwise zero.
|
||||
// - For top-level objects: recursively process nested objects and emit entries for all default values
|
||||
// found at any nesting level.
|
||||
func buildPrefillValues(openAPISchema string) ([]interface{}, error) {
|
||||
var root map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(openAPISchema), &root); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse openAPISchema: %w", err)
|
||||
}
|
||||
props, _ := root["properties"].(map[string]interface{})
|
||||
if props == nil {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
|
||||
var values []interface{}
|
||||
processSchemaProperties(props, []string{"spec"}, &values, true)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// processSchemaProperties recursively processes OpenAPI schema properties and extracts default values
|
||||
func processSchemaProperties(props map[string]interface{}, path []string, values *[]interface{}, topLevel bool) {
|
||||
for pname, raw := range props {
|
||||
sub, _ := raw.(map[string]interface{})
|
||||
if sub == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
typ, _ := sub["type"].(string)
|
||||
currentPath := append(path, pname)
|
||||
|
||||
switch typ {
|
||||
case "object":
|
||||
// Check if this object has a default value
|
||||
if objDefault, ok := sub["default"].(map[string]interface{}); ok {
|
||||
// Process the default object recursively
|
||||
processDefaultObject(objDefault, currentPath, values)
|
||||
}
|
||||
|
||||
// Also process child properties for their individual defaults
|
||||
if childProps, ok := sub["properties"].(map[string]interface{}); ok {
|
||||
processSchemaProperties(childProps, currentPath, values, false)
|
||||
}
|
||||
default:
|
||||
// For primitive types, use default if present, otherwise zero value
|
||||
val := defaultOrZero(sub)
|
||||
// Only emit zero-value entries when at top level
|
||||
if val != nil || topLevel {
|
||||
entry := map[string]interface{}{
|
||||
"path": toIfaceSlice(currentPath),
|
||||
"value": val,
|
||||
}
|
||||
*values = append(*values, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processDefaultObject recursively processes a default object and creates entries for all nested values
|
||||
func processDefaultObject(obj map[string]interface{}, path []string, values *[]interface{}) {
|
||||
for key, value := range obj {
|
||||
currentPath := append(path, key)
|
||||
|
||||
// If the value is a map, process it recursively
|
||||
if nestedObj, ok := value.(map[string]interface{}); ok {
|
||||
processDefaultObject(nestedObj, currentPath, values)
|
||||
} else {
|
||||
// For primitive values, create an entry
|
||||
entry := map[string]interface{}{
|
||||
"path": toIfaceSlice(currentPath),
|
||||
"value": value,
|
||||
}
|
||||
*values = append(*values, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeJSON makes maps/slices JSON-safe for k8s Unstructured:
|
||||
// - converts all int/int32/... to float64
|
||||
// - leaves strings, bools, nil as-is
|
||||
func normalizeJSON(v any) any {
|
||||
switch t := v.(type) {
|
||||
case map[string]any:
|
||||
out := make(map[string]any, len(t))
|
||||
for k, val := range t {
|
||||
out[k] = normalizeJSON(val)
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
out := make([]any, len(t))
|
||||
for i := range t {
|
||||
out[i] = normalizeJSON(t[i])
|
||||
}
|
||||
return out
|
||||
case int:
|
||||
return float64(t)
|
||||
case int8:
|
||||
return float64(t)
|
||||
case int16:
|
||||
return float64(t)
|
||||
case int32:
|
||||
return float64(t)
|
||||
case int64:
|
||||
return float64(t)
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return float64(reflect.ValueOf(t).Convert(reflect.TypeOf(uint64(0))).Uint())
|
||||
case float32:
|
||||
return float64(t)
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
var camelSplitter = regexp.MustCompile(`(?m)([A-Z]+[a-z0-9]*|[a-z0-9]+)`)
|
||||
|
||||
func splitCamel(s string) []string {
|
||||
return camelSplitter.FindAllString(s, -1)
|
||||
}
|
||||
|
||||
// --- helpers for schema inspection ---
|
||||
|
||||
func isScalarType(n map[string]any) bool {
|
||||
switch getString(n, "type") {
|
||||
case "string", "integer", "number", "boolean":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isIntOrString(n map[string]any) bool {
|
||||
// Kubernetes extension: x-kubernetes-int-or-string: true
|
||||
if v, ok := n["x-kubernetes-int-or-string"]; ok {
|
||||
if b, ok := v.(bool); ok && b {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// anyOf: integer|string
|
||||
if anyOf, ok := n["anyOf"].([]any); ok {
|
||||
hasInt := false
|
||||
hasStr := false
|
||||
for _, it := range anyOf {
|
||||
if m, ok := it.(map[string]any); ok {
|
||||
switch getString(m, "type") {
|
||||
case "integer":
|
||||
hasInt = true
|
||||
case "string":
|
||||
hasStr = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasInt && hasStr
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasEnum(n map[string]any) bool {
|
||||
_, ok := n["enum"]
|
||||
return ok
|
||||
}
|
||||
|
||||
func getString(n map[string]any, key string) string {
|
||||
if v, ok := n[key]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// shouldExcludeParamPath returns true if any part of the path contains
|
||||
// backup / bootstrap / password (case-insensitive)
|
||||
func shouldExcludeParamPath(parts []string) bool {
|
||||
for _, p := range parts {
|
||||
lp := strings.ToLower(p)
|
||||
if strings.Contains(lp, "backup") || strings.Contains(lp, "bootstrap") || strings.Contains(lp, "password") || strings.Contains(lp, "cloudinit") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func humanizePath(parts []string) string {
|
||||
// "systemDisk.image" -> "System Disk / Image"
|
||||
return strings.Join(parts, " / ")
|
||||
}
|
||||
|
||||
// titleFromKindPlural returns a presentable plural label, e.g.:
|
||||
// kind="VirtualMachine", plural="virtualmachines" => "VirtualMachines"
|
||||
func titleFromKindPlural(kind, plural string) string {
|
||||
return kind + "s"
|
||||
}
|
||||
|
||||
// The hidden lists below mirror the Helm templates you shared.
|
||||
// Each entry is a path as nested string array, e.g. ["metadata","creationTimestamp"].
|
||||
|
||||
func hiddenMetadataSystem() []any {
|
||||
return []any{
|
||||
[]any{"metadata", "annotations"},
|
||||
[]any{"metadata", "labels"},
|
||||
[]any{"metadata", "namespace"},
|
||||
[]any{"metadata", "creationTimestamp"},
|
||||
[]any{"metadata", "deletionGracePeriodSeconds"},
|
||||
[]any{"metadata", "deletionTimestamp"},
|
||||
[]any{"metadata", "finalizers"},
|
||||
[]any{"metadata", "generateName"},
|
||||
[]any{"metadata", "generation"},
|
||||
[]any{"metadata", "managedFields"},
|
||||
[]any{"metadata", "ownerReferences"},
|
||||
[]any{"metadata", "resourceVersion"},
|
||||
[]any{"metadata", "selfLink"},
|
||||
[]any{"metadata", "uid"},
|
||||
}
|
||||
}
|
||||
|
||||
func hiddenMetadataAPI() []any {
|
||||
return []any{
|
||||
[]any{"kind"},
|
||||
[]any{"apiVersion"},
|
||||
[]any{"appVersion"},
|
||||
}
|
||||
}
|
||||
|
||||
func hiddenStatus() []any {
|
||||
return []any{
|
||||
[]any{"status"},
|
||||
}
|
||||
}
|
||||
|
||||
// compareArbitrarySpecs compares two ArbitrarySpec objects by comparing their JSON content
|
||||
func compareArbitrarySpecs(spec1, spec2 dashv1alpha1.ArbitrarySpec) bool {
|
||||
// If both are empty, they're equal
|
||||
if len(spec1.JSON.Raw) == 0 && len(spec2.JSON.Raw) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// If one is empty and the other is not, they're different
|
||||
if len(spec1.JSON.Raw) == 0 || len(spec2.JSON.Raw) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse and normalize both specs
|
||||
norm1, err := normalizeJSONForComparison(spec1.JSON.Raw)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
norm2, err := normalizeJSONForComparison(spec2.JSON.Raw)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare normalized JSON
|
||||
equal := string(norm1) == string(norm2)
|
||||
|
||||
return equal
|
||||
}
|
||||
|
||||
// normalizeJSONForComparison normalizes JSON by sorting arrays and objects
|
||||
func normalizeJSONForComparison(data []byte) ([]byte, error) {
|
||||
var obj interface{}
|
||||
if err := json.Unmarshal(data, &obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Recursively normalize the object
|
||||
normalized := normalizeObject(obj)
|
||||
|
||||
// Re-marshal to get normalized JSON
|
||||
return json.Marshal(normalized)
|
||||
}
|
||||
|
||||
// normalizeObject recursively normalizes objects and arrays
|
||||
func normalizeObject(obj interface{}) interface{} {
|
||||
switch v := obj.(type) {
|
||||
case map[string]interface{}:
|
||||
// For maps, we don't need to sort keys as json.Marshal handles that
|
||||
result := make(map[string]interface{})
|
||||
for k, val := range v {
|
||||
result[k] = normalizeObject(val)
|
||||
}
|
||||
return result
|
||||
case []interface{}:
|
||||
// For arrays, we need to sort them if they contain objects with comparable fields
|
||||
if len(v) == 0 {
|
||||
return v
|
||||
}
|
||||
|
||||
// Check if this is an array of objects that can be sorted
|
||||
if canSortArray(v) {
|
||||
// Sort the array
|
||||
sorted := make([]interface{}, len(v))
|
||||
copy(sorted, v)
|
||||
sortArray(sorted)
|
||||
return sorted
|
||||
}
|
||||
|
||||
// If we can't sort, just normalize each element
|
||||
result := make([]interface{}, len(v))
|
||||
for i, val := range v {
|
||||
result[i] = normalizeObject(val)
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// canSortArray checks if an array can be sorted (contains objects with comparable fields)
|
||||
func canSortArray(arr []interface{}) bool {
|
||||
if len(arr) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if all elements are objects
|
||||
for _, item := range arr {
|
||||
if _, ok := item.(map[string]interface{}); !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check if objects have comparable fields (like "path" for CustomFormsPrefill values)
|
||||
firstObj, ok := arr[0].(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Look for "path" field which is used in CustomFormsPrefill values
|
||||
if _, hasPath := firstObj["path"]; hasPath {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// sortArray sorts an array of objects by their "path" field
|
||||
func sortArray(arr []interface{}) {
|
||||
sort.Slice(arr, func(i, j int) bool {
|
||||
objI, okI := arr[i].(map[string]interface{})
|
||||
objJ, okJ := arr[j].(map[string]interface{})
|
||||
|
||||
if !okI || !okJ {
|
||||
return false
|
||||
}
|
||||
|
||||
pathI, hasPathI := objI["path"]
|
||||
pathJ, hasPathJ := objJ["path"]
|
||||
|
||||
if !hasPathI || !hasPathJ {
|
||||
return false
|
||||
}
|
||||
|
||||
// Convert paths to strings for comparison
|
||||
pathIStr := fmt.Sprintf("%v", pathI)
|
||||
pathJStr := fmt.Sprintf("%v", pathJ)
|
||||
|
||||
return pathIStr < pathJStr
|
||||
})
|
||||
}
|
||||
451
internal/controller/dashboard/manager.go
Normal file
451
internal/controller/dashboard/manager.go
Normal file
@@ -0,0 +1,451 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
dashv1alpha1 "github.com/cozystack/cozystack/api/dashboard/v1alpha1"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
const (
|
||||
// Label keys for dashboard resource management
|
||||
LabelManagedBy = "dashboard.cozystack.io/managed-by"
|
||||
LabelResourceType = "dashboard.cozystack.io/resource-type"
|
||||
LabelCRDName = "dashboard.cozystack.io/crd-name"
|
||||
LabelCRDGroup = "dashboard.cozystack.io/crd-group"
|
||||
LabelCRDVersion = "dashboard.cozystack.io/crd-version"
|
||||
LabelCRDKind = "dashboard.cozystack.io/crd-kind"
|
||||
LabelCRDPlural = "dashboard.cozystack.io/crd-plural"
|
||||
|
||||
// Label values
|
||||
ManagedByValue = "cozystack-dashboard-controller"
|
||||
ResourceTypeStatic = "static"
|
||||
ResourceTypeDynamic = "dynamic"
|
||||
)
|
||||
|
||||
// AddToScheme exposes dashboard types registration for controller setup.
|
||||
func AddToScheme(s *runtime.Scheme) error {
|
||||
return dashv1alpha1.AddToScheme(s)
|
||||
}
|
||||
|
||||
// Manager owns logic for creating/updating dashboard resources derived from CRDs.
|
||||
// It’s easy to extend: add new ensure* methods and wire them into EnsureForCRD.
|
||||
type Manager struct {
|
||||
client client.Client
|
||||
scheme *runtime.Scheme
|
||||
crdListFn func(context.Context) ([]cozyv1alpha1.CozystackResourceDefinition, error)
|
||||
}
|
||||
|
||||
// Option pattern so callers can inject a custom lister.
|
||||
type Option func(*Manager)
|
||||
|
||||
// WithCRDListFunc overrides how Manager lists all CozystackResourceDefinitions.
|
||||
func WithCRDListFunc(fn func(context.Context) ([]cozyv1alpha1.CozystackResourceDefinition, error)) Option {
|
||||
return func(m *Manager) { m.crdListFn = fn }
|
||||
}
|
||||
|
||||
// NewManager constructs a dashboard Manager.
|
||||
func NewManager(c client.Client, scheme *runtime.Scheme, opts ...Option) *Manager {
|
||||
m := &Manager{client: c, scheme: scheme}
|
||||
for _, o := range opts {
|
||||
o(m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// EnsureForCRD is the single entry-point used by the controller.
|
||||
// Add more ensure* calls here as you implement support for other resources:
|
||||
//
|
||||
// - ensureBreadcrumb (implemented)
|
||||
// - ensureCustomColumnsOverride (implemented)
|
||||
// - ensureCustomFormsOverride (implemented)
|
||||
// - ensureCustomFormsPrefill (implemented)
|
||||
// - ensureFactory
|
||||
// - ensureMarketplacePanel (implemented)
|
||||
// - ensureSidebar (implemented)
|
||||
// - ensureTableUriMapping (implemented)
|
||||
func (m *Manager) EnsureForCRD(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) (reconcile.Result, error) {
|
||||
// Early return if crd.Spec.Dashboard is nil to prevent oscillation
|
||||
if crd.Spec.Dashboard == nil {
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
// MarketplacePanel
|
||||
if res, err := m.ensureMarketplacePanel(ctx, crd); err != nil || res.Requeue || res.RequeueAfter > 0 {
|
||||
return res, err
|
||||
}
|
||||
|
||||
// CustomFormsPrefill
|
||||
if res, err := m.ensureCustomFormsPrefill(ctx, crd); err != nil || res.Requeue || res.RequeueAfter > 0 {
|
||||
return res, err
|
||||
}
|
||||
|
||||
// CustomColumnsOverride
|
||||
if _, err := m.ensureCustomColumnsOverride(ctx, crd); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
if err := m.ensureTableUriMapping(ctx, crd); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
if err := m.ensureBreadcrumb(ctx, crd); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
if err := m.ensureCustomFormsOverride(ctx, crd); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
if err := m.ensureSidebar(ctx, crd); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
if err := m.ensureFactory(ctx, crd); err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
// InitializeStaticResources creates all static dashboard resources once during controller startup
|
||||
func (m *Manager) InitializeStaticResources(ctx context.Context) error {
|
||||
return m.ensureStaticResources(ctx)
|
||||
}
|
||||
|
||||
// addDashboardLabels adds standard dashboard management labels to a resource
|
||||
func (m *Manager) addDashboardLabels(obj client.Object, crd *cozyv1alpha1.CozystackResourceDefinition, resourceType string) {
|
||||
labels := obj.GetLabels()
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
|
||||
labels[LabelManagedBy] = ManagedByValue
|
||||
labels[LabelResourceType] = resourceType
|
||||
|
||||
if crd != nil {
|
||||
g, v, kind := pickGVK(crd)
|
||||
plural := pickPlural(kind, crd)
|
||||
|
||||
labels[LabelCRDName] = crd.Name
|
||||
labels[LabelCRDGroup] = g
|
||||
labels[LabelCRDVersion] = v
|
||||
labels[LabelCRDKind] = kind
|
||||
labels[LabelCRDPlural] = plural
|
||||
}
|
||||
|
||||
obj.SetLabels(labels)
|
||||
}
|
||||
|
||||
// getDashboardResourceSelector returns a label selector for dashboard-managed resources
|
||||
func (m *Manager) getDashboardResourceSelector() client.MatchingLabels {
|
||||
return client.MatchingLabels{
|
||||
LabelManagedBy: ManagedByValue,
|
||||
}
|
||||
}
|
||||
|
||||
// getDynamicResourceSelector returns a label selector for dynamic dashboard resources
|
||||
func (m *Manager) getDynamicResourceSelector() client.MatchingLabels {
|
||||
return client.MatchingLabels{
|
||||
LabelManagedBy: ManagedByValue,
|
||||
LabelResourceType: ResourceTypeDynamic,
|
||||
}
|
||||
}
|
||||
|
||||
// getStaticResourceSelector returns a label selector for static dashboard resources
|
||||
func (m *Manager) getStaticResourceSelector() client.MatchingLabels {
|
||||
return client.MatchingLabels{
|
||||
LabelManagedBy: ManagedByValue,
|
||||
LabelResourceType: ResourceTypeStatic,
|
||||
}
|
||||
}
|
||||
|
||||
// CleanupOrphanedResources removes dashboard resources that are no longer needed
|
||||
// This should be called after cache warming to ensure all current resources are known
|
||||
func (m *Manager) CleanupOrphanedResources(ctx context.Context) error {
|
||||
// Get all current CRDs to determine which resources should exist
|
||||
var allCRDs []cozyv1alpha1.CozystackResourceDefinition
|
||||
if m.crdListFn != nil {
|
||||
s, err := m.crdListFn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allCRDs = s
|
||||
} else {
|
||||
var crdList cozyv1alpha1.CozystackResourceDefinitionList
|
||||
if err := m.client.List(ctx, &crdList, &client.ListOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
allCRDs = crdList.Items
|
||||
}
|
||||
|
||||
// Build a set of expected resource names for each type
|
||||
expectedResources := m.buildExpectedResourceSet(allCRDs)
|
||||
|
||||
// Clean up each resource type
|
||||
resourceTypes := []client.Object{
|
||||
&dashv1alpha1.CustomColumnsOverride{},
|
||||
&dashv1alpha1.CustomFormsOverride{},
|
||||
&dashv1alpha1.CustomFormsPrefill{},
|
||||
&dashv1alpha1.MarketplacePanel{},
|
||||
&dashv1alpha1.Sidebar{},
|
||||
&dashv1alpha1.TableUriMapping{},
|
||||
&dashv1alpha1.Breadcrumb{},
|
||||
&dashv1alpha1.Factory{},
|
||||
}
|
||||
|
||||
for _, resourceType := range resourceTypes {
|
||||
if err := m.cleanupResourceType(ctx, resourceType, expectedResources); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildExpectedResourceSet creates a map of expected resource names by type
|
||||
func (m *Manager) buildExpectedResourceSet(crds []cozyv1alpha1.CozystackResourceDefinition) map[string]map[string]bool {
|
||||
expected := make(map[string]map[string]bool)
|
||||
|
||||
// Initialize maps for each resource type
|
||||
resourceTypes := []string{
|
||||
"CustomColumnsOverride",
|
||||
"CustomFormsOverride",
|
||||
"CustomFormsPrefill",
|
||||
"MarketplacePanel",
|
||||
"Sidebar",
|
||||
"TableUriMapping",
|
||||
"Breadcrumb",
|
||||
"Factory",
|
||||
}
|
||||
|
||||
for _, rt := range resourceTypes {
|
||||
expected[rt] = make(map[string]bool)
|
||||
}
|
||||
|
||||
// Add static resources (these should always exist)
|
||||
staticResources := CreateAllStaticResources()
|
||||
for _, resource := range staticResources {
|
||||
resourceType := resource.GetObjectKind().GroupVersionKind().Kind
|
||||
if expected[resourceType] != nil {
|
||||
expected[resourceType][resource.GetName()] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Add dynamic resources based on current CRDs
|
||||
for _, crd := range crds {
|
||||
if crd.Spec.Dashboard == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Note: We include ALL resources with dashboard config, regardless of module flag
|
||||
// because ensureFactory and ensureBreadcrumb create resources for all CRDs with dashboard config
|
||||
|
||||
g, v, kind := pickGVK(&crd)
|
||||
plural := pickPlural(kind, &crd)
|
||||
|
||||
// CustomColumnsOverride - created for ALL CRDs with dashboard config
|
||||
name := fmt.Sprintf("stock-namespace-%s.%s.%s", g, v, plural)
|
||||
expected["CustomColumnsOverride"][name] = true
|
||||
|
||||
// CustomFormsOverride - created for ALL CRDs with dashboard config
|
||||
name = fmt.Sprintf("%s.%s.%s", g, v, plural)
|
||||
expected["CustomFormsOverride"][name] = true
|
||||
|
||||
// CustomFormsPrefill - created for ALL CRDs with dashboard config
|
||||
expected["CustomFormsPrefill"][name] = true
|
||||
|
||||
// MarketplacePanel - only created for non-module CRDs
|
||||
if !crd.Spec.Dashboard.Module {
|
||||
expected["MarketplacePanel"][crd.Name] = true
|
||||
}
|
||||
|
||||
// Sidebar resources - created for ALL CRDs with dashboard config
|
||||
lowerKind := strings.ToLower(kind)
|
||||
detailsID := fmt.Sprintf("stock-project-factory-%s-details", lowerKind)
|
||||
expected["Sidebar"][detailsID] = true
|
||||
|
||||
// Add other stock sidebars that are created for each CRD
|
||||
stockSidebars := []string{
|
||||
"stock-instance-api-form",
|
||||
"stock-instance-api-table",
|
||||
"stock-instance-builtin-form",
|
||||
"stock-instance-builtin-table",
|
||||
"stock-project-factory-marketplace",
|
||||
"stock-project-factory-workloadmonitor-details",
|
||||
"stock-project-api-form",
|
||||
"stock-project-api-table",
|
||||
"stock-project-builtin-form",
|
||||
"stock-project-builtin-table",
|
||||
"stock-project-crd-form",
|
||||
"stock-project-crd-table",
|
||||
}
|
||||
for _, sidebarID := range stockSidebars {
|
||||
expected["Sidebar"][sidebarID] = true
|
||||
}
|
||||
|
||||
// TableUriMapping - created for ALL CRDs with dashboard config
|
||||
name = fmt.Sprintf("stock-namespace-%s.%s.%s", g, v, plural)
|
||||
expected["TableUriMapping"][name] = true
|
||||
|
||||
// Breadcrumb - created for ALL CRDs with dashboard config
|
||||
detailID := fmt.Sprintf("stock-project-factory-%s-details", lowerKind)
|
||||
expected["Breadcrumb"][detailID] = true
|
||||
|
||||
// Factory - created for ALL CRDs with dashboard config
|
||||
factoryName := fmt.Sprintf("%s-details", lowerKind)
|
||||
expected["Factory"][factoryName] = true
|
||||
}
|
||||
|
||||
return expected
|
||||
}
|
||||
|
||||
// cleanupResourceType removes orphaned resources of a specific type
|
||||
func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.Object, expectedResources map[string]map[string]bool) error {
|
||||
var (
|
||||
list client.ObjectList
|
||||
resourceKind string
|
||||
)
|
||||
switch resourceType.(type) {
|
||||
case *dashv1alpha1.CustomColumnsOverride:
|
||||
list = &dashv1alpha1.CustomColumnsOverrideList{}
|
||||
resourceKind = "CustomColumnsOverride"
|
||||
case *dashv1alpha1.CustomFormsOverride:
|
||||
list = &dashv1alpha1.CustomFormsOverrideList{}
|
||||
resourceKind = "CustomFormsOverride"
|
||||
case *dashv1alpha1.CustomFormsPrefill:
|
||||
list = &dashv1alpha1.CustomFormsPrefillList{}
|
||||
resourceKind = "CustomFormsPrefill"
|
||||
case *dashv1alpha1.MarketplacePanel:
|
||||
list = &dashv1alpha1.MarketplacePanelList{}
|
||||
resourceKind = "MarketplacePanel"
|
||||
case *dashv1alpha1.Sidebar:
|
||||
list = &dashv1alpha1.SidebarList{}
|
||||
resourceKind = "Sidebar"
|
||||
case *dashv1alpha1.TableUriMapping:
|
||||
list = &dashv1alpha1.TableUriMappingList{}
|
||||
resourceKind = "TableUriMapping"
|
||||
case *dashv1alpha1.Breadcrumb:
|
||||
list = &dashv1alpha1.BreadcrumbList{}
|
||||
resourceKind = "Breadcrumb"
|
||||
case *dashv1alpha1.Factory:
|
||||
list = &dashv1alpha1.FactoryList{}
|
||||
resourceKind = "Factory"
|
||||
default:
|
||||
return nil // Unknown type
|
||||
}
|
||||
|
||||
expected := expectedResources[resourceKind]
|
||||
if expected == nil {
|
||||
return nil // No expected resources for this type
|
||||
}
|
||||
|
||||
// List with dashboard labels
|
||||
if err := m.client.List(ctx, list, m.getDashboardResourceSelector()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete resources that are not in the expected set
|
||||
switch l := list.(type) {
|
||||
case *dashv1alpha1.CustomColumnsOverrideList:
|
||||
for _, item := range l.Items {
|
||||
if !expected[item.Name] {
|
||||
if err := m.client.Delete(ctx, &item); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
// Resource already deleted, continue
|
||||
}
|
||||
}
|
||||
}
|
||||
case *dashv1alpha1.CustomFormsOverrideList:
|
||||
for _, item := range l.Items {
|
||||
if !expected[item.Name] {
|
||||
if err := m.client.Delete(ctx, &item); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
// Resource already deleted, continue
|
||||
}
|
||||
}
|
||||
}
|
||||
case *dashv1alpha1.CustomFormsPrefillList:
|
||||
for _, item := range l.Items {
|
||||
if !expected[item.Name] {
|
||||
if err := m.client.Delete(ctx, &item); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
// Resource already deleted, continue
|
||||
}
|
||||
}
|
||||
}
|
||||
case *dashv1alpha1.MarketplacePanelList:
|
||||
for _, item := range l.Items {
|
||||
if !expected[item.Name] {
|
||||
if err := m.client.Delete(ctx, &item); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
// Resource already deleted, continue
|
||||
}
|
||||
}
|
||||
}
|
||||
case *dashv1alpha1.SidebarList:
|
||||
for _, item := range l.Items {
|
||||
if !expected[item.Name] {
|
||||
if err := m.client.Delete(ctx, &item); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
// Resource already deleted, continue
|
||||
}
|
||||
}
|
||||
}
|
||||
case *dashv1alpha1.TableUriMappingList:
|
||||
for _, item := range l.Items {
|
||||
if !expected[item.Name] {
|
||||
if err := m.client.Delete(ctx, &item); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
// Resource already deleted, continue
|
||||
}
|
||||
}
|
||||
}
|
||||
case *dashv1alpha1.BreadcrumbList:
|
||||
for _, item := range l.Items {
|
||||
if !expected[item.Name] {
|
||||
logger := log.FromContext(ctx)
|
||||
logger.Info("Deleting orphaned Breadcrumb resource", "name", item.Name)
|
||||
if err := m.client.Delete(ctx, &item); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case *dashv1alpha1.FactoryList:
|
||||
for _, item := range l.Items {
|
||||
if !expected[item.Name] {
|
||||
logger := log.FromContext(ctx)
|
||||
logger.Info("Deleting orphaned Factory resource", "name", item.Name)
|
||||
if err := m.client.Delete(ctx, &item); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
111
internal/controller/dashboard/marketplacepanel.go
Normal file
111
internal/controller/dashboard/marketplacepanel.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
dashv1alpha1 "github.com/cozystack/cozystack/api/dashboard/v1alpha1"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
|
||||
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
// ensureMarketplacePanel creates or updates a MarketplacePanel resource for the given CRD
|
||||
func (m *Manager) ensureMarketplacePanel(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) (reconcile.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
mp := &dashv1alpha1.MarketplacePanel{}
|
||||
mp.Name = crd.Name // cluster-scoped resource, name mirrors CRD name
|
||||
|
||||
// If dashboard is not set, delete the panel if it exists.
|
||||
if crd.Spec.Dashboard == nil {
|
||||
err := m.client.Get(ctx, client.ObjectKey{Name: mp.Name}, mp)
|
||||
if apierrors.IsNotFound(err) {
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
if err := m.client.Delete(ctx, mp); err != nil && !apierrors.IsNotFound(err) {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
logger.Info("Deleted MarketplacePanel because dashboard is not set", "name", mp.Name)
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
// Skip module and tenant resources (they don't need MarketplacePanel)
|
||||
if crd.Spec.Dashboard.Module || crd.Spec.Application.Kind == "Tenant" {
|
||||
err := m.client.Get(ctx, client.ObjectKey{Name: mp.Name}, mp)
|
||||
if apierrors.IsNotFound(err) {
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
if err := m.client.Delete(ctx, mp); err != nil && !apierrors.IsNotFound(err) {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
logger.Info("Deleted MarketplacePanel because resource is a module", "name", mp.Name)
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
// Build desired spec from CRD fields
|
||||
d := crd.Spec.Dashboard
|
||||
app := crd.Spec.Application
|
||||
|
||||
displayName := d.Singular
|
||||
if displayName == "" {
|
||||
displayName = app.Kind
|
||||
}
|
||||
|
||||
tags := make([]any, len(d.Tags))
|
||||
for i, t := range d.Tags {
|
||||
tags[i] = t
|
||||
}
|
||||
|
||||
specMap := map[string]any{
|
||||
"description": d.Description,
|
||||
"name": displayName,
|
||||
"type": "nonCrd",
|
||||
"apiGroup": "apps.cozystack.io",
|
||||
"apiVersion": "v1alpha1",
|
||||
"typeName": app.Plural, // e.g., "buckets"
|
||||
"disabled": false,
|
||||
"hidden": false,
|
||||
"tags": tags,
|
||||
"icon": d.Icon,
|
||||
}
|
||||
|
||||
specBytes, err := json.Marshal(specMap)
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, m.client, mp, func() error {
|
||||
if err := controllerutil.SetOwnerReference(crd, mp, m.scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add dashboard labels to dynamic resources
|
||||
m.addDashboardLabels(mp, crd, ResourceTypeDynamic)
|
||||
|
||||
// Only update spec if it's different to avoid unnecessary updates
|
||||
newSpec := dashv1alpha1.ArbitrarySpec{
|
||||
JSON: apiextv1.JSON{Raw: specBytes},
|
||||
}
|
||||
if !compareArbitrarySpecs(mp.Spec, newSpec) {
|
||||
mp.Spec = newSpec
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
logger.Info("Applied MarketplacePanel", "name", mp.Name)
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
368
internal/controller/dashboard/sidebar.go
Normal file
368
internal/controller/dashboard/sidebar.go
Normal file
@@ -0,0 +1,368 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
dashv1alpha1 "github.com/cozystack/cozystack/api/dashboard/v1alpha1"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
|
||||
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
)
|
||||
|
||||
// ensureSidebar creates/updates multiple Sidebar resources that share the same menu:
|
||||
// - The "details" sidebar tied to the current kind (stock-project-factory-<kind>-details)
|
||||
// - The stock-instance sidebars: api-form, api-table, builtin-form, builtin-table
|
||||
// - The stock-project sidebars: api-form, api-table, builtin-form, builtin-table, crd-form, crd-table
|
||||
//
|
||||
// Menu rules:
|
||||
// - The first section is "Marketplace" with two hardcoded entries:
|
||||
// - Marketplace (/openapi-ui/{clusterName}/{namespace}/factory/marketplace)
|
||||
// - Tenant Info (/openapi-ui/{clusterName}/{namespace}/factory/info-details/info)
|
||||
// - All other sections are built from CRDs where spec.dashboard != nil.
|
||||
// - Categories are ordered strictly as:
|
||||
// Marketplace, IaaS, PaaS, NaaS, <others A→Z>, Resources, Administration
|
||||
// - Items within each category: sort by Weight (desc), then Label (A→Z).
|
||||
func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
// Build the full menu once.
|
||||
|
||||
// 1) Fetch all CRDs
|
||||
var all []cozyv1alpha1.CozystackResourceDefinition
|
||||
if m.crdListFn != nil {
|
||||
s, err := m.crdListFn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
all = s
|
||||
} else {
|
||||
var crdList cozyv1alpha1.CozystackResourceDefinitionList
|
||||
if err := m.client.List(ctx, &crdList, &client.ListOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
all = crdList.Items
|
||||
}
|
||||
|
||||
// 2) Build category -> []item map (only for CRDs with spec.dashboard != nil)
|
||||
type item struct {
|
||||
Key string
|
||||
Label string
|
||||
Link string
|
||||
Weight int
|
||||
}
|
||||
categories := map[string][]item{} // category label -> children
|
||||
keysAndTags := map[string]any{} // plural -> []string{ "<lower(kind)>-sidebar" }
|
||||
|
||||
// Collect sidebar names for module resources
|
||||
var moduleSidebars []any
|
||||
|
||||
for i := range all {
|
||||
def := &all[i]
|
||||
|
||||
// Include ONLY when spec.dashboard != nil
|
||||
if def.Spec.Dashboard == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
g, v, kind := pickGVK(def)
|
||||
plural := pickPlural(kind, def)
|
||||
lowerKind := strings.ToLower(kind)
|
||||
|
||||
// Check if this resource is a module
|
||||
if def.Spec.Dashboard.Module {
|
||||
// Special case: info should have its own keysAndTags, not be in modules
|
||||
if lowerKind == "info" {
|
||||
keysAndTags[plural] = []any{fmt.Sprintf("%s-sidebar", lowerKind)}
|
||||
} else {
|
||||
// Add to modules sidebar list
|
||||
moduleSidebars = append(moduleSidebars, fmt.Sprintf("%s-sidebar", lowerKind))
|
||||
}
|
||||
} else {
|
||||
// Add to keysAndTags for non-module resources
|
||||
keysAndTags[plural] = []any{fmt.Sprintf("%s-sidebar", lowerKind)}
|
||||
}
|
||||
|
||||
// Only add to menu categories if not a module
|
||||
if !def.Spec.Dashboard.Module {
|
||||
cat := safeCategory(def) // falls back to "Resources" if empty
|
||||
|
||||
// Label: prefer dashboard.Plural if provided
|
||||
label := titleFromKindPlural(kind, plural)
|
||||
if def.Spec.Dashboard.Plural != "" {
|
||||
label = def.Spec.Dashboard.Plural
|
||||
}
|
||||
|
||||
// Weight (default 0)
|
||||
weight := def.Spec.Dashboard.Weight
|
||||
|
||||
link := fmt.Sprintf("/openapi-ui/{clusterName}/{namespace}/api-table/%s/%s/%s", g, v, plural)
|
||||
|
||||
categories[cat] = append(categories[cat], item{
|
||||
Key: plural,
|
||||
Label: label,
|
||||
Link: link,
|
||||
Weight: weight,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add modules to keysAndTags if we have any module sidebars
|
||||
if len(moduleSidebars) > 0 {
|
||||
keysAndTags["modules"] = moduleSidebars
|
||||
}
|
||||
|
||||
// Add sidebars for built-in Kubernetes resources
|
||||
keysAndTags["services"] = []any{"service-sidebar"}
|
||||
keysAndTags["secrets"] = []any{"secret-sidebar"}
|
||||
keysAndTags["ingresses"] = []any{"ingress-sidebar"}
|
||||
|
||||
// 3) Sort items within each category by Weight (desc), then Label (A→Z)
|
||||
for cat := range categories {
|
||||
sort.Slice(categories[cat], func(i, j int) bool {
|
||||
if categories[cat][i].Weight != categories[cat][j].Weight {
|
||||
return categories[cat][i].Weight < categories[cat][j].Weight // lower weight first
|
||||
}
|
||||
return strings.ToLower(categories[cat][i].Label) < strings.ToLower(categories[cat][j].Label)
|
||||
})
|
||||
}
|
||||
|
||||
// 4) Order categories strictly:
|
||||
// Marketplace (hardcoded), IaaS, PaaS, NaaS, <others A→Z>, Resources, Administration
|
||||
orderedCats := orderCategoryLabels(categories)
|
||||
|
||||
// 5) Build menuItems (hardcode "Marketplace"; then dynamic categories; then hardcode "Administration")
|
||||
menuItems := []any{
|
||||
map[string]any{
|
||||
"key": "marketplace",
|
||||
"label": "Marketplace",
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"key": "marketplace",
|
||||
"label": "Marketplace",
|
||||
"link": "/openapi-ui/{clusterName}/{namespace}/factory/marketplace",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, cat := range orderedCats {
|
||||
// Skip "Marketplace" and "Administration" here since they're hardcoded
|
||||
if strings.EqualFold(cat, "Marketplace") || strings.EqualFold(cat, "Administration") {
|
||||
continue
|
||||
}
|
||||
children := []any{}
|
||||
for _, it := range categories[cat] {
|
||||
children = append(children, map[string]any{
|
||||
"key": it.Key,
|
||||
"label": it.Label,
|
||||
"link": it.Link,
|
||||
})
|
||||
}
|
||||
if len(children) > 0 {
|
||||
menuItems = append(menuItems, map[string]any{
|
||||
"key": slugify(cat),
|
||||
"label": cat,
|
||||
"children": children,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add hardcoded Administration section
|
||||
menuItems = append(menuItems, map[string]any{
|
||||
"key": "administration",
|
||||
"label": "Administration",
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"key": "info",
|
||||
"label": "Info",
|
||||
"link": "/openapi-ui/{clusterName}/{namespace}/factory/info-details/info",
|
||||
},
|
||||
map[string]any{
|
||||
"key": "modules",
|
||||
"label": "Modules",
|
||||
"link": "/openapi-ui/{clusterName}/{namespace}/api-table/core.cozystack.io/v1alpha1/tenantmodules",
|
||||
},
|
||||
map[string]any{
|
||||
"key": "tenants",
|
||||
"label": "Tenants",
|
||||
"link": "/openapi-ui/{clusterName}/{namespace}/api-table/apps.cozystack.io/v1alpha1/tenants",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// 6) Prepare the list of Sidebar IDs to upsert with the SAME content
|
||||
// Create sidebars for ALL CRDs with dashboard config
|
||||
targetIDs := []string{
|
||||
// stock-instance sidebars
|
||||
"stock-instance-api-form",
|
||||
"stock-instance-api-table",
|
||||
"stock-instance-builtin-form",
|
||||
"stock-instance-builtin-table",
|
||||
|
||||
// stock-project sidebars
|
||||
"stock-project-factory-marketplace",
|
||||
"stock-project-factory-workloadmonitor-details",
|
||||
"stock-project-factory-kube-service-details",
|
||||
"stock-project-factory-kube-secret-details",
|
||||
"stock-project-factory-kube-ingress-details",
|
||||
"stock-project-api-form",
|
||||
"stock-project-api-table",
|
||||
"stock-project-builtin-form",
|
||||
"stock-project-builtin-table",
|
||||
"stock-project-crd-form",
|
||||
"stock-project-crd-table",
|
||||
}
|
||||
|
||||
// Add details sidebars for all CRDs with dashboard config
|
||||
for i := range all {
|
||||
def := &all[i]
|
||||
if def.Spec.Dashboard == nil {
|
||||
continue
|
||||
}
|
||||
_, _, kind := pickGVK(def)
|
||||
lowerKind := strings.ToLower(kind)
|
||||
detailsID := fmt.Sprintf("stock-project-factory-%s-details", lowerKind)
|
||||
targetIDs = append(targetIDs, detailsID)
|
||||
}
|
||||
|
||||
// 7) Upsert all target sidebars with identical menuItems and keysAndTags
|
||||
return m.upsertMultipleSidebars(ctx, crd, targetIDs, keysAndTags, menuItems)
|
||||
}
|
||||
|
||||
// upsertMultipleSidebars creates/updates several Sidebar resources with the same menu spec.
|
||||
func (m *Manager) upsertMultipleSidebars(
|
||||
ctx context.Context,
|
||||
crd *cozyv1alpha1.CozystackResourceDefinition,
|
||||
ids []string,
|
||||
keysAndTags map[string]any,
|
||||
menuItems []any,
|
||||
) error {
|
||||
for _, id := range ids {
|
||||
spec := map[string]any{
|
||||
"id": id,
|
||||
"keysAndTags": keysAndTags,
|
||||
"menuItems": menuItems,
|
||||
}
|
||||
|
||||
obj := &dashv1alpha1.Sidebar{}
|
||||
obj.SetName(id)
|
||||
|
||||
if _, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error {
|
||||
// Only set owner reference for dynamic sidebars (stock-project-factory-{kind}-details)
|
||||
// Static sidebars (stock-instance-*, stock-project-*) should not have owner references
|
||||
if strings.HasPrefix(id, "stock-project-factory-") && strings.HasSuffix(id, "-details") {
|
||||
// This is a dynamic sidebar, set owner reference only if it matches the current CRD
|
||||
_, _, kind := pickGVK(crd)
|
||||
lowerKind := strings.ToLower(kind)
|
||||
expectedID := fmt.Sprintf("stock-project-factory-%s-details", lowerKind)
|
||||
if id == expectedID {
|
||||
if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add dashboard labels to dynamic resources
|
||||
m.addDashboardLabels(obj, crd, ResourceTypeDynamic)
|
||||
} else {
|
||||
// This is a different CRD's sidebar, don't modify owner references or labels
|
||||
// Just update the spec
|
||||
}
|
||||
} else {
|
||||
// This is a static sidebar, don't set owner references
|
||||
// Add static labels
|
||||
labels := obj.GetLabels()
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
labels[LabelManagedBy] = ManagedByValue
|
||||
labels[LabelResourceType] = ResourceTypeStatic
|
||||
obj.SetLabels(labels)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only update spec if it's different to avoid unnecessary updates
|
||||
newSpec := dashv1alpha1.ArbitrarySpec{JSON: apiextv1.JSON{Raw: b}}
|
||||
if !compareArbitrarySpecs(obj.Spec, newSpec) {
|
||||
obj.Spec = newSpec
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// orderCategoryLabels returns category labels ordered strictly as:
|
||||
//
|
||||
// Marketplace, IaaS, PaaS, NaaS, <others A→Z>, Resources, Administration.
|
||||
//
|
||||
// It only returns labels that exist in `cats` (except "Marketplace" which is hardcoded by caller).
|
||||
func orderCategoryLabels[T any](cats map[string][]T) []string {
|
||||
if len(cats) == 0 {
|
||||
return []string{"Marketplace", "IaaS", "PaaS", "NaaS", "Resources", "Administration"}
|
||||
}
|
||||
|
||||
head := []string{"Marketplace", "IaaS", "PaaS", "NaaS"}
|
||||
tail := []string{"Resources", "Administration"}
|
||||
|
||||
present := make(map[string]struct{}, len(cats))
|
||||
for k := range cats {
|
||||
present[k] = struct{}{}
|
||||
}
|
||||
|
||||
var result []string
|
||||
|
||||
// Add head anchors (keep "Marketplace" in the order signature for the caller)
|
||||
for _, h := range head {
|
||||
result = append(result, h)
|
||||
delete(present, h)
|
||||
}
|
||||
|
||||
// Collect "others": exclude tail
|
||||
var others []string
|
||||
for k := range present {
|
||||
if k == "Resources" || k == "Administration" {
|
||||
continue
|
||||
}
|
||||
others = append(others, k)
|
||||
}
|
||||
sort.Slice(others, func(i, j int) bool { return strings.ToLower(others[i]) < strings.ToLower(others[j]) })
|
||||
|
||||
// Append others, then tail (always in fixed order)
|
||||
result = append(result, others...)
|
||||
result = append(result, tail...)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// safeCategory returns spec.dashboard.category or "Resources" if not set.
|
||||
func safeCategory(def *cozyv1alpha1.CozystackResourceDefinition) string {
|
||||
if def == nil || def.Spec.Dashboard == nil {
|
||||
return "Resources"
|
||||
}
|
||||
if def.Spec.Dashboard.Category != "" {
|
||||
return def.Spec.Dashboard.Category
|
||||
}
|
||||
return "Resources"
|
||||
}
|
||||
|
||||
// slugify converts a category label to a key-friendly identifier.
|
||||
// "User Management" -> "usermanagement", "PaaS" -> "paas".
|
||||
func slugify(s string) string {
|
||||
s = strings.TrimSpace(strings.ToLower(s))
|
||||
out := make([]byte, 0, len(s))
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') {
|
||||
out = append(out, c)
|
||||
}
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
1096
internal/controller/dashboard/static_helpers.go
Normal file
1096
internal/controller/dashboard/static_helpers.go
Normal file
File diff suppressed because it is too large
Load Diff
59
internal/controller/dashboard/static_processor.go
Normal file
59
internal/controller/dashboard/static_processor.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
dashv1alpha1 "github.com/cozystack/cozystack/api/dashboard/v1alpha1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
)
|
||||
|
||||
// ensureStaticResources ensures all static dashboard resources are created
|
||||
func (m *Manager) ensureStaticResources(ctx context.Context) error {
|
||||
// Use refactored resources from static_refactored.go
|
||||
// This replaces the old static variables with dynamic creation using helper functions
|
||||
staticResources := CreateAllStaticResources()
|
||||
|
||||
// Create or update each static resource
|
||||
for _, resource := range staticResources {
|
||||
if err := m.ensureStaticResource(ctx, resource); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureStaticResource creates or updates a single static resource
|
||||
func (m *Manager) ensureStaticResource(ctx context.Context, obj client.Object) error {
|
||||
// Create a copy to avoid modifying the original
|
||||
resource := obj.DeepCopyObject().(client.Object)
|
||||
|
||||
// Add dashboard labels to static resources
|
||||
m.addDashboardLabels(resource, nil, ResourceTypeStatic)
|
||||
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, m.client, resource, func() error {
|
||||
// For static resources, we don't need to set owner references
|
||||
// as they are meant to be persistent across CRD changes
|
||||
// Copy Spec from the original object to the live object
|
||||
switch o := obj.(type) {
|
||||
case *dashv1alpha1.CustomColumnsOverride:
|
||||
resource.(*dashv1alpha1.CustomColumnsOverride).Spec = o.Spec
|
||||
case *dashv1alpha1.Breadcrumb:
|
||||
resource.(*dashv1alpha1.Breadcrumb).Spec = o.Spec
|
||||
case *dashv1alpha1.CustomFormsOverride:
|
||||
resource.(*dashv1alpha1.CustomFormsOverride).Spec = o.Spec
|
||||
case *dashv1alpha1.Factory:
|
||||
resource.(*dashv1alpha1.Factory).Spec = o.Spec
|
||||
case *dashv1alpha1.Navigation:
|
||||
resource.(*dashv1alpha1.Navigation).Spec = o.Spec
|
||||
case *dashv1alpha1.TableUriMapping:
|
||||
resource.(*dashv1alpha1.TableUriMapping).Spec = o.Spec
|
||||
}
|
||||
// Ensure labels are always set
|
||||
m.addDashboardLabels(resource, nil, ResourceTypeStatic)
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
1847
internal/controller/dashboard/static_refactored.go
Normal file
1847
internal/controller/dashboard/static_refactored.go
Normal file
File diff suppressed because it is too large
Load Diff
13
internal/controller/dashboard/tableurimapping.go
Normal file
13
internal/controller/dashboard/tableurimapping.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
)
|
||||
|
||||
// ensureTableUriMapping creates or updates a TableUriMapping resource for the given CRD
|
||||
func (m *Manager) ensureTableUriMapping(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
// Links are fully managed by the CustomColumnsOverride.
|
||||
return nil
|
||||
}
|
||||
209
internal/controller/dashboard/ui_helpers.go
Normal file
209
internal/controller/dashboard/ui_helpers.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package dashboard
|
||||
|
||||
import "strings"
|
||||
|
||||
// ---------------- UI helpers (use float64 for numeric fields) ----------------
|
||||
|
||||
func contentCard(id string, style map[string]any, children []any) map[string]any {
|
||||
return contentCardWithTitle(id, "", style, children)
|
||||
}
|
||||
|
||||
func contentCardWithTitle(id any, title string, style map[string]any, children []any) map[string]any {
|
||||
data := map[string]any{
|
||||
"id": id,
|
||||
"style": style,
|
||||
}
|
||||
if title != "" {
|
||||
data["title"] = title
|
||||
}
|
||||
return map[string]any{
|
||||
"type": "ContentCard",
|
||||
"data": data,
|
||||
"children": children,
|
||||
}
|
||||
}
|
||||
|
||||
func antdText(id string, strong bool, text string, style map[string]any) map[string]any {
|
||||
// Auto-generate ID if not provided
|
||||
if id == "" {
|
||||
id = generateTextID("auto", "antd")
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"id": id,
|
||||
"text": text,
|
||||
"strong": strong,
|
||||
}
|
||||
if style != nil {
|
||||
data["style"] = style
|
||||
}
|
||||
return map[string]any{"type": "antdText", "data": data}
|
||||
}
|
||||
|
||||
func parsedText(id, text string, style map[string]any) map[string]any {
|
||||
// Auto-generate ID if not provided
|
||||
if id == "" {
|
||||
id = generateTextID("auto", "parsed")
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"id": id,
|
||||
"text": text,
|
||||
}
|
||||
if style != nil {
|
||||
data["style"] = style
|
||||
}
|
||||
return map[string]any{"type": "parsedText", "data": data}
|
||||
}
|
||||
|
||||
func parsedTextWithFormatter(id, text, formatter string) map[string]any {
|
||||
// Auto-generate ID if not provided
|
||||
if id == "" {
|
||||
id = generateTextID("auto", "formatted")
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"type": "parsedText",
|
||||
"data": map[string]any{
|
||||
"id": id,
|
||||
"text": text,
|
||||
"formatter": formatter,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func spacer(id string, space float64) map[string]any {
|
||||
// Auto-generate ID if not provided
|
||||
if id == "" {
|
||||
id = generateContainerID("auto", "spacer")
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"type": "Spacer",
|
||||
"data": map[string]any{
|
||||
"id": id,
|
||||
"$space": space,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func antdFlex(id string, gap float64, children []any) map[string]any {
|
||||
// Auto-generate ID if not provided
|
||||
if id == "" {
|
||||
id = generateContainerID("auto", "flex")
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"type": "antdFlex",
|
||||
"data": map[string]any{
|
||||
"id": id,
|
||||
"align": "center",
|
||||
"gap": gap,
|
||||
},
|
||||
"children": children,
|
||||
}
|
||||
}
|
||||
|
||||
func antdFlexVertical(id string, gap float64, children []any) map[string]any {
|
||||
// Auto-generate ID if not provided
|
||||
if id == "" {
|
||||
id = generateContainerID("auto", "flex-vertical")
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"type": "antdFlex",
|
||||
"data": map[string]any{
|
||||
"id": id,
|
||||
"vertical": true,
|
||||
"gap": gap,
|
||||
},
|
||||
"children": children,
|
||||
}
|
||||
}
|
||||
|
||||
func antdRow(id string, gutter []any, children []any) map[string]any {
|
||||
// Auto-generate ID if not provided
|
||||
if id == "" {
|
||||
id = generateContainerID("auto", "row")
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"type": "antdRow",
|
||||
"data": map[string]any{
|
||||
"id": id,
|
||||
"gutter": gutter,
|
||||
},
|
||||
"children": children,
|
||||
}
|
||||
}
|
||||
|
||||
func antdCol(id string, span float64, children []any) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "antdCol",
|
||||
"data": map[string]any{
|
||||
"id": id,
|
||||
"span": span,
|
||||
},
|
||||
"children": children,
|
||||
}
|
||||
}
|
||||
|
||||
func antdColWithStyle(id string, style map[string]any, children []any) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "antdCol",
|
||||
"data": map[string]any{
|
||||
"id": id,
|
||||
"style": style,
|
||||
},
|
||||
"children": children,
|
||||
}
|
||||
}
|
||||
|
||||
func antdLink(id, text, href string) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "antdLink",
|
||||
"data": map[string]any{
|
||||
"id": id,
|
||||
"text": text,
|
||||
"href": href,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- Badge helpers ----------------
|
||||
|
||||
// createBadge creates a badge element with the given text, color, and title
|
||||
func createBadge(id, text, color, title string) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": id,
|
||||
"text": text,
|
||||
"title": title,
|
||||
"style": map[string]any{
|
||||
"whiteSpace": "nowrap",
|
||||
"backgroundColor": color,
|
||||
"fontWeight": 400,
|
||||
"lineHeight": "24px",
|
||||
"minWidth": 24,
|
||||
"textAlign": "center",
|
||||
"borderRadius": "20px",
|
||||
"color": "#fff",
|
||||
"display": "inline-block",
|
||||
"fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif",
|
||||
"fontSize": "15px",
|
||||
"padding": "0 9px",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// createBadgeFromKind creates a badge using the existing badge generation functions
|
||||
func createBadgeFromKind(id, kind, title string) map[string]any {
|
||||
return createUnifiedBadgeFromKind(id, kind, title, BadgeSizeMedium)
|
||||
}
|
||||
|
||||
// createHeaderBadge creates a badge specifically for headers with consistent styling
|
||||
func createHeaderBadge(id, kind, plural string) map[string]any {
|
||||
return createUnifiedBadgeFromKind(id, kind, strings.ToLower(plural), BadgeSizeLarge)
|
||||
}
|
||||
407
internal/controller/dashboard/unified_helpers.go
Normal file
407
internal/controller/dashboard/unified_helpers.go
Normal file
@@ -0,0 +1,407 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ---------------- Unified ID generation helpers ----------------
|
||||
|
||||
// generateID creates a unique ID based on the provided components
|
||||
func generateID(components ...string) string {
|
||||
if len(components) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Join components with hyphens and convert to lowercase
|
||||
id := strings.ToLower(strings.Join(components, "-"))
|
||||
|
||||
// Remove any special characters that might cause issues
|
||||
id = strings.ReplaceAll(id, ".", "-")
|
||||
id = strings.ReplaceAll(id, "/", "-")
|
||||
id = strings.ReplaceAll(id, " ", "-")
|
||||
|
||||
// Remove multiple consecutive hyphens
|
||||
for strings.Contains(id, "--") {
|
||||
id = strings.ReplaceAll(id, "--", "-")
|
||||
}
|
||||
|
||||
// Remove leading/trailing hyphens
|
||||
id = strings.Trim(id, "-")
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
// generateSpecID creates a spec.id from metadata.name and other components
|
||||
func generateSpecID(metadataName string, components ...string) string {
|
||||
allComponents := append([]string{metadataName}, components...)
|
||||
return generateID(allComponents...)
|
||||
}
|
||||
|
||||
// generateMetadataName creates metadata.name from spec.id
|
||||
func generateMetadataName(specID string) string {
|
||||
// Convert ID format to metadata.name format
|
||||
// Replace / with . for metadata.name
|
||||
name := strings.ReplaceAll(specID, "/", ".")
|
||||
|
||||
// Clean up the name to be RFC 1123 compliant
|
||||
// Remove any leading/trailing dots and ensure it starts/ends with alphanumeric
|
||||
name = strings.Trim(name, ".")
|
||||
|
||||
// Replace multiple consecutive dots with single dot
|
||||
for strings.Contains(name, "..") {
|
||||
name = strings.ReplaceAll(name, "..", ".")
|
||||
}
|
||||
|
||||
// Replace any remaining problematic patterns
|
||||
// Handle cases like "stock-namespace-.v1" -> "stock-namespace-v1"
|
||||
name = strings.ReplaceAll(name, "-.", "-")
|
||||
name = strings.ReplaceAll(name, ".-", "-")
|
||||
|
||||
// Ensure it starts with alphanumeric character
|
||||
if len(name) > 0 && !isAlphanumeric(name[0]) {
|
||||
name = "a" + name
|
||||
}
|
||||
|
||||
// Ensure it ends with alphanumeric character
|
||||
if len(name) > 0 && !isAlphanumeric(name[len(name)-1]) {
|
||||
name = name + "a"
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// isAlphanumeric checks if a character is alphanumeric
|
||||
func isAlphanumeric(c byte) bool {
|
||||
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')
|
||||
}
|
||||
|
||||
// ---------------- Unified badge generation helpers ----------------
|
||||
|
||||
// BadgeConfig holds configuration for badge generation
|
||||
type BadgeConfig struct {
|
||||
Text string
|
||||
Color string
|
||||
Title string
|
||||
Size BadgeSize
|
||||
}
|
||||
|
||||
// BadgeSize represents the size of the badge
|
||||
type BadgeSize int
|
||||
|
||||
const (
|
||||
BadgeSizeSmall BadgeSize = iota
|
||||
BadgeSizeMedium
|
||||
BadgeSizeLarge
|
||||
)
|
||||
|
||||
// generateBadgeConfig creates a BadgeConfig from kind and optional custom values
|
||||
func generateBadgeConfig(kind string, customText, customColor, customTitle string) BadgeConfig {
|
||||
config := BadgeConfig{
|
||||
Text: initialsFromKind(kind),
|
||||
Color: hexColorForKind(kind),
|
||||
Title: strings.ToLower(kind),
|
||||
Size: BadgeSizeMedium,
|
||||
}
|
||||
|
||||
// Override with custom values if provided
|
||||
if customText != "" {
|
||||
config.Text = customText
|
||||
}
|
||||
if customColor != "" {
|
||||
config.Color = customColor
|
||||
}
|
||||
if customTitle != "" {
|
||||
config.Title = customTitle
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// createUnifiedBadge creates a badge using the unified BadgeConfig
|
||||
func createUnifiedBadge(id string, config BadgeConfig) map[string]any {
|
||||
fontSize := "15px"
|
||||
if config.Size == BadgeSizeLarge {
|
||||
fontSize = "20px"
|
||||
} else if config.Size == BadgeSizeSmall {
|
||||
fontSize = "12px"
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": id,
|
||||
"text": config.Text,
|
||||
"title": config.Title,
|
||||
"style": map[string]any{
|
||||
"backgroundColor": config.Color,
|
||||
"borderRadius": "20px",
|
||||
"color": "#fff",
|
||||
"display": "inline-block",
|
||||
"fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif",
|
||||
"fontSize": fontSize,
|
||||
"fontWeight": float64(400),
|
||||
"lineHeight": "24px",
|
||||
"minWidth": float64(24),
|
||||
"padding": "0 9px",
|
||||
"textAlign": "center",
|
||||
"whiteSpace": "nowrap",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// createUnifiedBadgeFromKind creates a badge from kind with automatic color generation
|
||||
func createUnifiedBadgeFromKind(id, kind, title string, size BadgeSize) map[string]any {
|
||||
config := BadgeConfig{
|
||||
Text: initialsFromKind(kind),
|
||||
Color: hexColorForKind(kind),
|
||||
Title: title,
|
||||
Size: size,
|
||||
}
|
||||
return createUnifiedBadge(id, config)
|
||||
}
|
||||
|
||||
// ---------------- Resource creation helpers with unified approach ----------------
|
||||
|
||||
// ResourceConfig holds configuration for resource creation
|
||||
type ResourceConfig struct {
|
||||
SpecID string
|
||||
MetadataName string
|
||||
Kind string
|
||||
Title string
|
||||
BadgeConfig BadgeConfig
|
||||
}
|
||||
|
||||
// createResourceConfig creates a ResourceConfig from components
|
||||
func createResourceConfig(components []string, kind, title string) ResourceConfig {
|
||||
// Generate spec.id from components
|
||||
specID := generateID(components...)
|
||||
|
||||
// Generate metadata.name from spec.id
|
||||
metadataName := generateMetadataName(specID)
|
||||
|
||||
// Generate badge config
|
||||
badgeConfig := generateBadgeConfig(kind, "", "", title)
|
||||
|
||||
return ResourceConfig{
|
||||
SpecID: specID,
|
||||
MetadataName: metadataName,
|
||||
Kind: kind,
|
||||
Title: title,
|
||||
BadgeConfig: badgeConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- Enhanced color generation ----------------
|
||||
|
||||
// getColorForKind returns a color for a specific kind with improved distribution
|
||||
func getColorForKind(kind string) string {
|
||||
// Use existing hexColorForKind function
|
||||
return hexColorForKind(kind)
|
||||
}
|
||||
|
||||
// getColorForType returns a color for a specific type (like "namespace", "service", etc.)
|
||||
func getColorForType(typeName string) string {
|
||||
// Map common types to specific colors for consistency
|
||||
colorMap := map[string]string{
|
||||
"namespace": "#a25792ff",
|
||||
"service": "#6ca100",
|
||||
"pod": "#009596",
|
||||
"node": "#8476d1",
|
||||
"secret": "#c46100",
|
||||
"configmap": "#b48c78ff",
|
||||
"ingress": "#2e7dff",
|
||||
"workloadmonitor": "#c46100",
|
||||
"module": "#8b5cf6",
|
||||
}
|
||||
|
||||
if color, exists := colorMap[strings.ToLower(typeName)]; exists {
|
||||
return color
|
||||
}
|
||||
|
||||
// Fall back to hash-based color generation
|
||||
return hexColorForKind(typeName)
|
||||
}
|
||||
|
||||
// ---------------- Automatic ID generation for UI elements ----------------
|
||||
|
||||
// generateElementID creates an ID for UI elements based on context and type
|
||||
func generateElementID(elementType, context string, components ...string) string {
|
||||
allComponents := append([]string{elementType, context}, components...)
|
||||
return generateID(allComponents...)
|
||||
}
|
||||
|
||||
// generateBadgeID creates an ID for badge elements
|
||||
func generateBadgeID(context string, kind string) string {
|
||||
return generateElementID("badge", context, kind)
|
||||
}
|
||||
|
||||
// generateLinkID creates an ID for link elements
|
||||
func generateLinkID(context string, linkType string) string {
|
||||
return generateElementID("link", context, linkType)
|
||||
}
|
||||
|
||||
// generateTextID creates an ID for text elements
|
||||
func generateTextID(context string, textType string) string {
|
||||
return generateElementID("text", context, textType)
|
||||
}
|
||||
|
||||
// generateContainerID creates an ID for container elements
|
||||
func generateContainerID(context string, containerType string) string {
|
||||
return generateElementID("container", context, containerType)
|
||||
}
|
||||
|
||||
// generateTableID creates an ID for table elements
|
||||
func generateTableID(context string, tableType string) string {
|
||||
return generateElementID("table", context, tableType)
|
||||
}
|
||||
|
||||
// ---------------- Enhanced resource creation with automatic IDs ----------------
|
||||
|
||||
// createResourceWithAutoID creates a resource with automatically generated IDs
|
||||
func createResourceWithAutoID(resourceType, name string, spec map[string]any) map[string]any {
|
||||
// Generate spec.id from name
|
||||
specID := generateSpecID(name)
|
||||
|
||||
// Add the spec.id to the spec
|
||||
spec["id"] = specID
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
// ---------------- Unified resource creation helpers ----------------
|
||||
|
||||
// UnifiedResourceConfig holds configuration for unified resource creation
|
||||
type UnifiedResourceConfig struct {
|
||||
Name string
|
||||
ResourceType string
|
||||
Kind string
|
||||
Plural string
|
||||
Title string
|
||||
Color string
|
||||
BadgeText string
|
||||
Size BadgeSize
|
||||
}
|
||||
|
||||
// createUnifiedFactory creates a factory using unified approach
|
||||
func createUnifiedFactory(config UnifiedResourceConfig, tabs []any, urlsToFetch []any) map[string]any {
|
||||
// Generate spec.id from name
|
||||
specID := generateSpecID(config.Name)
|
||||
|
||||
// Create header with unified badge
|
||||
badgeConfig := BadgeConfig{
|
||||
Text: config.BadgeText,
|
||||
Color: config.Color,
|
||||
Title: config.Title,
|
||||
Size: config.Size,
|
||||
}
|
||||
if badgeConfig.Text == "" {
|
||||
badgeConfig.Text = initialsFromKind(config.Kind)
|
||||
}
|
||||
if badgeConfig.Color == "" {
|
||||
badgeConfig.Color = getColorForKind(config.Kind)
|
||||
}
|
||||
|
||||
badge := createUnifiedBadge(generateBadgeID("header", config.Kind), badgeConfig)
|
||||
nameText := parsedText(generateTextID("header", "name"), "{reqsJsonPath[0]['.metadata.name']['-']}", map[string]any{
|
||||
"fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif",
|
||||
"fontSize": float64(20),
|
||||
"lineHeight": "24px",
|
||||
})
|
||||
|
||||
header := antdFlex(generateContainerID("header", "row"), float64(6), []any{
|
||||
badge,
|
||||
nameText,
|
||||
})
|
||||
|
||||
// Add marginBottom style to header
|
||||
if headerData, ok := header["data"].(map[string]any); ok {
|
||||
if headerData["style"] == nil {
|
||||
headerData["style"] = map[string]any{}
|
||||
}
|
||||
if style, ok := headerData["style"].(map[string]any); ok {
|
||||
style["marginBottom"] = float64(24)
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"key": config.Name,
|
||||
"id": specID,
|
||||
"sidebarTags": []any{fmt.Sprintf("%s-sidebar", strings.ToLower(config.Kind))},
|
||||
"withScrollableMainContentCard": true,
|
||||
"urlsToFetch": urlsToFetch,
|
||||
"data": []any{
|
||||
header,
|
||||
map[string]any{
|
||||
"type": "antdTabs",
|
||||
"data": map[string]any{
|
||||
"id": generateContainerID("tabs", strings.ToLower(config.Kind)),
|
||||
"defaultActiveKey": "details",
|
||||
"items": tabs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// createUnifiedCustomColumn creates a custom column using unified approach
|
||||
func createUnifiedCustomColumn(name, jsonPath, kind, title, href string) map[string]any {
|
||||
badgeConfig := generateBadgeConfig(kind, "", "", title)
|
||||
badge := createUnifiedBadge(generateBadgeID("column", kind), badgeConfig)
|
||||
|
||||
linkID := generateLinkID("column", "name")
|
||||
if jsonPath == ".metadata.namespace" {
|
||||
linkID = generateLinkID("column", "namespace")
|
||||
}
|
||||
|
||||
link := antdLink(linkID, "{reqsJsonPath[0]['"+jsonPath+"']['-']}", href)
|
||||
|
||||
return map[string]any{
|
||||
"name": name,
|
||||
"type": "factory",
|
||||
"jsonPath": jsonPath,
|
||||
"customProps": map[string]any{
|
||||
"disableEventBubbling": true,
|
||||
"items": []any{
|
||||
map[string]any{
|
||||
"type": "antdFlex",
|
||||
"data": map[string]any{
|
||||
"id": generateContainerID("column", "header"),
|
||||
"align": "center",
|
||||
"gap": float64(6),
|
||||
},
|
||||
"children": []any{badge, link},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- Utility functions ----------------
|
||||
|
||||
// hashString creates a short hash from a string for ID generation
|
||||
func hashString(s string) string {
|
||||
hash := sha1.Sum([]byte(s))
|
||||
return fmt.Sprintf("%x", hash[:4])
|
||||
}
|
||||
|
||||
// sanitizeForID removes characters that shouldn't be in IDs
|
||||
func sanitizeForID(s string) string {
|
||||
// Replace problematic characters
|
||||
s = strings.ReplaceAll(s, ".", "-")
|
||||
s = strings.ReplaceAll(s, "/", "-")
|
||||
s = strings.ReplaceAll(s, " ", "-")
|
||||
s = strings.ReplaceAll(s, "_", "-")
|
||||
|
||||
// Remove multiple consecutive hyphens
|
||||
for strings.Contains(s, "--") {
|
||||
s = strings.ReplaceAll(s, "--", "-")
|
||||
}
|
||||
|
||||
// Remove leading/trailing hyphens
|
||||
s = strings.Trim(s, "-")
|
||||
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
280
internal/controller/kubeovnplunger/kubeovn_plunger.go
Normal file
280
internal/controller/kubeovnplunger/kubeovn_plunger.go
Normal file
@@ -0,0 +1,280 @@
|
||||
package kubeovnplunger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cozystack/cozystack/internal/sse"
|
||||
"github.com/cozystack/cozystack/pkg/ovnstatus"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
)
|
||||
|
||||
var (
|
||||
srv *sse.Server
|
||||
)
|
||||
|
||||
const (
|
||||
rescanInterval = 1 * time.Minute
|
||||
)
|
||||
|
||||
// KubeOVNPlunger watches the ovn-central cluster members
|
||||
type KubeOVNPlunger struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
ClientSet kubernetes.Interface
|
||||
REST *rest.Config
|
||||
Registry prometheus.Registerer
|
||||
metrics metrics
|
||||
lastLeader map[string]string
|
||||
seenCIDs map[string]map[string]struct{}
|
||||
}
|
||||
|
||||
// Reconcile runs the checks on the ovn-central members to see if their views of the cluster are consistent
|
||||
func (r *KubeOVNPlunger) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
deploy := &appsv1.Deployment{}
|
||||
if err := r.Get(ctx, req.NamespacedName, deploy); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
iphints := map[string]string{}
|
||||
for _, env := range deploy.Spec.Template.Spec.Containers[0].Env {
|
||||
if env.Name != "NODE_IPS" {
|
||||
continue
|
||||
}
|
||||
for _, ip := range strings.Split(env.Value, ",") {
|
||||
iphints[ip] = ""
|
||||
}
|
||||
break
|
||||
}
|
||||
if len(iphints) == 0 {
|
||||
l.Info("WARNING: running without IP hints, some error conditions cannot be detected")
|
||||
}
|
||||
pods := &corev1.PodList{}
|
||||
|
||||
if err := r.List(ctx, pods, client.InNamespace(req.Namespace), client.MatchingLabels(map[string]string{"app": req.Name})); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("list ovn-central pods: %w", err)
|
||||
}
|
||||
|
||||
nbmv := make([]ovnstatus.MemberView, 0, len(pods.Items))
|
||||
sbmv := make([]ovnstatus.MemberView, 0, len(pods.Items))
|
||||
nbSnaps := make([]ovnstatus.HealthSnapshot, 0, len(pods.Items))
|
||||
sbSnaps := make([]ovnstatus.HealthSnapshot, 0, len(pods.Items))
|
||||
// TODO: get real iphints
|
||||
for i := range pods.Items {
|
||||
o := ovnstatus.OVNClient{}
|
||||
o.ApplyDefaults()
|
||||
o.Runner = func(ctx context.Context, bin string, args ...string) (string, error) {
|
||||
cmd := append([]string{bin}, args...)
|
||||
eo := ExecOptions{
|
||||
Namespace: req.Namespace,
|
||||
Pod: pods.Items[i].Name,
|
||||
Container: pods.Items[i].Spec.Containers[0].Name,
|
||||
Command: cmd,
|
||||
}
|
||||
res, err := r.ExecPod(ctx, eo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res.Stdout, nil
|
||||
}
|
||||
nb, sb, err1, err2 := o.HealthBoth(ctx)
|
||||
if err1 != nil || err2 != nil {
|
||||
l.Error(fmt.Errorf("health check failed: nb=%w, sb=%w", err1, err2), "pod", pods.Items[i].Name)
|
||||
continue
|
||||
}
|
||||
nbSnaps = append(nbSnaps, nb)
|
||||
sbSnaps = append(sbSnaps, sb)
|
||||
nbmv = append(nbmv, ovnstatus.BuildMemberView(nb))
|
||||
sbmv = append(sbmv, ovnstatus.BuildMemberView(sb))
|
||||
}
|
||||
r.recordAndPruneCIDs("nb", cidFromSnaps(nbSnaps))
|
||||
r.recordAndPruneCIDs("sb", cidFromSnaps(sbSnaps))
|
||||
nbmv = ovnstatus.NormalizeViews(nbmv)
|
||||
sbmv = ovnstatus.NormalizeViews(sbmv)
|
||||
nbecv := ovnstatus.AnalyzeConsensusWithIPHints(nbmv, &ovnstatus.Hints{ExpectedIPs: iphints})
|
||||
sbecv := ovnstatus.AnalyzeConsensusWithIPHints(sbmv, &ovnstatus.Hints{ExpectedIPs: iphints})
|
||||
expected := len(iphints)
|
||||
r.WriteClusterMetrics("nb", nbSnaps, nbecv, expected)
|
||||
r.WriteClusterMetrics("sb", sbSnaps, sbecv, expected)
|
||||
r.WriteMemberMetrics("nb", nbSnaps, nbmv, nbecv)
|
||||
r.WriteMemberMetrics("sb", sbSnaps, sbmv, sbecv)
|
||||
srv.Publish(nbecv.PrettyString() + sbecv.PrettyString())
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// SetupWithManager attaches a generic ticker to trigger a reconcile every <interval> seconds
|
||||
func (r *KubeOVNPlunger) SetupWithManager(mgr ctrl.Manager, kubeOVNNamespace, appName string) error {
|
||||
r.REST = rest.CopyConfig(mgr.GetConfig())
|
||||
cs, err := kubernetes.NewForConfig(r.REST)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build clientset: %w", err)
|
||||
}
|
||||
r.ClientSet = cs
|
||||
ch := make(chan event.GenericEvent, 10)
|
||||
mapFunc := func(context.Context, client.Object) []reconcile.Request {
|
||||
return []reconcile.Request{{
|
||||
NamespacedName: types.NamespacedName{Namespace: kubeOVNNamespace, Name: appName},
|
||||
}}
|
||||
}
|
||||
mapper := handler.EnqueueRequestsFromMapFunc(mapFunc)
|
||||
srv = sse.New(sse.Options{
|
||||
Addr: ":18080",
|
||||
AllowCORS: true,
|
||||
})
|
||||
r.initMetrics()
|
||||
r.lastLeader = make(map[string]string)
|
||||
r.seenCIDs = map[string]map[string]struct{}{"nb": {}, "sb": {}}
|
||||
if err := ctrl.NewControllerManagedBy(mgr).
|
||||
Named("kubeovnplunger").
|
||||
WatchesRawSource(source.Channel(ch, mapper)).
|
||||
Complete(r); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = mgr.Add(manager.RunnableFunc(func(ctx context.Context) error {
|
||||
go srv.ListenAndServe()
|
||||
<-ctx.Done()
|
||||
_ = srv.Shutdown(context.Background())
|
||||
return nil
|
||||
}))
|
||||
return mgr.Add(manager.RunnableFunc(func(ctx context.Context) error {
|
||||
ticker := time.NewTicker(rescanInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
ch <- event.GenericEvent{
|
||||
Object: &metav1.PartialObjectMetadata{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: kubeOVNNamespace,
|
||||
Name: appName,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
type ExecOptions struct {
|
||||
Namespace string
|
||||
Pod string
|
||||
Container string
|
||||
Command []string // e.g. []string{"sh", "-c", "echo hi"}
|
||||
Stdin io.Reader // optional
|
||||
TTY bool // if true, stderr is merged into stdout
|
||||
Timeout time.Duration // optional overall timeout
|
||||
}
|
||||
|
||||
type ExecResult struct {
|
||||
Stdout string
|
||||
Stderr string
|
||||
ExitCode *int // nil if not determinable
|
||||
}
|
||||
|
||||
// ExecPod runs a command in a pod and returns stdout/stderr/exit code.
|
||||
func (r *KubeOVNPlunger) ExecPod(ctx context.Context, opts ExecOptions) (*ExecResult, error) {
|
||||
if opts.Namespace == "" || opts.Pod == "" || opts.Container == "" {
|
||||
return nil, fmt.Errorf("namespace, pod, and container are required")
|
||||
}
|
||||
|
||||
req := r.ClientSet.CoreV1().RESTClient().
|
||||
Post().
|
||||
Resource("pods").
|
||||
Namespace(opts.Namespace).
|
||||
Name(opts.Pod).
|
||||
SubResource("exec").
|
||||
VersionedParams(&corev1.PodExecOptions{
|
||||
Container: opts.Container,
|
||||
Command: opts.Command,
|
||||
Stdin: opts.Stdin != nil,
|
||||
Stdout: true,
|
||||
Stderr: !opts.TTY,
|
||||
TTY: opts.TTY,
|
||||
}, scheme.ParameterCodec)
|
||||
|
||||
exec, err := remotecommand.NewSPDYExecutor(r.REST, "POST", req.URL())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("spdy executor: %w", err)
|
||||
}
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
streamCtx := ctx
|
||||
if opts.Timeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
streamCtx, cancel = context.WithTimeout(ctx, opts.Timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
streamErr := exec.StreamWithContext(streamCtx, remotecommand.StreamOptions{
|
||||
Stdin: opts.Stdin,
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderr,
|
||||
Tty: opts.TTY,
|
||||
})
|
||||
|
||||
res := &ExecResult{Stdout: stdout.String(), Stderr: stderr.String()}
|
||||
if streamErr != nil {
|
||||
// Try to surface exit code instead of treating all failures as transport errors
|
||||
type exitCoder interface{ ExitStatus() int }
|
||||
if ec, ok := streamErr.(exitCoder); ok {
|
||||
code := ec.ExitStatus()
|
||||
res.ExitCode = &code
|
||||
return res, nil
|
||||
}
|
||||
return res, fmt.Errorf("exec stream: %w", streamErr)
|
||||
}
|
||||
zero := 0
|
||||
res.ExitCode = &zero
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *KubeOVNPlunger) recordAndPruneCIDs(db, currentCID string) {
|
||||
|
||||
// Mark current as seen
|
||||
if r.seenCIDs[db] == nil {
|
||||
r.seenCIDs[db] = map[string]struct{}{}
|
||||
}
|
||||
if currentCID != "" {
|
||||
r.seenCIDs[db][currentCID] = struct{}{}
|
||||
}
|
||||
|
||||
// Build a set of "still active" CIDs this cycle (could be none if you failed to collect)
|
||||
active := map[string]struct{}{}
|
||||
if currentCID != "" {
|
||||
active[currentCID] = struct{}{}
|
||||
}
|
||||
|
||||
// Any seen CID that isn't active now is stale -> delete all its series
|
||||
for cid := range r.seenCIDs[db] {
|
||||
if _, ok := active[cid]; ok {
|
||||
continue
|
||||
}
|
||||
r.deleteAllFor(db, cid)
|
||||
delete(r.seenCIDs[db], cid)
|
||||
}
|
||||
}
|
||||
34
internal/controller/kubeovnplunger/kubeovn_plunger_test.go
Normal file
34
internal/controller/kubeovnplunger/kubeovn_plunger_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package kubeovnplunger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
)
|
||||
|
||||
var testPlunger *KubeOVNPlunger
|
||||
|
||||
func init() {
|
||||
scheme := runtime.NewScheme()
|
||||
cfg := config.GetConfigOrDie()
|
||||
c, _ := client.New(cfg, client.Options{})
|
||||
cs, _ := kubernetes.NewForConfig(cfg)
|
||||
testPlunger = &KubeOVNPlunger{
|
||||
Client: c,
|
||||
Scheme: scheme,
|
||||
ClientSet: cs,
|
||||
REST: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlungerGetsStatuses(t *testing.T) {
|
||||
_, err := testPlunger.Reconcile(context.Background(), ctrl.Request{})
|
||||
if err != nil {
|
||||
t.Errorf("error should be nil but it's %s", err)
|
||||
}
|
||||
}
|
||||
423
internal/controller/kubeovnplunger/metrics.go
Normal file
423
internal/controller/kubeovnplunger/metrics.go
Normal file
@@ -0,0 +1,423 @@
|
||||
package kubeovnplunger
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/cozystack/cozystack/pkg/ovnstatus"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
type metrics struct {
|
||||
// --- Core cluster health (per DB/cid) ---
|
||||
clusterQuorum *prometheus.GaugeVec // 1/0
|
||||
allAgree *prometheus.GaugeVec // 1/0
|
||||
membersExpected *prometheus.GaugeVec
|
||||
membersObserved *prometheus.GaugeVec
|
||||
ipsExpected *prometheus.GaugeVec
|
||||
ipsObserved *prometheus.GaugeVec
|
||||
excessMembers *prometheus.GaugeVec
|
||||
missingMembers *prometheus.GaugeVec
|
||||
unexpectedIPsCount *prometheus.GaugeVec
|
||||
missingExpectedIPsCount *prometheus.GaugeVec
|
||||
ipConflictsCount *prometheus.GaugeVec
|
||||
sidAddrDisagreements *prometheus.GaugeVec
|
||||
|
||||
// --- Consensus summary (per DB/cid) ---
|
||||
consensusMajoritySize *prometheus.GaugeVec
|
||||
consensusMinoritySize *prometheus.GaugeVec
|
||||
consensusDiffsTotal *prometheus.GaugeVec
|
||||
|
||||
// --- Detail exports (sparse, keyed by IP/SID) ---
|
||||
unexpectedIPGauge *prometheus.GaugeVec // {db,cid,ip} -> 1
|
||||
missingExpectedIPGauge *prometheus.GaugeVec // {db,cid,ip} -> 1
|
||||
ipConflictGauge *prometheus.GaugeVec // {db,cid,ip} -> count(sids)
|
||||
suspectStaleGauge *prometheus.GaugeVec // {db,cid,sid} -> 1
|
||||
|
||||
// --- Per-member liveness/freshness (per DB/cid/sid[/ip]) ---
|
||||
memberConnected *prometheus.GaugeVec // {db,cid,sid,ip}
|
||||
memberLeader *prometheus.GaugeVec // {db,cid,sid}
|
||||
memberLastMsgMs *prometheus.GaugeVec // {db,cid,sid}
|
||||
memberIndex *prometheus.GaugeVec // {db,cid,sid}
|
||||
memberIndexGap *prometheus.GaugeVec // {db,cid,sid}
|
||||
memberReporter *prometheus.GaugeVec // {db,cid,sid}
|
||||
memberMissingReporter *prometheus.GaugeVec // {db,cid,sid}
|
||||
|
||||
// --- Ops/housekeeping ---
|
||||
leaderTransitionsTotal *prometheus.CounterVec // {db,cid}
|
||||
collectErrorsTotal *prometheus.CounterVec // {db,cid}
|
||||
publishEventsTotal *prometheus.CounterVec // {db,cid}
|
||||
snapshotTimestampSec *prometheus.GaugeVec // {db,cid}
|
||||
}
|
||||
|
||||
func (r *KubeOVNPlunger) initMetrics() {
|
||||
p := promauto.With(r.Registry)
|
||||
|
||||
ns := "ovn"
|
||||
|
||||
// --- Core cluster health ---
|
||||
r.metrics.clusterQuorum = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "cluster", Name: "quorum",
|
||||
Help: "1 if cluster has quorum, else 0",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
r.metrics.allAgree = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "cluster", Name: "all_agree",
|
||||
Help: "1 if all members report identical membership",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
r.metrics.membersExpected = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "cluster", Name: "members_expected",
|
||||
Help: "Expected cluster size (replicas)",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
r.metrics.membersObserved = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "cluster", Name: "members_observed",
|
||||
Help: "Observed members (distinct SIDs across views)",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
r.metrics.ipsExpected = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "cluster", Name: "ips_expected",
|
||||
Help: "Expected distinct member IPs (from k8s hints)",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
r.metrics.ipsObserved = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "cluster", Name: "ips_observed",
|
||||
Help: "Observed distinct member IPs (from OVN views)",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
r.metrics.excessMembers = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "cluster", Name: "excess_members",
|
||||
Help: "Members over expected (>=0)",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
r.metrics.missingMembers = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "cluster", Name: "missing_members",
|
||||
Help: "Members short of expected (>=0)",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
r.metrics.unexpectedIPsCount = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "cluster", Name: "unexpected_ips",
|
||||
Help: "Count of IPs in OVN not present in k8s expected set",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
r.metrics.missingExpectedIPsCount = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "cluster", Name: "missing_expected_ips",
|
||||
Help: "Count of expected IPs not found in OVN",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
r.metrics.ipConflictsCount = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "cluster", Name: "ip_conflicts",
|
||||
Help: "Number of IPs claimed by multiple SIDs",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
r.metrics.sidAddrDisagreements = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "cluster", Name: "sid_address_disagreements",
|
||||
Help: "Number of SIDs seen with >1 distinct addresses",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
// --- Consensus summary ---
|
||||
r.metrics.consensusMajoritySize = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "consensus", Name: "majority_size",
|
||||
Help: "Majority group size (0 if none)",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
r.metrics.consensusMinoritySize = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "consensus", Name: "minority_size",
|
||||
Help: "Minority group size",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
r.metrics.consensusDiffsTotal = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "consensus", Name: "diffs_total",
|
||||
Help: "Total per-reporter differences vs truth (missing + extra + mismatches)",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
// --- Detail exports (sparse) ---
|
||||
r.metrics.unexpectedIPGauge = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "consensus", Name: "unexpected_ip",
|
||||
Help: "Unexpected IP present in OVN; value fixed at 1",
|
||||
}, []string{"db", "cid", "ip"})
|
||||
|
||||
r.metrics.missingExpectedIPGauge = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "consensus", Name: "missing_expected_ip",
|
||||
Help: "Expected IP missing from OVN; value fixed at 1",
|
||||
}, []string{"db", "cid", "ip"})
|
||||
|
||||
r.metrics.ipConflictGauge = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "consensus", Name: "ip_conflict",
|
||||
Help: "Number of SIDs claiming the same IP for this key",
|
||||
}, []string{"db", "cid", "ip"})
|
||||
|
||||
r.metrics.suspectStaleGauge = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "consensus", Name: "suspect_stale",
|
||||
Help: "Suspected stale SID candidate for kick; value fixed at 1 (emit only when remediation is warranted)",
|
||||
}, []string{"db", "cid", "sid"})
|
||||
|
||||
// --- Per-member liveness/freshness ---
|
||||
r.metrics.memberConnected = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "member", Name: "connected",
|
||||
Help: "1 if local server reports connected/quorum, else 0",
|
||||
}, []string{"db", "cid", "sid", "ip"})
|
||||
|
||||
r.metrics.memberLeader = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "member", Name: "leader",
|
||||
Help: "1 if this member is leader, else 0",
|
||||
}, []string{"db", "cid", "sid"})
|
||||
|
||||
r.metrics.memberLastMsgMs = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "member", Name: "last_msg_ms",
|
||||
Help: "Follower->leader 'last msg' age in ms (legacy heuristic). NaN/omit if unknown",
|
||||
}, []string{"db", "cid", "sid"})
|
||||
|
||||
r.metrics.memberIndex = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "member", Name: "index",
|
||||
Help: "Local Raft log index",
|
||||
}, []string{"db", "cid", "sid"})
|
||||
|
||||
r.metrics.memberIndexGap = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "member", Name: "index_gap",
|
||||
Help: "Leader index minus local index (>=0)",
|
||||
}, []string{"db", "cid", "sid"})
|
||||
|
||||
r.metrics.memberReporter = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "member", Name: "reporter",
|
||||
Help: "1 if a self-view from this SID was collected in the scrape cycle",
|
||||
}, []string{"db", "cid", "sid"})
|
||||
|
||||
r.metrics.memberMissingReporter = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "member", Name: "missing_reporter",
|
||||
Help: "1 if SID appears in union but produced no self-view",
|
||||
}, []string{"db", "cid", "sid"})
|
||||
|
||||
// --- Ops/housekeeping ---
|
||||
r.metrics.leaderTransitionsTotal = p.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: ns, Subsystem: "ops", Name: "leader_transitions_total",
|
||||
Help: "Count of observed leader SID changes",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
r.metrics.collectErrorsTotal = p.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: ns, Subsystem: "ops", Name: "collect_errors_total",
|
||||
Help: "Count of errors during health collection/analysis",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
r.metrics.publishEventsTotal = p.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: ns, Subsystem: "ops", Name: "publish_events_total",
|
||||
Help: "Count of SSE publish events (optional)",
|
||||
}, []string{"db", "cid"})
|
||||
|
||||
r.metrics.snapshotTimestampSec = p.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: ns, Subsystem: "ops", Name: "snapshot_timestamp_seconds",
|
||||
Help: "Unix timestamp of the last successful consensus snapshot",
|
||||
}, []string{"db", "cid"})
|
||||
}
|
||||
|
||||
func (r *KubeOVNPlunger) WriteClusterMetrics(db string, snaps []ovnstatus.HealthSnapshot, ecv ovnstatus.ExtendedConsensusResult, expectedReplicas int) {
|
||||
cid := cidFromSnaps(snaps)
|
||||
|
||||
// Core cluster health
|
||||
r.metrics.clusterQuorum.WithLabelValues(db, cid).Set(b2f(ecv.HasMajority))
|
||||
r.metrics.allAgree.WithLabelValues(db, cid).Set(b2f(ecv.AllAgree))
|
||||
r.metrics.membersExpected.WithLabelValues(db, cid).Set(float64(expectedReplicas))
|
||||
r.metrics.membersObserved.WithLabelValues(db, cid).Set(float64(ecv.MembersCount))
|
||||
r.metrics.ipsExpected.WithLabelValues(db, cid).Set(float64(len(ecv.ConsensusResult.TruthView.Members))) // optional; or len(hints.ExpectedIPs)
|
||||
r.metrics.ipsObserved.WithLabelValues(db, cid).Set(float64(ecv.DistinctIPCount))
|
||||
r.metrics.excessMembers.WithLabelValues(db, cid).Set(float64(ecv.ExpectedExcess))
|
||||
r.metrics.missingMembers.WithLabelValues(db, cid).Set(float64(ecv.ExpectedShortfall))
|
||||
r.metrics.unexpectedIPsCount.WithLabelValues(db, cid).Set(float64(len(ecv.UnexpectedIPs)))
|
||||
r.metrics.missingExpectedIPsCount.WithLabelValues(db, cid).Set(float64(len(ecv.MissingExpectedIPs)))
|
||||
r.metrics.ipConflictsCount.WithLabelValues(db, cid).Set(float64(len(ecv.IPConflicts)))
|
||||
|
||||
// Count SIDs with >1 distinct addresses
|
||||
disagree := 0
|
||||
for _, n := range ecv.SIDAddressDisagreements {
|
||||
if n > 1 {
|
||||
disagree++
|
||||
}
|
||||
}
|
||||
r.metrics.sidAddrDisagreements.WithLabelValues(db, cid).Set(float64(disagree))
|
||||
|
||||
// Consensus summary
|
||||
r.metrics.consensusMajoritySize.WithLabelValues(db, cid).Set(float64(len(ecv.MajorityMembers)))
|
||||
r.metrics.consensusMinoritySize.WithLabelValues(db, cid).Set(float64(len(ecv.MinorityMembers)))
|
||||
|
||||
// Sum diffs across reporters (missing + extra + mismatches)
|
||||
totalDiffs := 0
|
||||
for _, d := range ecv.Diffs {
|
||||
totalDiffs += len(d.MissingSIDs) + len(d.ExtraSIDs) + len(d.AddressMismatches)
|
||||
}
|
||||
r.metrics.consensusDiffsTotal.WithLabelValues(db, cid).Set(float64(totalDiffs))
|
||||
|
||||
// Sparse per-key exports (reset then re-emit for this {db,cid})
|
||||
r.metrics.unexpectedIPGauge.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
for _, ip := range ecv.UnexpectedIPs {
|
||||
r.metrics.unexpectedIPGauge.WithLabelValues(db, cid, ip).Set(1)
|
||||
}
|
||||
|
||||
r.metrics.missingExpectedIPGauge.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
for _, ip := range ecv.MissingExpectedIPs {
|
||||
r.metrics.missingExpectedIPGauge.WithLabelValues(db, cid, ip).Set(1)
|
||||
}
|
||||
|
||||
r.metrics.ipConflictGauge.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
for ip, sids := range ecv.IPConflicts {
|
||||
r.metrics.ipConflictGauge.WithLabelValues(db, cid, ip).Set(float64(len(sids)))
|
||||
}
|
||||
|
||||
// Only emit suspects when remediation is warranted (e.g., TooManyMembers / unexpected IPs / conflicts)
|
||||
r.metrics.suspectStaleGauge.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
if ecv.TooManyMembers || len(ecv.UnexpectedIPs) > 0 || len(ecv.IPConflicts) > 0 {
|
||||
for _, sid := range ecv.SuspectStaleSIDs {
|
||||
r.metrics.suspectStaleGauge.WithLabelValues(db, cid, sid).Set(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot timestamp
|
||||
r.metrics.snapshotTimestampSec.WithLabelValues(db, cid).Set(float64(time.Now().Unix()))
|
||||
}
|
||||
|
||||
func (r *KubeOVNPlunger) WriteMemberMetrics(db string, snaps []ovnstatus.HealthSnapshot, views []ovnstatus.MemberView, ecv ovnstatus.ExtendedConsensusResult) {
|
||||
cid := cidFromSnaps(snaps)
|
||||
|
||||
// Figure out current leader SID (prefer local view from any leader snapshot)
|
||||
curLeader := ""
|
||||
for _, s := range snaps {
|
||||
if s.Local.Leader {
|
||||
curLeader = s.Local.SID
|
||||
break
|
||||
}
|
||||
}
|
||||
// Leader transitions
|
||||
key := db + "|" + cid
|
||||
if prev, ok := r.lastLeader[key]; ok && prev != "" && curLeader != "" && prev != curLeader {
|
||||
r.metrics.leaderTransitionsTotal.WithLabelValues(db, cid).Inc()
|
||||
}
|
||||
if curLeader != "" {
|
||||
r.lastLeader[key] = curLeader
|
||||
}
|
||||
|
||||
// Build quick maps for reporter set & IP per SID (best-effort)
|
||||
reporter := map[string]struct{}{}
|
||||
for _, v := range views {
|
||||
if v.FromSID != "" {
|
||||
reporter[v.FromSID] = struct{}{}
|
||||
}
|
||||
}
|
||||
sidToIP := map[string]string{}
|
||||
for _, v := range views {
|
||||
for sid, addr := range v.Members {
|
||||
if sidToIP[sid] == "" && addr != "" {
|
||||
sidToIP[sid] = ovnstatus.AddrToIP(addr) // expose addrToIP or wrap here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset member vectors for this {db,cid} (avoid stale series)
|
||||
r.metrics.memberConnected.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.memberLeader.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.memberLastMsgMs.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.memberIndex.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.memberIndexGap.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.memberReporter.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.memberMissingReporter.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
|
||||
// Leader index (to compute gaps)
|
||||
lIdx := leaderIndex(snaps, curLeader)
|
||||
|
||||
// Emit one series per snapshot (self view)
|
||||
for _, s := range snaps {
|
||||
sid := s.Local.SID
|
||||
ip := sidToIP[sid]
|
||||
if ip == "" {
|
||||
ip = "unknown"
|
||||
}
|
||||
|
||||
r.metrics.memberConnected.WithLabelValues(db, cid, sid, ip).Set(b2f(s.Local.Connected))
|
||||
r.metrics.memberLeader.WithLabelValues(db, cid, sid).Set(b2f(s.Local.Leader))
|
||||
r.metrics.memberIndex.WithLabelValues(db, cid, sid).Set(float64(s.Local.Index))
|
||||
|
||||
if lIdx != nil && s.Local.Index >= 0 {
|
||||
gap := *lIdx - s.Local.Index
|
||||
if gap < 0 {
|
||||
gap = 0
|
||||
}
|
||||
r.metrics.memberIndexGap.WithLabelValues(db, cid, sid).Set(float64(gap))
|
||||
}
|
||||
|
||||
// Reporter presence
|
||||
_, isReporter := reporter[sid]
|
||||
r.metrics.memberReporter.WithLabelValues(db, cid, sid).Set(b2f(isReporter))
|
||||
}
|
||||
|
||||
// “Missing reporter” SIDs = union − reporters (from ecv)
|
||||
reporterSet := map[string]struct{}{}
|
||||
for sid := range reporter {
|
||||
reporterSet[sid] = struct{}{}
|
||||
}
|
||||
unionSet := map[string]struct{}{}
|
||||
for _, sid := range ecv.UnionMembers {
|
||||
unionSet[sid] = struct{}{}
|
||||
}
|
||||
for sid := range unionSet {
|
||||
if _, ok := reporterSet[sid]; !ok {
|
||||
r.metrics.memberMissingReporter.WithLabelValues(db, cid, sid).Set(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy follower freshness (if you kept LastMsgMs in servers parsing)
|
||||
// We only know LastMsgMs from the Full.Servers in each snapshot; pick the freshest per SID.
|
||||
lastMsg := map[string]int64{}
|
||||
for _, s := range snaps {
|
||||
for _, srv := range s.Full.Servers {
|
||||
if srv.LastMsgMs != nil {
|
||||
cur, ok := lastMsg[srv.SID]
|
||||
if !ok || *srv.LastMsgMs < cur {
|
||||
lastMsg[srv.SID] = *srv.LastMsgMs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for sid, ms := range lastMsg {
|
||||
r.metrics.memberLastMsgMs.WithLabelValues(db, cid, sid).Set(float64(ms))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *KubeOVNPlunger) deleteAllFor(db, cid string) {
|
||||
// Cluster-level vecs (db,cid)
|
||||
r.metrics.clusterQuorum.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.allAgree.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.membersExpected.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.membersObserved.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.ipsExpected.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.ipsObserved.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.excessMembers.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.missingMembers.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.unexpectedIPsCount.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.missingExpectedIPsCount.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.ipConflictsCount.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.sidAddrDisagreements.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
|
||||
r.metrics.consensusMajoritySize.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.consensusMinoritySize.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.consensusDiffsTotal.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
|
||||
// Sparse detail vecs (db,cid,*)
|
||||
r.metrics.unexpectedIPGauge.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.missingExpectedIPGauge.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.ipConflictGauge.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.suspectStaleGauge.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
|
||||
// Per-member vecs (db,cid,*)
|
||||
r.metrics.memberConnected.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.memberLeader.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.memberLastMsgMs.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.memberIndex.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.memberIndexGap.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.memberReporter.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.memberMissingReporter.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
|
||||
// Ops vecs (db,cid)
|
||||
r.metrics.leaderTransitionsTotal.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.collectErrorsTotal.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.publishEventsTotal.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
r.metrics.snapshotTimestampSec.DeletePartialMatch(prometheus.Labels{"db": db, "cid": cid})
|
||||
}
|
||||
31
internal/controller/kubeovnplunger/util.go
Normal file
31
internal/controller/kubeovnplunger/util.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package kubeovnplunger
|
||||
|
||||
import "github.com/cozystack/cozystack/pkg/ovnstatus"
|
||||
|
||||
func b2f(b bool) float64 {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Pull a cluster UUID (cid) from any snapshots’ Local.CID (falls back to "")
|
||||
func cidFromSnaps(snaps []ovnstatus.HealthSnapshot) string {
|
||||
for _, s := range snaps {
|
||||
if s.Local.CID != "" {
|
||||
return s.Local.CID
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Map SID -> last local index to compute gaps (optional)
|
||||
func leaderIndex(snaps []ovnstatus.HealthSnapshot, leaderSID string) (idx *int64) {
|
||||
for _, s := range snaps {
|
||||
if s.Local.SID == leaderSID && s.Local.Index > 0 {
|
||||
v := s.Local.Index
|
||||
return &v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -33,6 +33,7 @@ const requestedAt = "reconcile.fluxcd.io/requestedAt"
|
||||
|
||||
func (r *CozystackConfigReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) {
|
||||
log := log.FromContext(ctx)
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
digest, err := r.computeDigest(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -26,6 +26,7 @@ type TenantHelmReconciler struct {
|
||||
|
||||
func (r *TenantHelmReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
hr := &helmv2.HelmRelease{}
|
||||
if err := r.Get(ctx, req.NamespacedName, hr); err != nil {
|
||||
|
||||
47
internal/lineagecontrollerwebhook/config.go
Normal file
47
internal/lineagecontrollerwebhook/config.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package lineagecontrollerwebhook
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
)
|
||||
|
||||
type chartRef struct {
|
||||
repo string
|
||||
chart string
|
||||
}
|
||||
|
||||
type appRef struct {
|
||||
group string
|
||||
kind string
|
||||
}
|
||||
|
||||
type runtimeConfig struct {
|
||||
chartAppMap map[chartRef]*cozyv1alpha1.CozystackResourceDefinition
|
||||
appCRDMap map[appRef]*cozyv1alpha1.CozystackResourceDefinition
|
||||
}
|
||||
|
||||
func (l *LineageControllerWebhook) initConfig() {
|
||||
l.initOnce.Do(func() {
|
||||
if l.config.Load() == nil {
|
||||
l.config.Store(&runtimeConfig{
|
||||
chartAppMap: make(map[chartRef]*cozyv1alpha1.CozystackResourceDefinition),
|
||||
appCRDMap: make(map[appRef]*cozyv1alpha1.CozystackResourceDefinition),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (l *LineageControllerWebhook) Map(hr *helmv2.HelmRelease) (string, string, string, error) {
|
||||
cfg, ok := l.config.Load().(*runtimeConfig)
|
||||
if !ok {
|
||||
return "", "", "", fmt.Errorf("failed to load chart-app mapping from config")
|
||||
}
|
||||
s := hr.Spec.Chart.Spec
|
||||
val, ok := cfg.chartAppMap[chartRef{s.SourceRef.Name, s.Chart}]
|
||||
if !ok {
|
||||
return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app", hr.Namespace, hr.Name)
|
||||
}
|
||||
return "apps.cozystack.io/v1alpha1", val.Spec.Application.Kind, val.Spec.Release.Prefix, nil
|
||||
}
|
||||
54
internal/lineagecontrollerwebhook/controller.go
Normal file
54
internal/lineagecontrollerwebhook/controller.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package lineagecontrollerwebhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=cozystackresourcedefinitions,verbs=list;watch;get
|
||||
|
||||
func (c *LineageControllerWebhook) SetupWithManagerAsController(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&cozyv1alpha1.CozystackResourceDefinition{}).
|
||||
Complete(c)
|
||||
}
|
||||
|
||||
func (c *LineageControllerWebhook) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
l := log.FromContext(ctx)
|
||||
crds := &cozyv1alpha1.CozystackResourceDefinitionList{}
|
||||
if err := c.List(ctx, crds); err != nil {
|
||||
l.Error(err, "failed reading CozystackResourceDefinitions")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
cfg := &runtimeConfig{
|
||||
chartAppMap: make(map[chartRef]*cozyv1alpha1.CozystackResourceDefinition),
|
||||
appCRDMap: make(map[appRef]*cozyv1alpha1.CozystackResourceDefinition),
|
||||
}
|
||||
for _, crd := range crds.Items {
|
||||
chRef := chartRef{
|
||||
crd.Spec.Release.Chart.SourceRef.Name,
|
||||
crd.Spec.Release.Chart.Name,
|
||||
}
|
||||
appRef := appRef{
|
||||
"apps.cozystack.io",
|
||||
crd.Spec.Application.Kind,
|
||||
}
|
||||
|
||||
newRef := crd
|
||||
if _, exists := cfg.chartAppMap[chRef]; exists {
|
||||
l.Info("duplicate chart mapping detected; ignoring subsequent entry", "key", chRef)
|
||||
} else {
|
||||
cfg.chartAppMap[chRef] = &newRef
|
||||
}
|
||||
if _, exists := cfg.appCRDMap[appRef]; exists {
|
||||
l.Info("duplicate app mapping detected; ignoring subsequent entry", "key", appRef)
|
||||
} else {
|
||||
cfg.appCRDMap[appRef] = &newRef
|
||||
}
|
||||
}
|
||||
c.config.Store(cfg)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
73
internal/lineagecontrollerwebhook/matcher.go
Normal file
73
internal/lineagecontrollerwebhook/matcher.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package lineagecontrollerwebhook
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"text/template"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
// matchName checks if the provided name matches any of the resource names in the array.
|
||||
// Each entry in resourceNames is treated as a Go template that gets rendered using the passed context.
|
||||
// A nil resourceNames array matches any string.
|
||||
func matchName(ctx context.Context, name string, templateContext map[string]string, resourceNames []string) bool {
|
||||
if resourceNames == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
logger := log.FromContext(ctx)
|
||||
for _, templateStr := range resourceNames {
|
||||
tmpl, err := template.New("resourceName").Parse(templateStr)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to parse resource name template", "template", templateStr)
|
||||
continue
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.Execute(&buf, templateContext)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to execute resource name template", "template", templateStr, "context", templateContext)
|
||||
continue
|
||||
}
|
||||
|
||||
if buf.String() == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func matchResourceToSelector(ctx context.Context, name string, templateContext, l map[string]string, s *cozyv1alpha1.CozystackResourceDefinitionResourceSelector) bool {
|
||||
sel, err := metav1.LabelSelectorAsSelector(&s.LabelSelector)
|
||||
if err != nil {
|
||||
log.FromContext(ctx).Error(err, "failed to convert label selector to selector")
|
||||
return false
|
||||
}
|
||||
labelMatches := sel.Matches(labels.Set(l))
|
||||
nameMatches := matchName(ctx, name, templateContext, s.ResourceNames)
|
||||
return labelMatches && nameMatches
|
||||
}
|
||||
|
||||
func matchResourceToSelectorArray(ctx context.Context, name string, templateContext, l map[string]string, ss []*cozyv1alpha1.CozystackResourceDefinitionResourceSelector) bool {
|
||||
for _, s := range ss {
|
||||
if matchResourceToSelector(ctx, name, templateContext, l, s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchResourceToExcludeInclude(ctx context.Context, name string, templateContext, l map[string]string, resources *cozyv1alpha1.CozystackResourceDefinitionResources) bool {
|
||||
if resources == nil {
|
||||
return false
|
||||
}
|
||||
if matchResourceToSelectorArray(ctx, name, templateContext, l, resources.Exclude) {
|
||||
return false
|
||||
}
|
||||
return matchResourceToSelectorArray(ctx, name, templateContext, l, resources.Include)
|
||||
}
|
||||
23
internal/lineagecontrollerwebhook/types.go
Normal file
23
internal/lineagecontrollerwebhook/types.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package lineagecontrollerwebhook
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
)
|
||||
|
||||
// +kubebuilder:webhook:path=/mutate-lineage,mutating=true,failurePolicy=Fail,sideEffects=None,groups="",resources=pods,secrets,services,persistentvolumeclaims,verbs=create;update,versions=v1,name=mlineage.cozystack.io,admissionReviewVersions={v1}
|
||||
type LineageControllerWebhook struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
decoder admission.Decoder
|
||||
dynClient dynamic.Interface
|
||||
mapper meta.RESTMapper
|
||||
config atomic.Value
|
||||
initOnce sync.Once
|
||||
}
|
||||
200
internal/lineagecontrollerwebhook/webhook.go
Normal file
200
internal/lineagecontrollerwebhook/webhook.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package lineagecontrollerwebhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cozystack/cozystack/pkg/lineage"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1"
|
||||
)
|
||||
|
||||
var (
|
||||
NoAncestors = fmt.Errorf("no managed apps found in lineage")
|
||||
AncestryAmbiguous = fmt.Errorf("object ancestry is ambiguous")
|
||||
)
|
||||
|
||||
// getResourceSelectors returns the appropriate CozystackResourceDefinitionResources for a given GroupKind
|
||||
func (h *LineageControllerWebhook) getResourceSelectors(gk schema.GroupKind, crd *cozyv1alpha1.CozystackResourceDefinition) *cozyv1alpha1.CozystackResourceDefinitionResources {
|
||||
switch {
|
||||
case gk.Group == "" && gk.Kind == "Secret":
|
||||
return &crd.Spec.Secrets
|
||||
case gk.Group == "" && gk.Kind == "Service":
|
||||
return &crd.Spec.Services
|
||||
case gk.Group == "networking.k8s.io" && gk.Kind == "Ingress":
|
||||
return &crd.Spec.Ingresses
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SetupWithManager registers the handler with the webhook server.
|
||||
func (h *LineageControllerWebhook) SetupWithManagerAsWebhook(mgr ctrl.Manager) error {
|
||||
cfg := rest.CopyConfig(mgr.GetConfig())
|
||||
|
||||
var err error
|
||||
h.dynClient, err = dynamic.NewForConfig(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpClient, err := rest.HTTPClientFor(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.mapper, err = apiutil.NewDynamicRESTMapper(cfg, httpClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.initConfig()
|
||||
// Register HTTP path -> handler.
|
||||
mgr.GetWebhookServer().Register("/mutate-lineage", &admission.Webhook{Handler: h})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InjectDecoder lets controller-runtime give us a decoder for AdmissionReview requests.
|
||||
func (h *LineageControllerWebhook) InjectDecoder(d admission.Decoder) error {
|
||||
h.decoder = d
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle is called for each AdmissionReview that matches the webhook config.
|
||||
func (h *LineageControllerWebhook) Handle(ctx context.Context, req admission.Request) admission.Response {
|
||||
logger := log.FromContext(ctx).WithValues(
|
||||
"gvk", req.Kind.String(),
|
||||
"namespace", req.Namespace,
|
||||
"name", req.Name,
|
||||
"operation", req.Operation,
|
||||
)
|
||||
warn := make(admission.Warnings, 0)
|
||||
|
||||
obj := &unstructured.Unstructured{}
|
||||
if err := h.decodeUnstructured(req, obj); err != nil {
|
||||
return admission.Errored(400, fmt.Errorf("decode object: %w", err))
|
||||
}
|
||||
|
||||
labels, err := h.computeLabels(ctx, obj)
|
||||
for {
|
||||
if err != nil && errors.Is(err, NoAncestors) {
|
||||
return admission.Allowed("object not managed by app")
|
||||
}
|
||||
if err != nil && errors.Is(err, AncestryAmbiguous) {
|
||||
warn = append(warn, "object ancestry ambiguous, using first ancestor found")
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error(err, "error computing lineage labels")
|
||||
return admission.Errored(500, fmt.Errorf("error computing lineage labels: %w", err))
|
||||
}
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
h.applyLabels(obj, labels)
|
||||
|
||||
mutated, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return admission.Errored(500, fmt.Errorf("marshal mutated pod: %w", err))
|
||||
}
|
||||
logger.V(1).Info("mutated pod", "namespace", obj.GetNamespace(), "name", obj.GetName())
|
||||
return admission.PatchResponseFromRaw(req.Object.Raw, mutated).WithWarnings(warn...)
|
||||
}
|
||||
|
||||
func (h *LineageControllerWebhook) computeLabels(ctx context.Context, o *unstructured.Unstructured) (map[string]string, error) {
|
||||
owners := lineage.WalkOwnershipGraph(ctx, h.dynClient, h.mapper, h, o)
|
||||
if len(owners) == 0 {
|
||||
return nil, NoAncestors
|
||||
}
|
||||
obj, err := owners[0].GetUnstructured(ctx, h.dynClient, h.mapper)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gv, err := schema.ParseGroupVersion(obj.GetAPIVersion())
|
||||
if err != nil {
|
||||
// should never happen, we got an APIVersion right from the API
|
||||
return nil, fmt.Errorf("could not parse APIVersion %s to a group and version: %w", obj.GetAPIVersion(), err)
|
||||
}
|
||||
if len(owners) > 1 {
|
||||
err = AncestryAmbiguous
|
||||
}
|
||||
labels := map[string]string{
|
||||
// truncate apigroup to first 63 chars
|
||||
"apps.cozystack.io/application.group": func(s string) string {
|
||||
if len(s) < 63 {
|
||||
return s
|
||||
}
|
||||
s = s[:63]
|
||||
for b := s[62]; !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')); s = s[:len(s)-1] {
|
||||
b = s[len(s)-1]
|
||||
}
|
||||
return s
|
||||
}(gv.Group),
|
||||
"apps.cozystack.io/application.kind": obj.GetKind(),
|
||||
"apps.cozystack.io/application.name": obj.GetName(),
|
||||
}
|
||||
templateLabels := map[string]string{
|
||||
"kind": strings.ToLower(obj.GetKind()),
|
||||
"name": obj.GetName(),
|
||||
"namespace": o.GetNamespace(),
|
||||
}
|
||||
cfg := h.config.Load().(*runtimeConfig)
|
||||
crd := cfg.appCRDMap[appRef{gv.Group, obj.GetKind()}]
|
||||
resourceSelectors := h.getResourceSelectors(o.GroupVersionKind().GroupKind(), crd)
|
||||
|
||||
labels[corev1alpha1.TenantResourceLabelKey] = func(b bool) string {
|
||||
if b {
|
||||
return corev1alpha1.TenantResourceLabelValue
|
||||
}
|
||||
return "false"
|
||||
}(matchResourceToExcludeInclude(ctx, o.GetName(), templateLabels, o.GetLabels(), resourceSelectors))
|
||||
return labels, err
|
||||
}
|
||||
|
||||
func (h *LineageControllerWebhook) applyLabels(o *unstructured.Unstructured, labels map[string]string) {
|
||||
existing := o.GetLabels()
|
||||
if existing == nil {
|
||||
existing = make(map[string]string)
|
||||
}
|
||||
for k, v := range labels {
|
||||
existing[k] = v
|
||||
}
|
||||
o.SetLabels(existing)
|
||||
}
|
||||
|
||||
func (h *LineageControllerWebhook) decodeUnstructured(req admission.Request, out *unstructured.Unstructured) error {
|
||||
if h.decoder != nil {
|
||||
if err := h.decoder.Decode(req, out); err == nil {
|
||||
return nil
|
||||
}
|
||||
if req.Kind.Group != "" || req.Kind.Kind != "" || req.Kind.Version != "" {
|
||||
out.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Group: req.Kind.Group,
|
||||
Version: req.Kind.Version,
|
||||
Kind: req.Kind.Kind,
|
||||
})
|
||||
if err := h.decoder.Decode(req, out); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(req.Object.Raw) == 0 {
|
||||
return errors.New("empty admission object")
|
||||
}
|
||||
return json.Unmarshal(req.Object.Raw, &out.Object)
|
||||
}
|
||||
99
internal/shared/crdmem/memory.go
Normal file
99
internal/shared/crdmem/memory.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package crdmem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type Memory struct {
|
||||
mu sync.RWMutex
|
||||
data map[string]cozyv1alpha1.CozystackResourceDefinition
|
||||
primed bool
|
||||
primeOnce sync.Once
|
||||
}
|
||||
|
||||
func New() *Memory {
|
||||
return &Memory{data: make(map[string]cozyv1alpha1.CozystackResourceDefinition)}
|
||||
}
|
||||
|
||||
var (
|
||||
global *Memory
|
||||
globalOnce sync.Once
|
||||
)
|
||||
|
||||
func Global() *Memory {
|
||||
globalOnce.Do(func() { global = New() })
|
||||
return global
|
||||
}
|
||||
|
||||
func (m *Memory) Upsert(obj *cozyv1alpha1.CozystackResourceDefinition) {
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.data[obj.Name] = *obj.DeepCopy()
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *Memory) Delete(name string) {
|
||||
m.mu.Lock()
|
||||
delete(m.data, name)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *Memory) Snapshot() []cozyv1alpha1.CozystackResourceDefinition {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
out := make([]cozyv1alpha1.CozystackResourceDefinition, 0, len(m.data))
|
||||
for _, v := range m.data {
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *Memory) IsPrimed() bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.primed
|
||||
}
|
||||
|
||||
type runnable func(context.Context) error
|
||||
|
||||
func (r runnable) Start(ctx context.Context) error { return r(ctx) }
|
||||
|
||||
func (m *Memory) EnsurePrimingWithManager(mgr ctrl.Manager) error {
|
||||
var errOut error
|
||||
m.primeOnce.Do(func() {
|
||||
errOut = mgr.Add(runnable(func(ctx context.Context) error {
|
||||
if ok := mgr.GetCache().WaitForCacheSync(ctx); !ok {
|
||||
return nil
|
||||
}
|
||||
var list cozyv1alpha1.CozystackResourceDefinitionList
|
||||
if err := mgr.GetClient().List(ctx, &list); err == nil {
|
||||
for i := range list.Items {
|
||||
m.Upsert(&list.Items[i])
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.primed = true
|
||||
m.mu.Unlock()
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
})
|
||||
return errOut
|
||||
}
|
||||
|
||||
func (m *Memory) ListFromCacheOrAPI(ctx context.Context, c client.Client) ([]cozyv1alpha1.CozystackResourceDefinition, error) {
|
||||
if m.IsPrimed() {
|
||||
return m.Snapshot(), nil
|
||||
}
|
||||
var list cozyv1alpha1.CozystackResourceDefinitionList
|
||||
if err := c.List(ctx, &list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list.Items, nil
|
||||
}
|
||||
293
internal/sse/server.go
Normal file
293
internal/sse/server.go
Normal file
@@ -0,0 +1,293 @@
|
||||
// Package sse provides a tiny Server-Sent Events server with pluggable routes.
|
||||
// No external deps; safe for quick demos and small dashboards.
|
||||
package sse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Options configures the SSE server.
|
||||
type Options struct {
|
||||
// Addr is the listening address, e.g. ":8080" or "127.0.0.1:0".
|
||||
Addr string
|
||||
|
||||
// IndexPath is the path serving a minimal live HTML page ("" to disable).
|
||||
// e.g. "/" or "/status"
|
||||
IndexPath string
|
||||
|
||||
// StreamPath is the SSE endpoint path, e.g. "/stream".
|
||||
StreamPath string
|
||||
|
||||
// Title for the index page (cosmetic).
|
||||
Title string
|
||||
|
||||
// AllowCORS, if true, sets Access-Control-Allow-Origin: * for /stream.
|
||||
AllowCORS bool
|
||||
|
||||
// ClientBuf is the per-client buffered message queue size.
|
||||
// If 0, defaults to 16. When full, new messages are dropped for that client.
|
||||
ClientBuf int
|
||||
|
||||
// Heartbeat sends a comment line every interval to keep connections alive.
|
||||
// If 0, defaults to 25s.
|
||||
Heartbeat time.Duration
|
||||
|
||||
// Logger (optional). If nil, log.Printf is used.
|
||||
Logger *log.Logger
|
||||
}
|
||||
|
||||
// Server is a simple SSE broadcaster.
|
||||
type Server struct {
|
||||
opts Options
|
||||
mux *http.ServeMux
|
||||
http *http.Server
|
||||
|
||||
clientsMu sync.RWMutex
|
||||
clients map[*client]struct{}
|
||||
|
||||
// latest holds the most recent payload (sent to new clients on connect).
|
||||
latestMu sync.RWMutex
|
||||
latest string
|
||||
}
|
||||
|
||||
type client struct {
|
||||
ch chan string
|
||||
closeCh chan struct{}
|
||||
flusher http.Flusher
|
||||
w http.ResponseWriter
|
||||
req *http.Request
|
||||
logf func(string, ...any)
|
||||
heartbeat time.Duration
|
||||
}
|
||||
|
||||
func New(opts Options) *Server {
|
||||
if opts.ClientBuf <= 0 {
|
||||
opts.ClientBuf = 16
|
||||
}
|
||||
if opts.Heartbeat <= 0 {
|
||||
opts.Heartbeat = 25 * time.Second
|
||||
}
|
||||
if opts.Addr == "" {
|
||||
opts.Addr = ":8080"
|
||||
}
|
||||
if opts.StreamPath == "" {
|
||||
opts.StreamPath = "/stream"
|
||||
}
|
||||
if opts.IndexPath == "" {
|
||||
opts.IndexPath = "/"
|
||||
}
|
||||
s := &Server{
|
||||
opts: opts,
|
||||
mux: http.NewServeMux(),
|
||||
clients: make(map[*client]struct{}),
|
||||
}
|
||||
s.routes()
|
||||
s.http = &http.Server{
|
||||
Addr: opts.Addr,
|
||||
Handler: s.mux,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) routes() {
|
||||
if s.opts.IndexPath != "" {
|
||||
s.mux.HandleFunc(s.opts.IndexPath, s.handleIndex)
|
||||
}
|
||||
s.mux.HandleFunc(s.opts.StreamPath, s.handleStream)
|
||||
}
|
||||
|
||||
func (s *Server) logf(format string, args ...any) {
|
||||
if s.opts.Logger != nil {
|
||||
s.opts.Logger.Printf(format, args...)
|
||||
} else {
|
||||
log.Printf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// ListenAndServe starts the HTTP server (blocking).
|
||||
func (s *Server) ListenAndServe() error {
|
||||
s.logf("sse: listening on http://%s (index=%s, stream=%s)", s.http.Addr, s.opts.IndexPath, s.opts.StreamPath)
|
||||
return s.http.ListenAndServe()
|
||||
}
|
||||
|
||||
// Shutdown gracefully stops the server.
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
s.clientsMu.Lock()
|
||||
for c := range s.clients {
|
||||
close(c.closeCh)
|
||||
}
|
||||
s.clientsMu.Unlock()
|
||||
return s.http.Shutdown(ctx)
|
||||
}
|
||||
|
||||
// Publish broadcasts a new payload to all clients and stores it as latest.
|
||||
func (s *Server) Publish(payload string) {
|
||||
// Store latest
|
||||
s.latestMu.Lock()
|
||||
s.latest = payload
|
||||
s.latestMu.Unlock()
|
||||
|
||||
// Broadcast
|
||||
s.clientsMu.RLock()
|
||||
defer s.clientsMu.RUnlock()
|
||||
for c := range s.clients {
|
||||
select {
|
||||
case c.ch <- payload:
|
||||
default:
|
||||
// Drop if client is slow (buffer full)
|
||||
if s.opts.Logger != nil {
|
||||
s.opts.Logger.Printf("sse: dropping message to slow client %p", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
page := indexTemplate(s.opts.Title, s.opts.StreamPath)
|
||||
_, _ = w.Write([]byte(page))
|
||||
}
|
||||
|
||||
func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) {
|
||||
// Required SSE headers
|
||||
if s.opts.AllowCORS {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c := &client{
|
||||
ch: make(chan string, s.opts.ClientBuf),
|
||||
closeCh: make(chan struct{}),
|
||||
flusher: flusher,
|
||||
w: w,
|
||||
req: r,
|
||||
logf: s.logf,
|
||||
heartbeat: s.opts.Heartbeat,
|
||||
}
|
||||
|
||||
// Register client
|
||||
s.clientsMu.Lock()
|
||||
s.clients[c] = struct{}{}
|
||||
s.clientsMu.Unlock()
|
||||
|
||||
// Initial comment to open the stream for some proxies
|
||||
fmt.Fprintf(w, ": connected %s\n\n", time.Now().Format(time.RFC3339))
|
||||
flusher.Flush()
|
||||
|
||||
// Send latest if any
|
||||
s.latestMu.RLock()
|
||||
latest := s.latest
|
||||
s.latestMu.RUnlock()
|
||||
if latest != "" {
|
||||
writeSSE(w, latest)
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
// Start pump
|
||||
go c.pump()
|
||||
|
||||
// Block until client disconnects
|
||||
<-r.Context().Done()
|
||||
|
||||
// Unregister client
|
||||
close(c.closeCh)
|
||||
s.clientsMu.Lock()
|
||||
delete(s.clients, c)
|
||||
s.clientsMu.Unlock()
|
||||
}
|
||||
|
||||
func (c *client) pump() {
|
||||
t := time.NewTicker(c.heartbeat)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-c.closeCh:
|
||||
return
|
||||
case msg := <-c.ch:
|
||||
writeSSE(c.w, msg)
|
||||
c.flusher.Flush()
|
||||
case <-t.C:
|
||||
// heartbeat comment (keeps connections alive through proxies)
|
||||
fmt.Fprint(c.w, ": hb\n\n")
|
||||
c.flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeSSE(w http.ResponseWriter, msg string) {
|
||||
// Split on lines; each needs its own "data:" field per the SSE spec
|
||||
lines := strings.Split(strings.TrimRight(msg, "\n"), "\n")
|
||||
for _, ln := range lines {
|
||||
fmt.Fprintf(w, "data: %s\n", ln)
|
||||
}
|
||||
fmt.Fprint(w, "\n")
|
||||
}
|
||||
|
||||
// Minimal index page with live updates
|
||||
func indexTemplate(title, streamPath string) string {
|
||||
if title == "" {
|
||||
title = "SSE Stream"
|
||||
}
|
||||
if streamPath == "" {
|
||||
streamPath = "/stream"
|
||||
}
|
||||
const tpl = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{{.Title}}</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; margin: 2rem; }
|
||||
pre { background:#111; color:#eee; padding:1rem; border-radius:12px; white-space:pre-wrap;}
|
||||
.status { margin-bottom: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{.Title}}</h1>
|
||||
<div class="status">Connecting…</div>
|
||||
<pre id="out"></pre>
|
||||
<script>
|
||||
const statusEl = document.querySelector('.status');
|
||||
const out = document.getElementById('out');
|
||||
const es = new EventSource('{{.Stream}}');
|
||||
es.onmessage = (e) => {
|
||||
// Replace content with the latest full snapshot
|
||||
if (e.data === "") return;
|
||||
// We accumulate until a blank 'data:' terminator; simpler approach: reset on first line.
|
||||
// For this demo, server always sends full content in one event, so just overwrite.
|
||||
out.textContent = (out._acc ?? "") + e.data + "\n";
|
||||
};
|
||||
es.addEventListener('open', () => { statusEl.textContent = "Connected"; out._acc = ""; });
|
||||
es.addEventListener('error', () => { statusEl.textContent = "Disconnected (browser will retry)…"; out._acc = ""; });
|
||||
// Optional: keep the latest only per message
|
||||
es.onmessage = (e) => {
|
||||
out.textContent = e.data + "\n";
|
||||
statusEl.textContent = "Connected";
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
page, _ := template.New("idx").Parse(tpl)
|
||||
var b strings.Builder
|
||||
_ = page.Execute(&b, map[string]any{
|
||||
"Title": title,
|
||||
"Stream": streamPath,
|
||||
})
|
||||
return b.String()
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
OUT=../_out/repos/apps
|
||||
TMP := $(shell mktemp -d)
|
||||
OUT=../../_out/repos/apps
|
||||
CHARTS := $(shell find . -maxdepth 2 -name Chart.yaml | awk -F/ '{print $$2}')
|
||||
|
||||
include ../../scripts/common-envs.mk
|
||||
|
||||
repo:
|
||||
cd .. && ../hack/package_chart.sh apps $(OUT) $(TMP) library
|
||||
rm -rf "$(OUT)"
|
||||
helm package -d "$(OUT)" $(CHARTS) --version $(COZYSTACK_VERSION)
|
||||
helm repo index "$(OUT)"
|
||||
|
||||
fix-chartnames:
|
||||
find . -maxdepth 2 -name Chart.yaml | awk -F/ '{print $$2}' | while read i; do sed -i "s/^name: .*/name: $$i/" "$$i/Chart.yaml"; done
|
||||
|
||||
gen-versions-map: fix-chartnames
|
||||
../../hack/gen_versions_map.sh
|
||||
|
||||
check-version-map: gen-versions-map
|
||||
git diff --exit-code -- versions_map
|
||||
fix-charts:
|
||||
find . -maxdepth 2 -name Chart.yaml | awk -F/ '{print $$2}' | while read i; do sed -i -e "s/^name: .*/name: $$i/" -e "s/^version: .*/version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process/g" "$$i/Chart.yaml"; done
|
||||
|
||||
@@ -4,6 +4,5 @@
|
||||
cd packages/core/installer
|
||||
make image-cozystack REGISTRY=YOUR_CUSTOM_REGISTRY
|
||||
make apply
|
||||
kubectl delete pod dashboard-redis-master-0 -n cozy-dashboard
|
||||
kubectl delete po -l app=source-controller -n cozy-fluxcd
|
||||
```
|
||||
|
||||
@@ -2,24 +2,6 @@ apiVersion: v2
|
||||
name: bucket
|
||||
description: S3 compatible storage
|
||||
icon: /logos/bucket.svg
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.2.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
|
||||
appVersion: "0.2.0"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
include ../../../scripts/package.mk
|
||||
|
||||
generate:
|
||||
readme-generator-for-helm -v values.yaml -s values.schema.json -r README.md
|
||||
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
|
||||
yq -o json -i '.properties = {}' values.schema.json
|
||||
../../../hack/update-crd.sh
|
||||
|
||||
@@ -12,7 +12,14 @@ spec:
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
interval: 1m0s
|
||||
timeout: 5m0s
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
remediation:
|
||||
retries: -1
|
||||
upgrade:
|
||||
force: true
|
||||
remediation:
|
||||
retries: -1
|
||||
values:
|
||||
bucketName: {{ .Release.Name }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"properties": {},
|
||||
"title": "Chart Values",
|
||||
"type": "object"
|
||||
}
|
||||
"title": "Chart Values",
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,6 @@ apiVersion: v2
|
||||
name: clickhouse
|
||||
description: Managed ClickHouse service
|
||||
icon: /logos/clickhouse.svg
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.11.1
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
|
||||
appVersion: "24.9.2"
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
CLICKHOUSE_BACKUP_TAG = $(shell awk '$$0 ~ /^version:/ {print $$2}' Chart.yaml)
|
||||
PRESET_ENUM := ["nano","micro","small","medium","large","xlarge","2xlarge"]
|
||||
|
||||
include ../../../scripts/common-envs.mk
|
||||
include ../../../scripts/package.mk
|
||||
|
||||
generate:
|
||||
readme-generator-for-helm -v values.yaml -s values.schema.json -r README.md
|
||||
yq -i -o json --indent 4 '.properties.resourcesPreset.enum = $(PRESET_ENUM)' values.schema.json
|
||||
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
|
||||
../../../hack/update-crd.sh
|
||||
|
||||
image:
|
||||
docker buildx build images/clickhouse-backup \
|
||||
--provenance false \
|
||||
--builder=$(BUILDER) \
|
||||
--platform=$(PLATFORM) \
|
||||
--tag $(REGISTRY)/clickhouse-backup:$(call settag,$(CLICKHOUSE_BACKUP_TAG)) \
|
||||
--cache-from type=registry,ref=$(REGISTRY)/clickhouse-backup:latest \
|
||||
--cache-to type=inline \
|
||||
--metadata-file images/clickhouse-backup.json \
|
||||
--push=$(PUSH) \
|
||||
--label "org.opencontainers.image.source=https://github.com/cozystack/cozystack" \
|
||||
--load=$(LOAD)
|
||||
$(BUILDX_ARGS)
|
||||
echo "$(REGISTRY)/clickhouse-backup:$(call settag,$(CLICKHOUSE_BACKUP_TAG))@$$(yq e '."containerimage.digest"' images/clickhouse-backup.json -o json -r)" \
|
||||
> images/clickhouse-backup.tag
|
||||
rm -f images/clickhouse-backup.json
|
||||
|
||||
@@ -23,35 +23,54 @@ For more details, read [Restic: Effective Backup from Stdin](https://blog.aenix.
|
||||
|
||||
### Common parameters
|
||||
|
||||
| Name | Description | Value |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| `replicas` | Number of Clickhouse replicas | `2` |
|
||||
| `shards` | Number of Clickhouse shards | `1` |
|
||||
| `resources` | Explicit CPU and memory configuration for each ClickHouse replica. When left empty, the preset defined in `resourcesPreset` is applied. | `{}` |
|
||||
| `resourcesPreset` | Default sizing preset used when `resources` is omitted. Allowed values: nano, micro, small, medium, large, xlarge, 2xlarge. | `small` |
|
||||
| `size` | Persistent Volume Claim size, available for application data | `10Gi` |
|
||||
| `storageClass` | StorageClass used to store the application data | `""` |
|
||||
| Name | Description | Type | Value |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
|
||||
| `replicas` | Number of Clickhouse replicas | `int` | `2` |
|
||||
| `shards` | Number of Clickhouse shards | `int` | `1` |
|
||||
| `resources` | Explicit CPU and memory configuration for each Clickhouse replica. When left empty, the preset defined in `resourcesPreset` is applied. | `*object` | `null` |
|
||||
| `resources.cpu` | CPU available to each replica | `*quantity` | `null` |
|
||||
| `resources.memory` | Memory (RAM) available to each replica | `*quantity` | `null` |
|
||||
| `resourcesPreset` | Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`. | `string` | `small` |
|
||||
| `size` | Persistent Volume Claim size, available for application data | `quantity` | `10Gi` |
|
||||
| `storageClass` | StorageClass used to store the data | `string` | `""` |
|
||||
|
||||
|
||||
### Application-specific parameters
|
||||
|
||||
| Name | Description | Value |
|
||||
| ---------------- | -------------------------------------------------------- | ----- |
|
||||
| `logStorageSize` | Size of Persistent Volume for logs | `2Gi` |
|
||||
| `logTTL` | TTL (expiration time) for query_log and query_thread_log | `15` |
|
||||
| `users` | Users configuration | `{}` |
|
||||
| Name | Description | Type | Value |
|
||||
| ---------------------- | ------------------------------------------------------------ | ------------------- | ------- |
|
||||
| `logStorageSize` | Size of Persistent Volume for logs | `quantity` | `2Gi` |
|
||||
| `logTTL` | TTL (expiration time) for `query_log` and `query_thread_log` | `int` | `15` |
|
||||
| `users` | Users configuration | `map[string]object` | `{...}` |
|
||||
| `users[name].password` | Password for the user | `*string` | `null` |
|
||||
| `users[name].readonly` | User is `readonly`, default is `false`. | `*bool` | `null` |
|
||||
|
||||
|
||||
### Backup parameters
|
||||
|
||||
| Name | Description | Value |
|
||||
| ------------------------ | ---------------------------------------------- | ------------------------------------------------------ |
|
||||
| `backup.enabled` | Enable periodic backups | `false` |
|
||||
| `backup.s3Region` | AWS S3 region where backups are stored | `us-east-1` |
|
||||
| `backup.s3Bucket` | S3 bucket used for storing backups | `s3.example.org/clickhouse-backups` |
|
||||
| `backup.schedule` | Cron schedule for automated backups | `0 2 * * *` |
|
||||
| `backup.cleanupStrategy` | Retention strategy for cleaning up old backups | `--keep-last=3 --keep-daily=3 --keep-within-weekly=1m` |
|
||||
| `backup.s3AccessKey` | Access key for S3, used for authentication | `oobaiRus9pah8PhohL1ThaeTa4UVa7gu` |
|
||||
| `backup.s3SecretKey` | Secret key for S3, used for authentication | `ju3eum4dekeich9ahM1te8waeGai0oog` |
|
||||
| `backup.resticPassword` | Password for Restic backup encryption | `ChaXoveekoh6eigh4siesheeda2quai0` |
|
||||
| Name | Description | Type | Value |
|
||||
| ------------------------ | ---------------------------------------------- | -------- | ------------------------------------------------------ |
|
||||
| `backup` | Backup configuration | `object` | `{}` |
|
||||
| `backup.enabled` | Enable regular backups, default is `false` | `bool` | `false` |
|
||||
| `backup.s3Region` | AWS S3 region where backups are stored | `string` | `us-east-1` |
|
||||
| `backup.s3Bucket` | S3 bucket used for storing backups | `string` | `s3.example.org/clickhouse-backups` |
|
||||
| `backup.schedule` | Cron schedule for automated backups | `string` | `0 2 * * *` |
|
||||
| `backup.cleanupStrategy` | Retention strategy for cleaning up old backups | `string` | `--keep-last=3 --keep-daily=3 --keep-within-weekly=1m` |
|
||||
| `backup.s3AccessKey` | Access key for S3, used for authentication | `string` | `<your-access-key>` |
|
||||
| `backup.s3SecretKey` | Secret key for S3, used for authentication | `string` | `<your-secret-key>` |
|
||||
| `backup.resticPassword` | Password for Restic backup encryption | `string` | `<password>` |
|
||||
|
||||
|
||||
### Clickhouse Keeper parameters
|
||||
|
||||
| Name | Description | Type | Value |
|
||||
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
|
||||
| `clickhouseKeeper` | Clickhouse Keeper configuration | `*object` | `null` |
|
||||
| `clickhouseKeeper.enabled` | Deploy ClickHouse Keeper for cluster coordination | `*bool` | `true` |
|
||||
| `clickhouseKeeper.size` | Persistent Volume Claim size, available for application data | `*quantity` | `1Gi` |
|
||||
| `clickhouseKeeper.resourcesPreset` | Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`. | `string` | `micro` |
|
||||
| `clickhouseKeeper.replicas` | Number of Keeper replicas | `*int` | `3` |
|
||||
|
||||
|
||||
## Parameter examples and reference
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
ghcr.io/cozystack/cozystack/clickhouse-backup:0.11.1@sha256:3faf7a4cebf390b9053763107482de175aa0fdb88c1e77424fd81100b1c3a205
|
||||
ghcr.io/cozystack/cozystack/clickhouse-backup:0.0.0@sha256:3faf7a4cebf390b9053763107482de175aa0fdb88c1e77424fd81100b1c3a205
|
||||
|
||||
96
packages/apps/clickhouse/templates/chkeeper.yaml
Normal file
96
packages/apps/clickhouse/templates/chkeeper.yaml
Normal file
@@ -0,0 +1,96 @@
|
||||
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
|
||||
{{- $clusterDomain := (index $cozyConfig.data "cluster-domain") | default "cozy.local" }}
|
||||
|
||||
{{- if .Values.clickhouseKeeper.enabled }}
|
||||
apiVersion: "clickhouse-keeper.altinity.com/v1"
|
||||
kind: "ClickHouseKeeperInstallation"
|
||||
metadata:
|
||||
name: "{{ .Release.Name }}-keeper"
|
||||
annotations:
|
||||
prometheus.io/port: "7000"
|
||||
prometheus.io/scrape: "true"
|
||||
spec:
|
||||
namespaceDomainPattern: "%s.svc.{{ $clusterDomain }}"
|
||||
configuration:
|
||||
clusters:
|
||||
- name: "cluster1"
|
||||
layout:
|
||||
replicasCount: {{ .Values.clickhouseKeeper.replicas }}
|
||||
settings:
|
||||
logger/level: "trace"
|
||||
logger/console: "true"
|
||||
listen_host: "0.0.0.0"
|
||||
keeper_server/four_letter_word_white_list: "*"
|
||||
keeper_server/coordination_settings/raft_logs_level: "information"
|
||||
prometheus/endpoint: "/metrics"
|
||||
prometheus/port: "7000"
|
||||
prometheus/metrics: "true"
|
||||
prometheus/events: "true"
|
||||
prometheus/asynchronous_metrics: "true"
|
||||
prometheus/status_info: "false"
|
||||
|
||||
defaults:
|
||||
templates:
|
||||
# Templates are specified as default for all clusters
|
||||
podTemplate: default
|
||||
dataVolumeClaimTemplate: default
|
||||
|
||||
templates:
|
||||
podTemplates:
|
||||
- name: default
|
||||
metadata:
|
||||
labels:
|
||||
app: "{{ .Release.Name }}-keeper"
|
||||
annotations:
|
||||
prometheus.io/port: "7000"
|
||||
prometheus.io/scrape: "true"
|
||||
spec:
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchExpressions:
|
||||
- key: "app"
|
||||
operator: In
|
||||
values:
|
||||
- "{{ .Release.Name }}-keeper"
|
||||
topologyKey: "kubernetes.io/hostname"
|
||||
containers:
|
||||
- name: clickhouse-keeper
|
||||
imagePullPolicy: IfNotPresent
|
||||
image: clickhouse/clickhouse-keeper:24.9.2.42
|
||||
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.clickhouseKeeper.resourcesPreset .Values.resources $) | nindent 20 }}
|
||||
securityContext:
|
||||
fsGroup: 101
|
||||
|
||||
volumeClaimTemplates:
|
||||
- name: default
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: "{{ .Values.clickhouseKeeper.size }}"
|
||||
---
|
||||
apiVersion: operator.victoriametrics.com/v1beta1
|
||||
kind: VMPodScrape
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-keeper
|
||||
namespace: {{ .Release.Namespace }}
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ .Release.Name }}-keeper
|
||||
namespaceSelector:
|
||||
matchNames:
|
||||
- {{ .Release.Namespace }}
|
||||
podMetricsEndpoints:
|
||||
- port: metrics
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
scheme: http
|
||||
relabelConfigs:
|
||||
- action: replace
|
||||
sourceLabels: [__meta_kubernetes_pod_node_name]
|
||||
targetLabel: instance
|
||||
{{- end }}
|
||||
@@ -91,6 +91,18 @@ spec:
|
||||
layout:
|
||||
shardsCount: {{ .Values.shards }}
|
||||
replicasCount: {{ .Values.replicas }}
|
||||
{{- if .Values.clickhouseKeeper.enabled }}
|
||||
zookeeper:
|
||||
nodes:
|
||||
{{- $replicas := int .Values.clickhouseKeeper.replicas }}
|
||||
{{- $release := .Release.Name }}
|
||||
{{- $namespace := .Release.Namespace }}
|
||||
{{- $clusterDomain := .Values.clusterDomain }}
|
||||
{{- range $i := until $replicas }}
|
||||
- host: "chk-{{ $release }}-keeper-cluster1-0-{{ $i }}.{{ $namespace }}.svc.{{ $clusterDomain }}"
|
||||
port: 2181
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
templates:
|
||||
volumeClaimTemplates:
|
||||
- name: data-volume-template
|
||||
|
||||
@@ -23,6 +23,9 @@ rules:
|
||||
- workloadmonitors
|
||||
resourceNames:
|
||||
- {{ .Release.Name }}
|
||||
{{- if .Values.clickhouseKeeper.enabled }}
|
||||
- {{ .Release.Name }}-keeper
|
||||
{{- end }}
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
kind: RoleBinding
|
||||
|
||||
@@ -11,3 +11,18 @@ spec:
|
||||
selector:
|
||||
app.kubernetes.io/instance: {{ $.Release.Name }}
|
||||
version: {{ $.Chart.Version }}
|
||||
{{- if .Values.clickhouseKeeper.enabled }}
|
||||
---
|
||||
apiVersion: cozystack.io/v1alpha1
|
||||
kind: WorkloadMonitor
|
||||
metadata:
|
||||
name: {{ $.Release.Name }}-keeper
|
||||
spec:
|
||||
replicas: {{ .Values.clickhouseKeeper.replicas }}
|
||||
minReplicas: 1
|
||||
kind: clickhouse
|
||||
type: clickhouse
|
||||
selector:
|
||||
app: {{ $.Release.Name }}-keeper
|
||||
version: {{ $.Chart.Version }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,100 +1,223 @@
|
||||
{
|
||||
"properties": {
|
||||
"backup": {
|
||||
"properties": {
|
||||
"cleanupStrategy": {
|
||||
"default": "--keep-last=3 --keep-daily=3 --keep-within-weekly=1m",
|
||||
"description": "Retention strategy for cleaning up old backups",
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"default": false,
|
||||
"description": "Enable periodic backups",
|
||||
"type": "boolean"
|
||||
},
|
||||
"resticPassword": {
|
||||
"default": "ChaXoveekoh6eigh4siesheeda2quai0",
|
||||
"description": "Password for Restic backup encryption",
|
||||
"type": "string"
|
||||
},
|
||||
"s3AccessKey": {
|
||||
"default": "oobaiRus9pah8PhohL1ThaeTa4UVa7gu",
|
||||
"description": "Access key for S3, used for authentication",
|
||||
"type": "string"
|
||||
},
|
||||
"s3Bucket": {
|
||||
"default": "s3.example.org/clickhouse-backups",
|
||||
"description": "S3 bucket used for storing backups",
|
||||
"type": "string"
|
||||
},
|
||||
"s3Region": {
|
||||
"default": "us-east-1",
|
||||
"description": "AWS S3 region where backups are stored",
|
||||
"type": "string"
|
||||
},
|
||||
"s3SecretKey": {
|
||||
"default": "ju3eum4dekeich9ahM1te8waeGai0oog",
|
||||
"description": "Secret key for S3, used for authentication",
|
||||
"type": "string"
|
||||
},
|
||||
"schedule": {
|
||||
"default": "0 2 * * *",
|
||||
"description": "Cron schedule for automated backups",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
"title": "Chart Values",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"backup": {
|
||||
"description": "Backup configuration",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"required": [
|
||||
"cleanupStrategy",
|
||||
"enabled",
|
||||
"resticPassword",
|
||||
"s3AccessKey",
|
||||
"s3Bucket",
|
||||
"s3Region",
|
||||
"s3SecretKey",
|
||||
"schedule"
|
||||
],
|
||||
"properties": {
|
||||
"cleanupStrategy": {
|
||||
"description": "Retention strategy for cleaning up old backups",
|
||||
"type": "string",
|
||||
"default": "--keep-last=3 --keep-daily=3 --keep-within-weekly=1m"
|
||||
},
|
||||
"logStorageSize": {
|
||||
"default": "2Gi",
|
||||
"description": "Size of Persistent Volume for logs",
|
||||
"type": "string"
|
||||
"enabled": {
|
||||
"description": "Enable regular backups, default is `false`",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"logTTL": {
|
||||
"default": 15,
|
||||
"description": "TTL (expiration time) for query_log and query_thread_log",
|
||||
"type": "number"
|
||||
"resticPassword": {
|
||||
"description": "Password for Restic backup encryption",
|
||||
"type": "string",
|
||||
"default": "\u003cpassword\u003e"
|
||||
},
|
||||
"s3AccessKey": {
|
||||
"description": "Access key for S3, used for authentication",
|
||||
"type": "string",
|
||||
"default": "\u003cyour-access-key\u003e"
|
||||
},
|
||||
"s3Bucket": {
|
||||
"description": "S3 bucket used for storing backups",
|
||||
"type": "string",
|
||||
"default": "s3.example.org/clickhouse-backups"
|
||||
},
|
||||
"s3Region": {
|
||||
"description": "AWS S3 region where backups are stored",
|
||||
"type": "string",
|
||||
"default": "us-east-1"
|
||||
},
|
||||
"s3SecretKey": {
|
||||
"description": "Secret key for S3, used for authentication",
|
||||
"type": "string",
|
||||
"default": "\u003cyour-secret-key\u003e"
|
||||
},
|
||||
"schedule": {
|
||||
"description": "Cron schedule for automated backups",
|
||||
"type": "string",
|
||||
"default": "0 2 * * *"
|
||||
}
|
||||
}
|
||||
},
|
||||
"clickhouseKeeper": {
|
||||
"description": "Clickhouse Keeper configuration",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"required": [
|
||||
"resourcesPreset"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Deploy ClickHouse Keeper for cluster coordination",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"replicas": {
|
||||
"default": 2,
|
||||
"description": "Number of Clickhouse replicas",
|
||||
"type": "number"
|
||||
},
|
||||
"resources": {
|
||||
"default": {},
|
||||
"description": "Explicit CPU and memory configuration for each ClickHouse replica. When left empty, the preset defined in `resourcesPreset` is applied.",
|
||||
"type": "object"
|
||||
"description": "Number of Keeper replicas",
|
||||
"type": "integer",
|
||||
"default": 3
|
||||
},
|
||||
"resourcesPreset": {
|
||||
"default": "small",
|
||||
"description": "Default sizing preset used when `resources` is omitted. Allowed values: nano, micro, small, medium, large, xlarge, 2xlarge.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"nano",
|
||||
"micro",
|
||||
"small",
|
||||
"medium",
|
||||
"large",
|
||||
"xlarge",
|
||||
"2xlarge"
|
||||
]
|
||||
},
|
||||
"shards": {
|
||||
"default": 1,
|
||||
"description": "Number of Clickhouse shards",
|
||||
"type": "number"
|
||||
"description": "Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.",
|
||||
"type": "string",
|
||||
"default": "micro",
|
||||
"enum": [
|
||||
"nano",
|
||||
"micro",
|
||||
"small",
|
||||
"medium",
|
||||
"large",
|
||||
"xlarge",
|
||||
"2xlarge"
|
||||
]
|
||||
},
|
||||
"size": {
|
||||
"default": "10Gi",
|
||||
"description": "Persistent Volume Claim size, available for application data",
|
||||
"type": "string"
|
||||
},
|
||||
"storageClass": {
|
||||
"default": "",
|
||||
"description": "StorageClass used to store the application data",
|
||||
"type": "string"
|
||||
"description": "Persistent Volume Claim size, available for application data",
|
||||
"default": "1Gi",
|
||||
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"x-kubernetes-int-or-string": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Chart Values",
|
||||
"type": "object"
|
||||
}
|
||||
"logStorageSize": {
|
||||
"description": "Size of Persistent Volume for logs",
|
||||
"default": "2Gi",
|
||||
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"x-kubernetes-int-or-string": true
|
||||
},
|
||||
"logTTL": {
|
||||
"description": "TTL (expiration time) for `query_log` and `query_thread_log`",
|
||||
"type": "integer",
|
||||
"default": 15
|
||||
},
|
||||
"replicas": {
|
||||
"description": "Number of Clickhouse replicas",
|
||||
"type": "integer",
|
||||
"default": 2
|
||||
},
|
||||
"resources": {
|
||||
"description": "Explicit CPU and memory configuration for each Clickhouse replica. When left empty, the preset defined in `resourcesPreset` is applied.",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"properties": {
|
||||
"cpu": {
|
||||
"description": "CPU available to each replica",
|
||||
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"x-kubernetes-int-or-string": true
|
||||
},
|
||||
"memory": {
|
||||
"description": "Memory (RAM) available to each replica",
|
||||
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"x-kubernetes-int-or-string": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesPreset": {
|
||||
"description": "Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.",
|
||||
"type": "string",
|
||||
"default": "small",
|
||||
"enum": [
|
||||
"nano",
|
||||
"micro",
|
||||
"small",
|
||||
"medium",
|
||||
"large",
|
||||
"xlarge",
|
||||
"2xlarge"
|
||||
]
|
||||
},
|
||||
"shards": {
|
||||
"description": "Number of Clickhouse shards",
|
||||
"type": "integer",
|
||||
"default": 1
|
||||
},
|
||||
"size": {
|
||||
"description": "Persistent Volume Claim size, available for application data",
|
||||
"default": "10Gi",
|
||||
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"x-kubernetes-int-or-string": true
|
||||
},
|
||||
"storageClass": {
|
||||
"description": "StorageClass used to store the data",
|
||||
"type": "string"
|
||||
},
|
||||
"users": {
|
||||
"description": "Users configuration",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"description": "Password for the user",
|
||||
"type": "string"
|
||||
},
|
||||
"readonly": {
|
||||
"description": "User is `readonly`, default is `false`.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,36 @@
|
||||
## @section Common parameters
|
||||
##
|
||||
## @param replicas Number of Clickhouse replicas
|
||||
## @param replicas {int} Number of Clickhouse replicas
|
||||
replicas: 2
|
||||
## @param shards Number of Clickhouse shards
|
||||
## @param shards {int} Number of Clickhouse shards
|
||||
shards: 1
|
||||
## @param resources Explicit CPU and memory configuration for each ClickHouse replica. When left empty, the preset defined in `resourcesPreset` is applied.
|
||||
resources: {}
|
||||
## @param resources {*resources} Explicit CPU and memory configuration for each Clickhouse replica. When left empty, the preset defined in `resourcesPreset` is applied.
|
||||
## @field resources.cpu {*quantity} CPU available to each replica
|
||||
## @field resources.memory {*quantity} Memory (RAM) available to each replica
|
||||
# resources:
|
||||
# cpu: 4000m
|
||||
# memory: 4Gi
|
||||
resources: {}
|
||||
|
||||
## @param resourcesPreset Default sizing preset used when `resources` is omitted. Allowed values: nano, micro, small, medium, large, xlarge, 2xlarge.
|
||||
|
||||
|
||||
## @param resourcesPreset {string enum:"nano,micro,small,medium,large,xlarge,2xlarge"} Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.
|
||||
resourcesPreset: "small"
|
||||
## @param size Persistent Volume Claim size, available for application data
|
||||
## @param size {quantity} Persistent Volume Claim size, available for application data
|
||||
size: 10Gi
|
||||
## @param storageClass StorageClass used to store the application data
|
||||
## @param storageClass {string} StorageClass used to store the data
|
||||
storageClass: ""
|
||||
|
||||
|
||||
## @section Application-specific parameters
|
||||
##
|
||||
## @param logStorageSize Size of Persistent Volume for logs
|
||||
## @param logStorageSize {quantity} Size of Persistent Volume for logs
|
||||
logStorageSize: 2Gi
|
||||
## @param logTTL TTL (expiration time) for query_log and query_thread_log
|
||||
## @param logTTL {int} TTL (expiration time) for `query_log` and `query_thread_log`
|
||||
logTTL: 15
|
||||
## @param users [object] Users configuration
|
||||
## @param users {map[string]user} Users configuration
|
||||
## @field user.password {*string} Password for the user
|
||||
## @field user.readonly {*bool} User is `readonly`, default is `false`.
|
||||
## Example:
|
||||
## users:
|
||||
## user1:
|
||||
@@ -38,21 +44,34 @@ users: {}
|
||||
|
||||
## @section Backup parameters
|
||||
|
||||
## @param backup.enabled Enable periodic backups
|
||||
## @param backup.s3Region AWS S3 region where backups are stored
|
||||
## @param backup.s3Bucket S3 bucket used for storing backups
|
||||
## @param backup.schedule Cron schedule for automated backups
|
||||
## @param backup.cleanupStrategy Retention strategy for cleaning up old backups
|
||||
## @param backup.s3AccessKey Access key for S3, used for authentication
|
||||
## @param backup.s3SecretKey Secret key for S3, used for authentication
|
||||
## @param backup.resticPassword Password for Restic backup encryption
|
||||
## @param backup {backup} Backup configuration
|
||||
## @field backup.enabled {bool} Enable regular backups, default is `false`
|
||||
## @field backup.s3Region {string} AWS S3 region where backups are stored
|
||||
## @field backup.s3Bucket {string} S3 bucket used for storing backups
|
||||
## @field backup.schedule {string} Cron schedule for automated backups
|
||||
## @field backup.cleanupStrategy {string} Retention strategy for cleaning up old backups
|
||||
## @field backup.s3AccessKey {string} Access key for S3, used for authentication
|
||||
## @field backup.s3SecretKey {string} Secret key for S3, used for authentication
|
||||
## @field backup.resticPassword {string} Password for Restic backup encryption
|
||||
backup:
|
||||
enabled: false
|
||||
s3Region: us-east-1
|
||||
s3Bucket: s3.example.org/clickhouse-backups
|
||||
s3Bucket: "s3.example.org/clickhouse-backups"
|
||||
schedule: "0 2 * * *"
|
||||
cleanupStrategy: "--keep-last=3 --keep-daily=3 --keep-within-weekly=1m"
|
||||
s3AccessKey: oobaiRus9pah8PhohL1ThaeTa4UVa7gu
|
||||
s3SecretKey: ju3eum4dekeich9ahM1te8waeGai0oog
|
||||
resticPassword: ChaXoveekoh6eigh4siesheeda2quai0
|
||||
s3AccessKey: "<your-access-key>"
|
||||
s3SecretKey: "<your-secret-key>"
|
||||
resticPassword: "<password>"
|
||||
|
||||
|
||||
## @section Clickhouse Keeper parameters
|
||||
## @param clickhouseKeeper {*clickhouseKeeper} Clickhouse Keeper configuration
|
||||
## @field clickhouseKeeper.enabled {*bool} Deploy ClickHouse Keeper for cluster coordination
|
||||
## @field clickhouseKeeper.size {*quantity} Persistent Volume Claim size, available for application data
|
||||
## @field clickhouseKeeper.resourcesPreset {string enum:"nano,micro,small,medium,large,xlarge,2xlarge"} Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.
|
||||
## @field clickhouseKeeper.replicas {*int} Number of Keeper replicas
|
||||
clickhouseKeeper:
|
||||
enabled: true
|
||||
size: 1Gi
|
||||
resourcesPreset: micro
|
||||
replicas: 3
|
||||
|
||||
@@ -2,24 +2,6 @@ apiVersion: v2
|
||||
name: ferretdb
|
||||
description: Managed FerretDB service
|
||||
icon: /logos/ferretdb.svg
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 1.0.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
|
||||
appVersion: 2.4.0
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
include ../../../scripts/package.mk
|
||||
PRESET_ENUM := ["nano","micro","small","medium","large","xlarge","2xlarge"]
|
||||
|
||||
generate:
|
||||
readme-generator-for-helm -v values.yaml -s values.schema.json -r README.md
|
||||
yq -i -o json --indent 4 '.properties.resourcesPreset.enum = $(PRESET_ENUM)' values.schema.json
|
||||
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
|
||||
../../../hack/update-crd.sh
|
||||
|
||||
update:
|
||||
tag=$$(git ls-remote --tags --sort="v:refname" https://github.com/FerretDB/FerretDB | awk -F'[/^]' '{sub("^v", "", $$3)} END{print $$3}') && \
|
||||
|
||||
@@ -8,42 +8,52 @@ Internally, FerretDB service is backed by Postgres.
|
||||
|
||||
### Common parameters
|
||||
|
||||
| Name | Description | Value |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| `replicas` | Number of replicas | `2` |
|
||||
| `resources` | Explicit CPU and memory configuration for each FerretDB replica. When left empty, the preset defined in `resourcesPreset` is applied. | `{}` |
|
||||
| `resourcesPreset` | Default sizing preset used when `resources` is omitted. Allowed values: none, nano, micro, small, medium, large, xlarge, 2xlarge. | `micro` |
|
||||
| `size` | Persistent Volume size | `10Gi` |
|
||||
| `storageClass` | StorageClass used to store the data | `""` |
|
||||
| `external` | Enable external access from outside the cluster | `false` |
|
||||
| Name | Description | Type | Value |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
|
||||
| `replicas` | Number of replicas | `int` | `2` |
|
||||
| `resources` | Explicit CPU and memory configuration for each FerretDB replica. When left empty, the preset defined in `resourcesPreset` is applied. | `*object` | `null` |
|
||||
| `resources.cpu` | CPU available to each replica | `*quantity` | `null` |
|
||||
| `resources.memory` | Memory (RAM) available to each replica | `*quantity` | `null` |
|
||||
| `resourcesPreset` | Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`. | `string` | `micro` |
|
||||
| `size` | Persistent Volume Claim size, available for application data | `quantity` | `10Gi` |
|
||||
| `storageClass` | StorageClass used to store the data | `string` | `""` |
|
||||
| `external` | Enable external access from outside the cluster | `bool` | `false` |
|
||||
|
||||
|
||||
### Application-specific parameters
|
||||
|
||||
| Name | Description | Value |
|
||||
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------- | ----- |
|
||||
| `quorum.minSyncReplicas` | Minimum number of synchronous replicas that must acknowledge a transaction before it is considered committed | `0` |
|
||||
| `quorum.maxSyncReplicas` | Maximum number of synchronous replicas that can acknowledge a transaction (must be lower than the total number of replicas) | `0` |
|
||||
| `users` | Users configuration | `{}` |
|
||||
| Name | Description | Type | Value |
|
||||
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------- | ------------------- | ------- |
|
||||
| `quorum` | Configuration for the quorum-based synchronous replication | `object` | `{}` |
|
||||
| `quorum.minSyncReplicas` | Minimum number of synchronous replicas that must acknowledge a transaction before it is considered committed | `int` | `0` |
|
||||
| `quorum.maxSyncReplicas` | Maximum number of synchronous replicas that can acknowledge a transaction (must be lower than the total number of replicas) | `int` | `0` |
|
||||
| `users` | Users configuration | `map[string]object` | `{...}` |
|
||||
| `users[name].password` | Password for the user | `*string` | `null` |
|
||||
|
||||
|
||||
### Backup parameters
|
||||
|
||||
| Name | Description | Value |
|
||||
| ------------------------ | ---------------------------------------------------------- | ----------------------------------- |
|
||||
| `backup.enabled` | Enable regular backups | `false` |
|
||||
| `backup.schedule` | Cron schedule for automated backups | `0 2 * * * *` |
|
||||
| `backup.retentionPolicy` | Retention policy | `30d` |
|
||||
| `backup.destinationPath` | Path to store the backup (i.e. s3://bucket/path/to/folder) | `s3://bucket/path/to/folder/` |
|
||||
| `backup.endpointURL` | S3 Endpoint used to upload data to the cloud | `http://minio-gateway-service:9000` |
|
||||
| `backup.s3AccessKey` | Access key for S3, used for authentication | `oobaiRus9pah8PhohL1ThaeTa4UVa7gu` |
|
||||
| `backup.s3SecretKey` | Secret key for S3, used for authentication | `ju3eum4dekeich9ahM1te8waeGai0oog` |
|
||||
| Name | Description | Type | Value |
|
||||
| ------------------------ | ---------------------------------------------------------- | -------- | ----------------------------------- |
|
||||
| `backup` | Backup configuration | `object` | `{}` |
|
||||
| `backup.enabled` | Enable regular backups, default is `false`. | `bool` | `false` |
|
||||
| `backup.schedule` | Cron schedule for automated backups | `string` | `0 2 * * * *` |
|
||||
| `backup.retentionPolicy` | Retention policy | `string` | `30d` |
|
||||
| `backup.endpointURL` | S3 Endpoint used to upload data to the cloud | `string` | `http://minio-gateway-service:9000` |
|
||||
| `backup.destinationPath` | Path to store the backup (i.e. s3://bucket/path/to/folder) | `string` | `s3://bucket/path/to/folder/` |
|
||||
| `backup.s3AccessKey` | Access key for S3, used for authentication | `string` | `<your-access-key>` |
|
||||
| `backup.s3SecretKey` | Secret key for S3, used for authentication | `string` | `<your-secret-key>` |
|
||||
|
||||
|
||||
### Bootstrap (recovery) parameters
|
||||
|
||||
| Name | Description | Value |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| `bootstrap.enabled` | Restore database cluster from a backup | `false` |
|
||||
| `bootstrap.recoveryTime` | Timestamp (PITR) up to which recovery will proceed, expressed in RFC 3339 format. If left empty, will restore latest | `""` |
|
||||
| `bootstrap.oldName` | Name of database cluster before deleting | `""` |
|
||||
| Name | Description | Type | Value |
|
||||
| ------------------------ | --------------------------------------------------------------------------------------------------------------------- | --------- | ------- |
|
||||
| `bootstrap` | Bootstrap (recovery) configuration | `object` | `{}` |
|
||||
| `bootstrap.enabled` | Restore database cluster from a backup | `*bool` | `false` |
|
||||
| `bootstrap.recoveryTime` | Timestamp (PITR) up to which recovery will proceed, expressed in RFC 3339 format. If left empty, will restore latest. | `*string` | `""` |
|
||||
| `bootstrap.oldName` | Name of database cluster before deleting | `*string` | `""` |
|
||||
|
||||
|
||||
## Parameter examples and reference
|
||||
|
||||
|
||||
@@ -1,120 +1,187 @@
|
||||
{
|
||||
"properties": {
|
||||
"backup": {
|
||||
"properties": {
|
||||
"destinationPath": {
|
||||
"default": "s3://bucket/path/to/folder/",
|
||||
"description": "Path to store the backup (i.e. s3://bucket/path/to/folder)",
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"default": false,
|
||||
"description": "Enable regular backups",
|
||||
"type": "boolean"
|
||||
},
|
||||
"endpointURL": {
|
||||
"default": "http://minio-gateway-service:9000",
|
||||
"description": "S3 Endpoint used to upload data to the cloud",
|
||||
"type": "string"
|
||||
},
|
||||
"retentionPolicy": {
|
||||
"default": "30d",
|
||||
"description": "Retention policy",
|
||||
"type": "string"
|
||||
},
|
||||
"s3AccessKey": {
|
||||
"default": "oobaiRus9pah8PhohL1ThaeTa4UVa7gu",
|
||||
"description": "Access key for S3, used for authentication",
|
||||
"type": "string"
|
||||
},
|
||||
"s3SecretKey": {
|
||||
"default": "ju3eum4dekeich9ahM1te8waeGai0oog",
|
||||
"description": "Secret key for S3, used for authentication",
|
||||
"type": "string"
|
||||
},
|
||||
"schedule": {
|
||||
"default": "0 2 * * * *",
|
||||
"description": "Cron schedule for automated backups",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
"title": "Chart Values",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"backup": {
|
||||
"description": "Backup configuration",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"required": [
|
||||
"destinationPath",
|
||||
"enabled",
|
||||
"endpointURL",
|
||||
"retentionPolicy",
|
||||
"s3AccessKey",
|
||||
"s3SecretKey",
|
||||
"schedule"
|
||||
],
|
||||
"properties": {
|
||||
"destinationPath": {
|
||||
"description": "Path to store the backup (i.e. s3://bucket/path/to/folder)",
|
||||
"type": "string",
|
||||
"default": "s3://bucket/path/to/folder/"
|
||||
},
|
||||
"bootstrap": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"default": false,
|
||||
"description": "Restore database cluster from a backup",
|
||||
"type": "boolean"
|
||||
},
|
||||
"oldName": {
|
||||
"default": "",
|
||||
"description": "Name of database cluster before deleting",
|
||||
"type": "string"
|
||||
},
|
||||
"recoveryTime": {
|
||||
"default": "",
|
||||
"description": "Timestamp (PITR) up to which recovery will proceed, expressed in RFC 3339 format. If left empty, will restore latest",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
"enabled": {
|
||||
"description": "Enable regular backups, default is `false`.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"external": {
|
||||
"default": false,
|
||||
"description": "Enable external access from outside the cluster",
|
||||
"type": "boolean"
|
||||
"endpointURL": {
|
||||
"description": "S3 Endpoint used to upload data to the cloud",
|
||||
"type": "string",
|
||||
"default": "http://minio-gateway-service:9000"
|
||||
},
|
||||
"quorum": {
|
||||
"properties": {
|
||||
"maxSyncReplicas": {
|
||||
"default": 0,
|
||||
"description": "Maximum number of synchronous replicas that can acknowledge a transaction (must be lower than the total number of replicas)",
|
||||
"type": "number"
|
||||
},
|
||||
"minSyncReplicas": {
|
||||
"default": 0,
|
||||
"description": "Minimum number of synchronous replicas that must acknowledge a transaction before it is considered committed",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
"retentionPolicy": {
|
||||
"description": "Retention policy",
|
||||
"type": "string",
|
||||
"default": "30d"
|
||||
},
|
||||
"replicas": {
|
||||
"default": 2,
|
||||
"description": "Number of replicas",
|
||||
"type": "number"
|
||||
"s3AccessKey": {
|
||||
"description": "Access key for S3, used for authentication",
|
||||
"type": "string",
|
||||
"default": "\u003cyour-access-key\u003e"
|
||||
},
|
||||
"resources": {
|
||||
"default": {},
|
||||
"description": "Explicit CPU and memory configuration for each FerretDB replica. When left empty, the preset defined in `resourcesPreset` is applied.",
|
||||
"type": "object"
|
||||
"s3SecretKey": {
|
||||
"description": "Secret key for S3, used for authentication",
|
||||
"type": "string",
|
||||
"default": "\u003cyour-secret-key\u003e"
|
||||
},
|
||||
"resourcesPreset": {
|
||||
"default": "micro",
|
||||
"description": "Default sizing preset used when `resources` is omitted. Allowed values: none, nano, micro, small, medium, large, xlarge, 2xlarge.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"nano",
|
||||
"micro",
|
||||
"small",
|
||||
"medium",
|
||||
"large",
|
||||
"xlarge",
|
||||
"2xlarge"
|
||||
]
|
||||
},
|
||||
"size": {
|
||||
"default": "10Gi",
|
||||
"description": "Persistent Volume size",
|
||||
"type": "string"
|
||||
},
|
||||
"storageClass": {
|
||||
"default": "",
|
||||
"description": "StorageClass used to store the data",
|
||||
"type": "string"
|
||||
"schedule": {
|
||||
"description": "Cron schedule for automated backups",
|
||||
"type": "string",
|
||||
"default": "0 2 * * * *"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Chart Values",
|
||||
"type": "object"
|
||||
}
|
||||
"bootstrap": {
|
||||
"description": "Bootstrap (recovery) configuration",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Restore database cluster from a backup",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"oldName": {
|
||||
"description": "Name of database cluster before deleting",
|
||||
"type": "string"
|
||||
},
|
||||
"recoveryTime": {
|
||||
"description": "Timestamp (PITR) up to which recovery will proceed, expressed in RFC 3339 format. If left empty, will restore latest.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"external": {
|
||||
"description": "Enable external access from outside the cluster",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"quorum": {
|
||||
"description": "Configuration for the quorum-based synchronous replication",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"required": [
|
||||
"maxSyncReplicas",
|
||||
"minSyncReplicas"
|
||||
],
|
||||
"properties": {
|
||||
"maxSyncReplicas": {
|
||||
"description": "Maximum number of synchronous replicas that can acknowledge a transaction (must be lower than the total number of replicas)",
|
||||
"type": "integer",
|
||||
"default": 0
|
||||
},
|
||||
"minSyncReplicas": {
|
||||
"description": "Minimum number of synchronous replicas that must acknowledge a transaction before it is considered committed",
|
||||
"type": "integer",
|
||||
"default": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"replicas": {
|
||||
"description": "Number of replicas",
|
||||
"type": "integer",
|
||||
"default": 2
|
||||
},
|
||||
"resources": {
|
||||
"description": "Explicit CPU and memory configuration for each FerretDB replica. When left empty, the preset defined in `resourcesPreset` is applied.",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"properties": {
|
||||
"cpu": {
|
||||
"description": "CPU available to each replica",
|
||||
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"x-kubernetes-int-or-string": true
|
||||
},
|
||||
"memory": {
|
||||
"description": "Memory (RAM) available to each replica",
|
||||
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"x-kubernetes-int-or-string": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesPreset": {
|
||||
"description": "Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.",
|
||||
"type": "string",
|
||||
"default": "micro",
|
||||
"enum": [
|
||||
"nano",
|
||||
"micro",
|
||||
"small",
|
||||
"medium",
|
||||
"large",
|
||||
"xlarge",
|
||||
"2xlarge"
|
||||
]
|
||||
},
|
||||
"size": {
|
||||
"description": "Persistent Volume Claim size, available for application data",
|
||||
"default": "10Gi",
|
||||
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"x-kubernetes-int-or-string": true
|
||||
},
|
||||
"storageClass": {
|
||||
"description": "StorageClass used to store the data",
|
||||
"type": "string"
|
||||
},
|
||||
"users": {
|
||||
"description": "Users configuration",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"description": "Password for the user",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,35 @@
|
||||
## @section Common parameters
|
||||
##
|
||||
## @param replicas Number of replicas
|
||||
## @param replicas {int} Number of replicas
|
||||
replicas: 2
|
||||
## @param resources Explicit CPU and memory configuration for each FerretDB replica. When left empty, the preset defined in `resourcesPreset` is applied.
|
||||
resources: {}
|
||||
## @param resources {*resources} Explicit CPU and memory configuration for each FerretDB replica. When left empty, the preset defined in `resourcesPreset` is applied.
|
||||
## @field resources.cpu {*quantity} CPU available to each replica
|
||||
## @field resources.memory {*quantity} Memory (RAM) available to each replica
|
||||
# resources:
|
||||
# cpu: 4000m
|
||||
# memory: 4Gi
|
||||
## @param resourcesPreset Default sizing preset used when `resources` is omitted. Allowed values: none, nano, micro, small, medium, large, xlarge, 2xlarge.
|
||||
resourcesPreset: "micro"
|
||||
## @param size Persistent Volume size
|
||||
size: 10Gi
|
||||
## @param storageClass StorageClass used to store the data
|
||||
storageClass: ""
|
||||
## @param external Enable external access from outside the cluster
|
||||
external: false
|
||||
resources: {}
|
||||
|
||||
## @param resourcesPreset {string enum:"nano,micro,small,medium,large,xlarge,2xlarge"} Default sizing preset used when `resources` is omitted. Allowed values: `nano`, `micro`, `small`, `medium`, `large`, `xlarge`, `2xlarge`.
|
||||
resourcesPreset: "micro"
|
||||
## @param size {quantity} Persistent Volume Claim size, available for application data
|
||||
size: 10Gi
|
||||
## @param storageClass {string} StorageClass used to store the data
|
||||
storageClass: ""
|
||||
## @param external {bool} Enable external access from outside the cluster
|
||||
external: false
|
||||
|
||||
## @section Application-specific parameters
|
||||
##
|
||||
## Configuration for the quorum-based synchronous replication
|
||||
## @param quorum.minSyncReplicas Minimum number of synchronous replicas that must acknowledge a transaction before it is considered committed
|
||||
## @param quorum.maxSyncReplicas Maximum number of synchronous replicas that can acknowledge a transaction (must be lower than the total number of replicas)
|
||||
## @param quorum {quorum} Configuration for the quorum-based synchronous replication
|
||||
## @field quorum.minSyncReplicas {int} Minimum number of synchronous replicas that must acknowledge a transaction before it is considered committed
|
||||
## @field quorum.maxSyncReplicas {int} Maximum number of synchronous replicas that can acknowledge a transaction (must be lower than the total number of replicas)
|
||||
quorum:
|
||||
minSyncReplicas: 0
|
||||
maxSyncReplicas: 0
|
||||
## @param users [object] Users configuration
|
||||
|
||||
## @param users {map[string]user} Users configuration
|
||||
## @field user.password {*string} Password for the user
|
||||
## Example:
|
||||
## users:
|
||||
## user1:
|
||||
@@ -36,30 +40,34 @@ quorum:
|
||||
users: {}
|
||||
|
||||
|
||||
|
||||
## @section Backup parameters
|
||||
##
|
||||
## @param backup.enabled Enable regular backups
|
||||
## @param backup.schedule Cron schedule for automated backups
|
||||
## @param backup.retentionPolicy Retention policy
|
||||
## @param backup.destinationPath Path to store the backup (i.e. s3://bucket/path/to/folder)
|
||||
## @param backup.endpointURL S3 Endpoint used to upload data to the cloud
|
||||
## @param backup.s3AccessKey Access key for S3, used for authentication
|
||||
## @param backup.s3SecretKey Secret key for S3, used for authentication
|
||||
|
||||
## @param backup {backup} Backup configuration
|
||||
## @field backup.enabled {bool} Enable regular backups, default is `false`.
|
||||
## @field backup.schedule {string} Cron schedule for automated backups
|
||||
## @field backup.retentionPolicy {string} Retention policy
|
||||
## @field backup.endpointURL {string} S3 Endpoint used to upload data to the cloud
|
||||
## @field backup.destinationPath {string} Path to store the backup (i.e. s3://bucket/path/to/folder)
|
||||
## @field backup.s3AccessKey {string} Access key for S3, used for authentication
|
||||
## @field backup.s3SecretKey {string} Secret key for S3, used for authentication
|
||||
backup:
|
||||
enabled: false
|
||||
retentionPolicy: 30d
|
||||
destinationPath: s3://bucket/path/to/folder/
|
||||
endpointURL: http://minio-gateway-service:9000
|
||||
schedule: "0 2 * * * *"
|
||||
s3AccessKey: oobaiRus9pah8PhohL1ThaeTa4UVa7gu
|
||||
s3SecretKey: ju3eum4dekeich9ahM1te8waeGai0oog
|
||||
retentionPolicy: 30d
|
||||
endpointURL: http://minio-gateway-service:9000
|
||||
destinationPath: s3://bucket/path/to/folder/
|
||||
s3AccessKey: "<your-access-key>"
|
||||
s3SecretKey: "<your-secret-key>"
|
||||
|
||||
|
||||
## @section Bootstrap (recovery) parameters
|
||||
##
|
||||
## @param bootstrap.enabled Restore database cluster from a backup
|
||||
## @param bootstrap.recoveryTime Timestamp (PITR) up to which recovery will proceed, expressed in RFC 3339 format. If left empty, will restore latest
|
||||
## @param bootstrap.oldName Name of database cluster before deleting
|
||||
## @param bootstrap {bootstrap} Bootstrap (recovery) configuration
|
||||
## @field bootstrap.enabled {*bool} Restore database cluster from a backup
|
||||
## @field bootstrap.recoveryTime {*string} Timestamp (PITR) up to which recovery will proceed, expressed in RFC 3339 format. If left empty, will restore latest.
|
||||
## @field bootstrap.oldName {*string} Name of database cluster before deleting
|
||||
##
|
||||
bootstrap:
|
||||
enabled: false
|
||||
|
||||
@@ -2,24 +2,6 @@ apiVersion: v2
|
||||
name: http-cache
|
||||
description: Layer7 load balancer and caching service
|
||||
icon: /logos/nginx.svg
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.6.1
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
|
||||
appVersion: "1.25.3"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
NGINX_CACHE_TAG = $(shell awk '$$1 == "version:" {print $$2}' Chart.yaml)
|
||||
PRESET_ENUM := ["nano","micro","small","medium","large","xlarge","2xlarge"]
|
||||
|
||||
include ../../../scripts/common-envs.mk
|
||||
include ../../../scripts/package.mk
|
||||
@@ -8,24 +7,18 @@ image: image-nginx
|
||||
|
||||
image-nginx:
|
||||
docker buildx build images/nginx-cache \
|
||||
--provenance false \
|
||||
--builder=$(BUILDER) \
|
||||
--platform=$(PLATFORM) \
|
||||
--tag $(REGISTRY)/nginx-cache:$(call settag,$(NGINX_CACHE_TAG)) \
|
||||
--cache-from type=registry,ref=$(REGISTRY)/nginx-cache:latest \
|
||||
--cache-to type=inline \
|
||||
--metadata-file images/nginx-cache.json \
|
||||
--push=$(PUSH) \
|
||||
--label "org.opencontainers.image.source=https://github.com/cozystack/cozystack" \
|
||||
--load=$(LOAD)
|
||||
$(BUILDX_ARGS)
|
||||
echo "$(REGISTRY)/nginx-cache:$(call settag,$(NGINX_CACHE_TAG))@$$(yq e '."containerimage.digest"' images/nginx-cache.json -o json -r)" \
|
||||
> images/nginx-cache.tag
|
||||
rm -f images/nginx-cache.json
|
||||
|
||||
generate:
|
||||
readme-generator-for-helm -v values.yaml -s values.schema.json -r README.md
|
||||
yq -i -o json --indent 4 '.properties.haproxy.properties.resourcesPreset.enum = $(PRESET_ENUM)' values.schema.json
|
||||
yq -i -o json --indent 4 '.properties.nginx.properties.resourcesPreset.enum = $(PRESET_ENUM)' values.schema.json
|
||||
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
|
||||
../../../hack/update-crd.sh
|
||||
|
||||
update:
|
||||
tag=$$(git ls-remote --tags --sort="v:refname" https://github.com/chrislim2888/IP2Location-C-Library | awk -F'[/^]' 'END{print $$3}') && \
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user